Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2015-09-16

Code sample: A runloop observer in Swift.

The problem: I have a text file that must conform to certain syntax rules. The user can edit the file, and the GUI should show if the lexical structure is correct. If not, it should show where the syntax rules are violated and show some information as to which rule is violated. (Very similar to spelling errors in text editing GUI's)

After each editing operation (delete char, insert char, etc) the entire file must be checked again for a correct syntax. Of course when a file gets really large, these checks can take a while. This delay should not impact the user while he is typing.

The solution is to do the syntax check in its own thread, not in the main runloop that processes the GUI. But the results of these checks must be fed back into the GUI processing thread (main runloop). Apple has created runloop observers for such situations.

Once the main runloop has processed all events for the current go-around it is put to sleep (or made waiting) until new events arrive. It is possible to start a process of our own when this happens. That process is called a runloop observer and forms part of the main runloop. As such, the runloop observer can safely update the GUI.

Ok, so far the theory, here is the code:

    private var editCount: Int = 0
    private let parseQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)
    private var parseResults: Array<Parser.Result> = []

    func lexicalStructureChanged() {
        
        // When the lexical structure changes, the parser must process the entire document again.
        // Since parsing large documents can take some time, parsing is done in a separate thread.
        // This thread is executed asynchronously on the parseQueue. It communicates the results back via the parseResults array.
        // A runloop observer is added to the current runloop to check for the availability of results. That observer will keep observing until a result is found and then dismiss itself.
        // Note that the observer will not try to update a document with a different "editCount" as the editCount the parser was started with. This ensures that old results are discarded.
        // The editCount will always be >0, but the parser editCount may be 0 if there was no error, and thus no action to be taken.
        
        let runLoopObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.BeforeWaiting.rawValue, Boolean(1), 0, {
            
            (observer: CFRunLoopObserver!, activity: CFRunLoopActivity) -> Void in // This will keep a reference to self alive until all parse results are processed
            
            // Check if a result is available
            
            if self.parseResults.isEmpty { return }
            
            
            // There is a result, this observer can be detached
            
            CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes)
            
            
            // Process the result
            
            self.processParseResults()
            
        } )
        
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), runLoopObserver, kCFRunLoopCommonModes)
        
        editCount++

        dispatch_async(parseQueue, { // This will keep a reference to self alive until the parser is done
            self.parseResults.append(Parser.parse(self.lines, editCount: self.editCount))
        } )
    }

    private func processParseResults() {

        let (count, lineIndex, elementIndex, message) = parseResults.removeAtIndex(0)

        if count < editCount { return }
        ...
    }

The first function lexicalStructureChanged is called for every lexical change to the document.

The second function is only shown to illustrate that the oldest entry in the parseResults array is removed when the runloop observer is called.

The runloop is first created (let runLoopObserver =) then added (CFRunLoopAddObserver) and after the runloop observer is in place, the parser is started as an asynchronous process using GCD (dispatch_async).

The parser does nothing with the editCount parameter except returning exactly the same value as it was started with. Thus allowing the process processParseResults to ignore results that were returned for older edits of the document. This will probably only happen when the user types very fast and the document is sufficiently large. In my tests so far the parsing was always complete in less than a millisecond, thus the updates to the GUI were faster than my typing!

Note that both code blocks (closures) retain "self". This ensures that Self is available when the parseResults and the runloop observer are called. The "weak" or "unowned" modifiers should not be used. Otherwise a runtime error could occur when the user closes the document before the parser is ready.

PS: Make sure that you do not update the GUI from a process in the parseQueue, you will get very strange error messages from CoreAnimation that way. If there is a need to update the GUI from that thread use:

                dispatch_async(dispatch_get_main_queue(), { guiUpdate() })

(and yes, you could do all GUI updating that way, but it gets very difficult to know which update is still applicable and which is not)

Happy coding...

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

No comments:

Post a Comment