I'm developing a companion app for an online magazine called The Hair Society (thehairsociety.org). Thing is, I need to retrieve the existing articles for the app to actually make sense.
I've tried many different solutions (NewsAPI, SwiftyJSON, Kanna, etc.) but none of which have worked - which is probably due to my lack of sense of where to put things.
Here's some code:
ArticlesView + ArticleRow:
Code Block // // ArticlesView.swift // Hair Society Go // // Created by Joshua Srery on 11/29/20. // import SwiftUI struct ArticlesView: View { var body: some View { NavigationView { List { ForEach(0 ..< 5) { item in NavigationLink(destination: ArticleView(title: "Replace with Title var", image: "Replace with Img var", content: "Replace with Content var", author: "Replace with Author var", date: "Replace with Date var")) { ArticleRow(image: "Replace with Img var", title: "Replace with Title var", author: "Replace with Author var", date: "Replace with Date var") } } } .navigationTitle("Articles") .toolbar(content: { Menu { Button("Date", action: {}) Button("Title", action: {}) Button("Customize…", action: {}) } label: { Label("Filter", systemImage: "line.horizontal.3.decrease.circle") } }) } } } struct ArticleRow: View { let image: String let title: String let author: String let date: String var body: some View { HStack { Image(image) .resizable() .frame(minWidth: 75, maxWidth: 100, maxHeight: 75) .cornerRadius(12) VStack(alignment: .leading, content: { Text(title) .font(.headline) Text("\(author) • \(date)") .font(.subheadline) }) } } }
That filter option will probably show in a later thread.
ArticleView:
Code Block // // ArticleView.swift // Hair Society Go // // Created by Joshua Srery on 12/16/20. // import SwiftUI struct ArticleView: View { let title: String let image: String let content: String let author: String let date: String var body: some View { ScrollView { GeometryReader { geometry in ZStack { if geometry.frame(in: .global).minY <= 0 { Image(image) .resizable() .aspectRatio(contentMode: .fill) .frame(width: geometry.size.width, height: geometry.size.height) .offset(y: geometry.frame(in: .global).minY/9) .clipped() } else { Image(image) .resizable() .aspectRatio(contentMode: .fill) .frame(width: geometry.size.width, height: geometry.size.height + geometry.frame(in: .global).minY) .clipped() .offset(y: -geometry.frame(in: .global).minY) } } } .frame(height: 400) VStack(alignment: .leading) { Text(title) .font(.largeTitle) .bold() .lineLimit(nil) .padding(.top, 10) Text("\(author) • \(date)") .foregroundColor(.gray) .padding(.top, 10) AdvertisementView(ad: "Replace with Ad var") .padding(.top) .frame(width: 350) Text(content) .lineLimit(nil) .padding(.top, 15) } .frame(minWidth: 350) .padding(.horizontal, 15) } .edgesIgnoringSafeArea(.top) } } struct AdvertisementView: View { let ad: String var body: some View { VStack { Text("Advertisement") .font(Font.system(.body).smallCaps()) .foregroundColor(.secondary) .bold() .tracking(1.0) Image(ad) .resizable() .frame(height: 37.5) Text("Advertisement") .font(Font.system(.body).smallCaps()) .foregroundColor(.secondary) .bold() .tracking(1.0) }.padding(.vertical, 3) .background(Color.white) .cornerRadius(25) .shadow(radius: 10) } }
OK, let's concentrate on this issue.how can I use XMLParser to parse that specific data (title, image, author, date, content, etc.) and map it to the specific variables?
If you want to work with XMLParser, you need to implement a class conforming to XMLParserDelegate.
Creating a subclass of XMLParser and making itself conform to XMLParserDelegate is an often found pattern, though it is not necessary.
Code Block import Foundation struct Article { var title: String = "" var date: Date? var author: String? var img: URL? /// content in HTML var content: String = "" } class ArticlesParser: XMLParser { // Public property to hold the result var articles: [Article] = [] var dateTimeZone = TimeZone(abbreviation: "GMT-6") lazy var dateFormater: DateFormatter = { let df = DateFormatter() //Please set up this DateFormatter for the entry `date` df.locale = Locale(identifier: "en_US_POSIX") df.dateFormat = "MMM dd, yyyy" df.timeZone = dateTimeZone return df }() private var textBuffer: String = "" private var nextArticle: Article? = nil override init(data: Data) { super.init(data: data) self.delegate = self } } extension ArticlesParser: XMLParserDelegate { // Called when opening tag (`<elementName>`) is found func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) { switch elementName { case "posts": nextArticle = Article() case "title": textBuffer = "" case "date": textBuffer = "" case "author": textBuffer = "" case "img": textBuffer = "" case "content": textBuffer = "" default: print("Ignoring \(elementName)") break } } // Called when closing tag (`</elementName>`) is found func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) { switch elementName { case "posts": if let article = nextArticle { articles.append(article) } case "title": nextArticle?.title = textBuffer case "date": print("date: \(textBuffer)") nextArticle?.date = dateFormater.date(from: textBuffer) case "author": nextArticle?.author = textBuffer case "img": print("img: \(textBuffer)") nextArticle?.img = URL(string: textBuffer) case "content": nextArticle?.content = textBuffer default: print("Ignoring \(elementName)") break } } // Called when a character sequence is found // This may be called multiple times in a single element func parser(_ parser: XMLParser, foundCharacters string: String) { textBuffer += string } // Called when a CDATA block is found func parser(_ parser: XMLParser, foundCDATA CDATABlock: Data) { guard let string = String(data: CDATABlock, encoding: .utf8) else { print("CDATA contains non-textual data, ignored") return } textBuffer += string } // For debugging func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) { print(parseError) print("on:", parser.lineNumber, "at:", parser.columnNumber) } }
You have no need to implement all the methods defined in XMLParserDelegate, depending on the content of the xml.
If the actual xml is more complex than the example, you need to re-write many parts of this code.
You can use it like this:
(Assuming data is containing the xml as Data.)
Code Block let parser = ArticlesParser(data: data) if parser.parse() { print(parser.articles) //... } else { if let error = parser.parserError { print(error) } else { print("Failed with unknown reason") } }
One more, the xml header should be something like this:
Code Block <?xml version="1.0" encoding="UTF-8"?>
I guess it is broken when you post the example. But if the API actually returns such a header, you may need to fix the API.