swiftxmlswift-playground

Swift XMLParser in Playground - Delegate Deallocated


This code snippet is from a demo for working with XML files in Swift. Since this code is executed from playground following image shows the project structure.

swift playground files

The XMLapp file contains the class XMLapp. This class contains a nested class XMLreader: NSObject, XMLParserDelegate. Since the XMLapp class is created in playground, it contains static function to call the code for reading XML.

public static func readXML() {
    print("readXML")
    let xmlReader = XMLreader()
    xmlReader.parse()
}

Inside the main playground, following call is made XMLapp.readXML(), which has this output:

readXML
begin parsing
end parsing

import Foundation

public class XMLapp {

    class XMLreader: NSObject, XMLParserDelegate {

        var xmlParser: XMLParser {
            let url = Bundle.main.url(forResource:"sample.xml",withExtension:nil)!
            return XMLParser(contentsOf: url)!
        }

        public func parser(_ parser: XMLParser, foundElementDeclarationWithName elementName: String, model: String) {
            print("element name: \(elementName), model: \(model)")
        }
        
        public func parserDidStartDocument(_ parser: XMLParser) {
            print("parserDidStartDocument")
        }
        
        public func parserDidEndDocument(_ parser: XMLParser) {
            print("parserDidEndDocument")
        }
        
        // ...
        // other protocol methods
        
        public func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
            print("didStartElement")
        }

        public func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
            print("didEndElement")
        }
        
        // ...
        // other protocol methods

        public override init() {
            super.init()
            xmlParser.delegate = self
        }

        public func parse() {
            print("begin parsing")
            xmlParser.parse()
            print("end parsing")
        }
    }

    public static func readXML() {
        print("readXML")
        let xmlReader = XMLreader()
        xmlReader.parse()
    }
}

The callbacks are not getting called. Even though the delegate for the xmlParser is initialized. Please has anybody come across this issue in playground before? The following call is made to assign the delegate, still it is not working as expected.

xmlParser.delegate = self


Solution

  • The xmlParser property in XMLreader is a computed property. This means that return XMLParser(contentsOf: url)! gets evaluated every time you access it, so you get a new instance every time. As a result, the instance of XMLParser you set the delegate is a different instance from which you have called parse:

    xmlParser.delegate = self // you get one instance here
    xmlParser.parse() // and a different instance here
    

    To fix this, you can make xmlParser a stored property:

    let xmlParser: XMLParser = {
        let url = Bundle.main.url(forResource:"sample.xml",withExtension:nil)!
        return XMLParser(contentsOf: url)!
    }()
    

    Notice the =. Learn more about the difference in syntax here.

    Another way is to assign the xmlParser to a local, set the delegate, and then parse. This way you ensure that you are working with the same instance:

    let parser = xmlParser
    parser.delegate = self
    parser.parse()