Parse XML in SwiftUI

This is about a different app now.

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)
    }
}

Accepted Reply

how can I use XMLParser to parse that specific data (title, image, author, date, content, etc.) and map it to the specific variables? 

OK, let's concentrate on this issue.

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.

Replies

OK, that's the code.

Where is the problem ?
What do you expect ?
What do you get ?

Note: you'd better not mention the name of your customer. I am not sure they'll appreciate this post so much if they see it.

Note: you'd better not mention the name of your customer. I am not sure they'll appreciate this post so much if they see it.

Permission was granted.

Where is the problem ?
What do you expect ?
What do you get ?

The problem? I already identified it. I have no knowledge of how to implement a system to parse XML.
Expectation? To parse the XML data and transfer it to the article title, author, image, date, and content.
What I get? An app with sample data that would destroy the purpose of it being a companion app to the magazine.

I just need some kind of system (whether that be a Swift Package or...) to parse the XML feed of the website to article data.
Your code is too primitive, so I cannot think of what to ask.

So, just some general advice:
  • XML may contain information in various ways. You may not find any third party libraries working unless the XML is designed for a particular library.

  • In Apple's frameworks, there is XMLParser. Which is so called a SAX-based parser, you may need to write plenty of code depending on the content of the XML.

You may not find any third party libraries working unless the XML is designed for a particular library.

Good to know.

...you may need to write plenty of code depending on the content of the XML.

I expected that.
Thing is, I need to create a whole new XML file because the website (which is built in WordPress) doesn't show all content. I can do that on my own, but how can I use XMLParser to parse that specific data (title, image, author, date, content, etc.) and map it to the specific variables? I'm fine with a reference, but the documentation isn't helping.

Here's an example:
Code Block
<?xml version=“1.0” encoding “UTF-8”?>
<posts>
<title>New Year, New Resolutions</title>
<date>January 4, 2021</date>
<author>The Hair Society</author>
<img>https://storage.googleapis.com/thehairsociety/2021/01/4a8e3956-bca1f16a-95f3-4f0a-90d5-155fa7194780.jpeg</img>
<content><![CDATA[<span style="font-weight: 400;"><img class="alignright wp-image-19831" src="https://storage.googleapis.com/thehairsociety/2021/01/c4d25cf5-d4b49cc7-c37c-407e-a6a8-a5f36e32a336.jpeg" alt="2021 Resolutions" width="464" height="261" />The year 2021 is here! We've long anticipated this new beginning, and now is the time to buckle down. What will your salon or spa look like in the New Year? If you haven't considered a New Year's resolution, let us help you develop your 2021 game plan.</span>
<span style="font-weight: 400;">First, what is a resolution? A resolution is simply an intention. It is something you aim to do or not to do. It should aid in improving yourself or your business. It is a goal to be better than the year before! Once you've nailed down one or two resolutions for your team, it's time to take action. Here are a few practices that will help you achieve your resolution.</span>
<strong>Start Small</strong>
<span style="font-weight: 400;">Everyone wants to set high expectations, especially after enduring a prior year of hardships. When your initial goal is to increase an asset, you may have to dial it back a few notches. It is easy to become discouraged from the start if your goal is too outlandish. Be sure it is attainable in the next twelve months. For example, if you want to increase your revenue, start with a small percentage of growth. After all, a 10-20% increase is still better than no growth at all. Small wins are still gains. That leads me to my next resolution tip.</span>
<strong><img class="alignleft wp-image-19832" src="https://storage.googleapis.com/thehairsociety/2021/01/573ecc49-9fa635f4-e4e6-4969-8c07-601a7c6f720c.jpeg" alt="New Year Resolution Tips" width="299" height="299" />Measurable Benefits</strong>
<span style="font-weight: 400;">Ask yourself if you can measure the goal for the year. It may require additional data collection methods, but the feedback will prove beneficial in the long run. If you resolve to increase your presence on social media, you can draw your attention to increased followers, the number of "shares" or "likes," etc. Maybe you are aiming to keep a tidy shop in 2021. You'll want to measure client satisfaction with a brief questionnaire or count the number of notable mentions on cleanliness in online reviews.</span>
<span style="font-weight: 400;">A more personal goal for the new year could be organization. How can your team become more organized and then record the benefits? You may track inventory to assess how successful your system of organization. You may begin to notice a change in how a scheduled workday. When your team becomes more organized, the time spent scrambling for misplaced items has now decreased. You may see more time in the workday to increase productivity elsewhere. It is your job to keep a resolution alive throughout the year, so be prepared to measure your hard work.</span>
<strong>Visualize The Steps</strong>
<span style="font-weight: 400;">Your next move is to envision the steps your team will take to succeed. The real changes take place during small acts of progress. Don't intimidate your team with talk about the result. It would be best if you tried to focus on the steps in achievement versus the resolution itself. When your attention is on 2-3 daily tasks, your team can feel empowered to be a piece in the big picture.</span>
<span style="font-weight: 400;">If your salon's resolution is to book more appointments, don't obsess over gaps throughout the day. Instead, your team should encourage booking a client's future appointment weeks in advance and consistently use social media to highlight times of low client volumes. For instance, every Monday, highlight openings for the week on Facebook or Instagram. An online reminder can make all the difference! Not to mention, creating a social media post can be done quickly without taking away from critical salon work.</span>
<strong>More Education</strong>
<span style="font-weight: 400;"><img class="alignright wp-image-19833" src="https://storage.googleapis.com/thehairsociety/2021/01/30bcf8e0-afa6a486-b14f-493d-ae68-9088f9c6debb.jpeg" alt="Continuing Education" width="502" height="251" />Our last resolution tip for success is to encourage lifelong learning. Regardless of your chosen resolution, the hair world is ever-changing. When you think you've learned it all, technology or society reveals new challenges. As our technology or tools change, we must stay updated. You and your team should strive to learn new skills and knowledge.</span>
<span style="font-weight: 400;">What knowledge is essential to study? There are many different hair types out there, and there are also many other conditions that affect hair follicles. Trichology could be the winning edge your team needs to study. Trichology is the science of the hair and scalp. Specializing in medical-related conditions that affect the hair and scalp, a trichologist can care for specific client needs. Although not medically certified as a doctor, a trichologist is trained to treat various client complaints associated with hair and scalp health disorders.</span>
<span style="font-weight: 400;">Arming your team with this knowledge will address hair concerns sooner rather than later. If you can help your clients maintain their hair before a hair loss problem arises, wouldn't you want to? There are many hair care products and treatments trichologists recommend. If your team can connect with a specialist in the hair industry, your salon or spa will provide a holistic approach to caring for clients.</span>
<span style="font-weight: 400;">Move your team into the New Year with resolutions that will stick! As your team continues to put in the hardworking effort month after month, the benefits will soon reap themselves. Happy 2021! </span>]]></content>
</posts>

