(Expansion) Retrieve articles from an XML parser

Please read the last thread: Call a Class into views

The right code for the XML parser is almost set in stone, just I need a way to retrieve article data (not just through loadArticles()) like title, author, etc.
I've decided, and was suggested, that I go the same route as my other app, Lunch Card, where there's a function where you use a dataUrl and data to call ArticlesParser(). Thing is, since the code for the XML parser is a bit more complicated, it's harder to pass variables around just like with Lunch Card. There's another class I have to deal with. So I either have to (1) redefine articles the same way with an Array;
Code Block
@Published var articles: [ArticleInfo] = []

...or (2), find some other way to call articles from the ObservableObject, maybe in a StateObject or ObservedObject.
I want to try and borrow as much code from Lunch Card as possible, but for an XML parser with multiple classes and extensions, it a bit easier said than done. Here's full code for ArticleInfo and ArticlesView.

Not sure how ArticleInfo is an integer, so I have no idea what's going on or why it's not working as well. Also, I'm trying to experiment with borrowing the JSON decoder/encoder code, so tell me if that's the wrong way to go.

Replies

Your new ArticlesParser lacks the property articles and it does not compile.

Thing is, since the code for the XML parser is a bit more complicated, it's harder to pass variables around just like with Lunch Card.

Once you finished (and once you made it work), ArticlesParser is as easy to use as JSONDecoder.

Not sure how ArticleInfo is an integer

Unfortunately, the current Xcode generates inappropriate diagnostics when there are type related errors.
In your case, the real problem exists in this line:
Code Block
ArticleRow(image: article.img, title: article.title, author: article.author, date: article.date)

The type of image is String, but the type of article.img is URL?.
date in ArticleRow is String, but article.date is Date?.

You need to pass the right type.

You need to pass the right type.

Thanks for pointing that out.

Your new ArticlesParser lacks the property articles and it does not compile.

So do I redefine it? I'm still a little confused.

So do I redefine it? I'm still a little confused.

Sorry, I do not understand why you removed it. It is I who is confused.

Sorry, I do not understand why you removed it. It is I who is confused.

Apologies, I must have switched it around.

ArticlesParser now holds articles as a published variable. But ArticlesInfo - where the loadArticles() function is held - now cannot call articles because that property does not exist in ArticlesInfo.

Code Block
class ArticlesInfo: ObservableObject {
    var dataUrl = URL(string: https://storage.googleapis.com/thehairsociety/2021/03/2f23e2d4-ths.xml)
    func loadArticles() {
        do {
            if FileManager.default.fileExists(atPath: dataUrl!.path) {
                return
            }
            //Read `Data` from a file specified by URL
            let data = try Data(contentsOf: dataUrl!)
            //Parse Available Articles
            let articles = try // ArticlesParser? JSONDecoder?
            self.articles = articles // <-- Value of type 'ArticlesInfo' has no property 'articles'
        } catch {
            print(error)
        }
    }
}

So do I need to call articles from ArticlesParser or should I just redefine articles? Hopefully this clears some things up a bit.

On the topic of using the Lunch Card method, would using JSON be easier to use than XML? I'm curious because after doing a few tests it seems it's a bit simpler though I haven't tested actually article content (HTML) yet.

ArticlesParser now holds articles as a published variable. 

ArticlesParser is not an ObservableObject, so making it @Published does not make sense.

But ArticlesInfo - where the loadArticles() function is held - now cannot call articles because that property does not exist in ArticlesInfo.

Why did you remove articles? I completely do not understand why.

On the topic of using the Lunch Card method, would using JSON be easier to use than XML?

If you easily remove some declarations without thinking why it is there, both JSON and XML cannot be easy for you.


Why did you remove articles? I completely do not understand why.

I don't understand how I removed articles. Sorry if I'm missing something but I don't remember ever removing it. It's in the ObservableObject where you told me to put it in the first place.

You can do it in the same way:

Code Block
class ArticlesInfo: ObservableObject {
@Published var articles: ...
//...
func loadArticles() {
// Use `ArticlesParser` here
//...
}
}

Here's code for ArticleInfo since it was updated to integrate the extension into the actual ObservableObject:
Code Block
import Foundation
struct ArticleInfo: Identifiable, Codable {
    var id: String = ""
    var title: String = ""
    var date: Date?
    var author: String = ""
    var img: URL?
    var content: String = ""
}
class ArticlesInfo: ObservableObject {
    @Published var articles: [ArticleInfo] = []
    var dataUrl = URL(string: "https://storage.googleapis.com/thehairsociety/2021/03/2f23e2d4-ths.xml")
    
    func loadArticles() {
        do {
            let data = try Data(contentsOf: dataUrl!)
            let parser = ArticlesParser(data: data)
            if parser.parse() {
                print(parser.articles) // Value of type 'ArticlesParser' has no property 'articles'
                let articles: try //...?
                self.articles = articles // Unsure what I put here
            } else {
                if let error = parser.parserError {
                    print(error)
                } else {
                    print("Failed with unknown reason")
                }
            }
        } catch {
            print(error)
        }
    }
}
class ArticlesParser: XMLParser {
    var dateTimeZone = TimeZone(abbreviation: "GMT-6")
    lazy var dateFormater: DateFormatter = {
        let df = DateFormatter()
        df.locale = Locale(identifier: "en_US_POSIX")
        df.dateFormat = "MMM dd, yyyy"
        df.timeZone = dateTimeZone
        return df
    }()
    
    private var textBuffer: String = ""
    private var nextArticle: ArticleInfo? = 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 = ArticleInfo()
        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) // Cannot find 'articles' in scope
            }
        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)
    }
}

Here's code for ArticleInfo 

Seems you have gone back to the first place.
Have you ever learnt programming? If this is just a school homework and you have no intention to learn programming, you should better find a good friend, rather than asking here.

Your latest ArticlesParser lacks the property articles


to integrate the extension into the actual ObservableObject:

What do you mean by integrate?

What do you mean by integrate?

Sorry, meant "merge." There was no point for the extension if the main ObservableObject would just have the variable.

There was no point for the extension if the main ObservableObject would just have the variable.

You continue saying words which I do not understand...

The extension is for ArticlesParser which is not an ObservableObject.

You continue saying words which I do not understand

I added an extension for ArticlesInfo and decided against it. Apologies for not clarifying.

I added an extension for ArticlesInfo and decided against it.

Please show the extension for ArticlesInfo.
Code Block
extension ArticlesInfo {
var dataUrl: URL {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("THS.xml")
}
func saveArticles() {
do {
//Convert Array of `CardInfo` to `Data`
let data = try JSONEncoder().encode(dataUrl)
//Write `Data` to a file specified by URL
try data.write(to: dataUrl, options: .atomic)
} catch {
print(error)
}
}
func loadArticles() {
do {
if FileManager.default.fileExists(atPath: dataUrl.path) {
return
}
//Read `Data` from a file specified by URL
let data = try Data(contentsOf: dataUrl)
//Parse Available Articles
let articles = try JSONDecoder().decode([ArticleInfo].self, from: data)
self.articles = articles
} catch {
print(error)
}
}
}

The code that was here was moved into ArticlesInfo itself and eventually changed into what it is now.
Please show all the latest code related.
I cannot find any reasons why you needed to add an extension for ArticlesInfo.

Do you really understand what you are doing?