2

I have a class that implements the XMLParserDelegate protocol and during initialisation it gets a string and a completion handler as the arguments. I'm trying to call completion handler after parsing of the string, but the XMLParserDelegate methods are not being reached because of deallocation of the class.

class MyXMLParser: NSObject, XMLParserDelegate {

private (set) var parser: XMLParser?
private (set) var completion: ((String?) -> Void)?

public init(_ xml: String, _ completion: @escaping ((String?) -> Void)) {
    let data = xml.data(using: String.Encoding.utf8)
    self.parser = XMLParser(data: data ?? Data())
    self.completion = completion
    self.parser?.delegate = self
    self.parser?.parse()
}

deinit {
    // Being called before Parser methods
}

// MARK: - Parser delegate methods

func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
    // Custom implementation
}

func parserDidEndDocument(_ parser: XMLParser) {
    // Custom implementation
    self.completion?("Test")
}

func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) {
    self.completion?(nil)
}

}

And I'm calling it the following way:

func someFunc() {
    let parser = MyXMLParser(someXMLString) { text in
        // Custom implementation
    }
}

I want the closure to stay alive until it gets the value, instead of getting deinitialized after its local scope inside of the function has been ended. Great example of what I want to achieve is UIView.animate() completion block that does not get deallocated even if it is located inside of some function.

jscs
  • 63,694
  • 13
  • 151
  • 195
Lew Winczynski
  • 1,190
  • 1
  • 11
  • 20

2 Answers2

0

You need to store the MyXMLParser somewhere other than a local variable. Simple as that.

Notice that UIView.animate(...) is a class method. That means that the class is involved somehow in the lifetime management.

One straightforward option to mimic that is to change your interface slightly, so that instead of directly creating the MyXMLParser, you ask the class to do it for you:

class MyXMLParser : NSObject, XMLParserDelegate {

    private static var createdParsers: Set<MyXMLParser> = []

    static func parse(_ xml: String, _ completion: @escaping (String?) -> Void) {
        let newParser = MyXMLParser(xml, completion)
        self.createdParsers.insert(newParser)
    }

    private static func parserDidEndParsing(_ parser: MyXMLParser) {
        self.createdParsers.remove(parser)
    }

    private let parser: XMLParser
    private let completion: (String?) -> Void

    private init(_ xml: String, _ completion: @escaping (String?) -> Void) {
        // Same as existing code
    }

    //...

    func parserDidEndDocument(_ parser: XMLParser) {
        //...
        MyXMLParser.parserDidEndParsing(self)
    }
}

Here an individual parser is owned by its class, and you create one by calling MyXMLParser.parse(myXmlString) { (text) in /* whatever */ }. It's important to note the cleanup step in parserDidEndDocument, removing the instance from the class's stored collection, so that you don't have unused instances hanging around.

jscs
  • 63,694
  • 13
  • 151
  • 195
  • That's obvious. But, e.g. UIView.animate() completion block does not get deallocated when it is located inside of some function. That's what I want to achieve. – Lew Winczynski Feb 18 '19 at 22:06
  • It's not at all clear that you were trying to do that. Please [edit your question](https://stackoverflow.com/posts/54755755/edit) to explain your goal. – jscs Feb 18 '19 at 22:07
0

You need to make sure your parser object stays around. Right now it goes away as soon as someFunc ends. You may want to declare parser as a class-level variable.

Mike Taverne
  • 9,156
  • 2
  • 42
  • 58