how can I use XMLParser to parse that specific data (title, image, author, date, content, etc.) and map it to the specific variables? 

OK, let's concentrate on this issue.

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.
I tested out ArticlesParser and it gave me an error.
Code Block
struct ArticlesView: View {
let fileURL = Bundle.main.url(forResource: "THS", withExtension: "xml")
    let parser = ArticlesParser(data: fileURL) // <-- Cannot convert value of type 'URL?' to expected argument type 'Data' & Cannot use instance member 'fileURL' within property initializer; property initializers run before 'self' is available
        if parser.parse() { // <-- Expected declaration (in declaration of 'ArticlesView')
            print(parser.articles)
        } else {
            if let error = parser.parserError {
                print(error)
            } else {
                print("Failed with unknown reason")
        }
    }
//...
}

I was expecting it to read from the XML file (THS.xml) from the app bundle and use the ArticlesParser protocol with it. But instead it threw me those errors. I have a sneaking suspicion that it has to do with the fileURL and how it's called, but I'm not sure what I need to declare by the if statement.

I tested out ArticlesParserand it gave me an error.

You should read the notes written.

(Assuming data is containing the xml as Data.)

You should read the notes written.

Ok, my bad. I apologize.

Question that I just realized needed answered: How would I call ArticlesParser and Article into ArticlesView and other related views (ArticleView, ArticleRow, etc.)?

And I assume Data would be held in Article or ArticlesParser?

Question that I just realized needed answered: How would I call ArticlesParser and Article into ArticlesView and other related views (ArticleView, ArticleRow, etc.)?

And I assume Data would be held in Article or ArticlesParser?

Do you remember that I wrote:
Code Block
let's concentrate on this issue.

Please do not ask too many things in a thread. You asked how to use XMLParser and I have shown the answer for it.


One more.
Please check another thread of yours, and see how data is used there...

Please do not ask too many things in a thread. You asked how to use XMLParser and I have shown the answer for it.

Understood, I'll create a new thread.
Here's the new thread:
(Expansion) Call a Class into views