// // ArticleInfo.swift // Hair Society Go // // Created by Joshua Srery on 1/13/21. // Code provided by OOPer on Apple Developer Forums // 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] = [] } 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) } } extension ArticlesInfo { var dataUrl: URL { FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] .appendingPathComponent("THS.xml") } func saveCards() { 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) } } }