I have a problem with LibXML2 SAX parser in Swift 3.
I need something like XMLPullParser from Android in iOS. Which downloads XML from the server and parses it when loading Stream.
My XML looks like this:
<?xml version="1.0" encoding="UTF-8" ?> <ResultList id="12345678-0" platforms="A;B;C;D;E"> <Book id="1111111111" author="Author A" title="Title A" price="9.95" ... /> <Book id="1111111112" author="Author B" title="Title B" price="2.00" ... /> <Book id="1111111113" author="Author C" title="Title C" price="5.00" ... /> <ResultInfo bookcount="3" /> </ResultList>
So, all data is stored in attributes, not in child nodes.
I myself made the following class mainly based on these examples:
XMLPerformance , XMLPerformance-Swift, and iOS-XML-Streaming
import Foundation class LibXMLParser: NSObject, URLSessionDataDelegate { var url: URL? var delegate: LibXMLParserDelegate? var done = false var context: xmlParserCtxtPtr? var simpleSAXHandlerStruct: xmlSAXHandler = { var handler = xmlSAXHandler() handler.initialized = XML_SAX2_MAGIC handler.startElementNs = startElementSAX handler.endElementNs = endElementSAX handler.characters = charactersFoundSAX //handler.error = errorEncounteredSAX return handler }() init(url: URL) { super.init() self.url = url } func parse() { self.done = false let session = URLSession(configuration: .default, delegate: self, delegateQueue: OperationQueue.main) let dataTask = session.dataTask(with: URLRequest(url: url!)) dataTask.resume() self.context = xmlCreatePushParserCtxt(&simpleSAXHandlerStruct, Unmanaged.passUnretained(self).toOpaque(), nil, 0, nil) self.delegate?.parserDidStartDocument() repeat { RunLoop.current.run(mode: .defaultRunLoopMode, before: Date.distantFuture) } while !self.done xmlFreeParserCtxt(self.context) self.delegate?.parserDidEndDocument() } func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { print("Did receive data") data.withUnsafeBytes { (bytes: UnsafePointer<CChar>) -> Void in xmlParseChunk(self.context, bytes, CInt(data.count), 0) } } func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { xmlParseChunk(self.context, nil, 0, 1) self.done = true } func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { self.done = true //self.delegate?.parserErrorOccurred(error) } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { self.done = true //self.delegate?.parserErrorOccurred(error) } } private func startElementSAX(_ ctx: UnsafeMutableRawPointer?, name: UnsafePointer<xmlChar>?, prefix: UnsafePointer<xmlChar>?, URI: UnsafePointer<xmlChar>?, nb_namespaces: CInt, namespaces: UnsafeMutablePointer<UnsafePointer<xmlChar>?>?, nb_attributes: CInt, nb_defaulted: CInt, attributes: UnsafeMutablePointer<UnsafePointer<xmlChar>?>?) { let parser = Unmanaged<LibXMLParser>.fromOpaque(ctx!).takeUnretainedValue() parser.delegate?.parserDidStartElement(String(cString: name!), nb_attributes: nb_attributes, attributes: attributes) } private func endElementSAX(_ ctx: UnsafeMutableRawPointer?, name: UnsafePointer<xmlChar>?, prefix: UnsafePointer<xmlChar>?, URI: UnsafePointer<xmlChar>?) { let parser = Unmanaged<LibXMLParser>.fromOpaque(ctx!).takeUnretainedValue() parser.delegate?.parserDidEndElement(String(cString: name!)) } private func charactersFoundSAX(_ ctx: UnsafeMutableRawPointer?, ch: UnsafePointer<xmlChar>?, len: CInt) { let parser = Unmanaged<LibXMLParser>.fromOpaque(ctx!).takeUnretainedValue() parser.delegate?.parserFoundCharacters(String(cString: ch!)) }
I initialize this class using a URL . When I call parse() , it creates a URLSession and a URLSessionDataTask with a self delegate to override the didReceive data: Data method. After that, I create xmlParserCtxtPtr and loop until the dataTask ends.
When it receives data, I parse it using the xmlParseChunk method, and startElementSAX calls the delegate that I set from the ViewController class. (I just need the name of the element, the number of attributes and attributes.)
So far so good.
In my ViewController (UITableViewController), I have the following code:
func downloadBooksLibXML() { print("Downloadingโฆ") UIApplication.shared.isNetworkActivityIndicatorVisible = true DispatchQueue.global().async { print("Setting up parser") let parser = LibXMLParser(url: URL(string: self.baseUrl + self.parameters!)!) parser.delegate = self parser.parse() } } func parserDidStartDocument() { } func parserDidEndDocument() { DispatchQueue.main.sync { UIApplication.shared.isNetworkActivityIndicatorVisible = false self.isDone = true print("Finished") } } func parserDidStartElement(_ elementName: String, nb_attributes: CInt, attributes: UnsafeMutablePointer<UnsafePointer<xmlChar>?>?) { print(elementName) switch elementName { case "Book": DispatchQueue.main.async { let book = self.buildBook(nb_attributes: nb_attributes, attributes: attributes) self.books.append(book) self.tableView.beginUpdates() self.tableView.insertRows(at: [IndexPath(row: self.books.count - 1, section: 0)], with: .automatic) self.tableView.endUpdates() self.navigationItem.title = String(format: NSLocalizedString("books_found", comment: "Books found"), "\(self.books.count)") } case "ResultList": break case "ResultInfo": break default: break } } func buildBook(nb_attributes: CInt, attributes: UnsafeMutablePointer<UnsafePointer<xmlChar>?>?) -> Book { let fields = 5 let book = Book() for i in 0..<Int(nb_attributes) { if let localname = attributes?[i * fields + 0],
------
Update
Thus, the problem of getting attribute values โโwas resolved by nwellnhof's answer. I updated my code above to much better code. Now it does not iterate over all attributes. Now my new problem:
I created a buildBook method to get the Book attribute of XML attributes. I basically translated the method here What is the correct way to get the attribute value in a parser package (C ++)? in Swift and use setValue(value: Any?, forKey: String) to set the attributes of my book object.
But now my problem is that it does not update the tableView. I tried to execute the buildBook method synchronously in the background thread using DispatchQueue.global().sync Sync and update the tableView in the asynchronous main thread using DispatchQueue.main.async . But then it falls on tableView.endUpdates() , although it is in the main thread.
------
Any help would be greatly appreciated.