2

I am writing a Mac application (target 10.9+, using Xcode 6 Beta 3 on Mavericks) in Swift where I have a number of NSTextFields (labels) updating several times per second for extended periods of time by modifying their .stringvalue from a background thread. This seems to work well for a varying duration of time (anywhere between five minutes to 2 hours), but then the application window seems to stop updating. The text stops updating, hovering over the 'stoplight' controls on the upper-left does not show the symbols, and clicking in text boxes, etc., does not highlight the box/show the I-beam. However, my indeterminate progress wheel DOES continue to spin, and when I resize/minimize/zoom the window, or scroll in an NSScrollView box, the window updates during the movement.

My first guess was that some sort of window buffer was being used instead of a live image, so I tried to force an update using window.update(), window.flushWindowIfNeeded(), and window.flushWindow(), all to no avail. Can someone please tell me what's going on, why my window stops updating, and how to fix this problem?

Matt
  • 2,180
  • 5
  • 29
  • 49
  • Are the updates to .stringvalue actually happening on the background threads, or are you marshaling the calls to the main (UI) thread with something like performSelectorOnMainThread? If the former, it's conceivable that those calls are leaving something in an invalid state. – adv12 Aug 20 '14 at 21:55
  • They're happening on background threads. I'm trying to leave the main thread as empty as possible, because I'm (ironically) trying to ensure that the UI is as responsive as possible, with no BBOD (beach ball of death). – Matt Aug 20 '14 at 21:58
  • Well, I know that in the .NET world, at least, doing what you're doing is a big no-no and will cause undefined behavior. I'm pretty sure the same is true with Cocoa. If two threads are reading/writing the state of an object at the same time without proper synchronization, all kinds of bad things can happen. – adv12 Aug 20 '14 at 22:01
  • I'm mainly a VB.NET programmer, so I'm familiar with delegates and all that. However, AFAIK, there's no equivalent in the Swift environment--at least, none that I've been able to find in the documentation so far. – Matt Aug 20 '14 at 22:05
  • This question may help: http://stackoverflow.com/questions/24034544/dispatch-after-gcd-in-swift – adv12 Aug 20 '14 at 22:19
  • 2
    Actually, this one looks better: http://stackoverflow.com/questions/24985716/in-swift-how-to-call-method-with-parameters-on-gcd-main-thread – adv12 Aug 20 '14 at 22:26
  • @adv12 I'm not sure what it actually does...? – Matt Aug 20 '14 at 22:26
  • So the consensus I'm seeing in related stackoverflow answers is that in Swift, the proper way to execute something on the UI thread from a background thread is to use Grand Central Dispatch's dispatch_async function, passing in the result of dispatch_get_main_queue() to identify the thread and a Swift closure (equivalent to an Objective-C block) as the code to run. – adv12 Aug 21 '14 at 17:46

1 Answers1

4

Your problem is right here:

I have a number of NSTextFields (labels) updating several times per second for extended periods of time by modifying their .stringvalue from a background thread.

In OSX (and iOS), UI updates must occur in the main thread/queue. Doing otherwise is undefined behavior; sometimes it'll work, sometimes it won't, sometimes it'll just crash.

A quick fix to your issue would be to simply use Grand Central Dispatch (GCD) to dispatch those updates to the main queue with dispatch_async like:

dispatch_async(dispatch_get_main_queue(), ^{
    textField.stringValue = "..."
});

The simplified version of what that does is it puts the block/closure (the code between {}) in a queue that the default run loop (which runs on the main thread/queue) checks on each pass through its loop. When the run loop sees a new block in the queue, it pops it off and executes it. Also, since that's using dispatch_async (as opposed to dispatch_sync), the code that did the dispatch won't block; dispatch_async will queue up the block and return right away.

Note: If you haven't read about GCD, I highly recommend taking a look at this link and the reference link above (this is also a good one on general concurrency in OSX/iOS that touches on GCD).

Using a timer to relieve strain on your UI

Edit: Several times a second really isn't that much, so this section is probably overkill. However, if you get over 30-60 times a second, then it will become relevant.

You don't want to run in to a situation where you're queueing up a backlog of UI updates faster than they can be processed. In that case it would make more sense to update your NSTextField with a timer.

The basic idea would be to store the value that you want displayed in your NSTextField in some intermediary variable somewhere. Then, start a timer that fires a function on the main thread/queue tenth of a second or so. In that function, update your NSTextField with the value stored in that intermediary variable. Since the timer will already be running on the main thread/queue, you'll already be in the right place to do your UI update.

I'd use NSTimer to setup the timer. It would look something like this:

var timer: NSTimer?

func startUIUpdateTimer() {
    // NOTE: For our purposes, the timer must run on the main queue, so use GCD to make sure.
    //       This can still be called from the main queue without a problem since we're using dispatch_async.
    dispatch_async(dispatch_get_main_queue()) {
        // Start a time that calls self.updateUI() once every tenth of a second
        timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target:self, selector:"updateUI", userInfo: nil, repeats: true)
    }
}

func updateUI() {
    // Update the NSTextField(s)
    textField.stringValue = variableYouStoredTheValueIn
}

Note: as @adv12 pointed out, you should think about data synchronization when you're accessing the same data from multiple threads.

Note: you can also use GCD for timers using dispatch sources, but NSTimer is easier to work with (see here if interested).

Using a timer like that should keep your UI very responsive; no need to worry about "leaving the main thread as empty as possible". If, for some reason, you start losing some responsiveness, simply change the timer so that it doesn't update as often.


Update: Data Synchronization

As @adv12 pointed out, you should synchronize your data access if you're updating data on a background thread and then using it to update the UI in the main thread. You can actually use GCD to do this rather easily by creating a serial queue and making sure you only read/write your data in blocks dispatched to that queue. Since serial queues only execute one block at a time, in the order the blocks are received, it guarantees that only one block of code will be accessing your data at the same time.

Setup your serial queue:

let dataAccessQueue = dispatch_queue_create("dataAccessQueue", DISPATCH_QUEUE_SERIAL)

Surround your reads and write with:

dispatch_sync(dataAccessQueue) {
    // do reads and/or writes here
}
Or Arbel
  • 2,866
  • 1
  • 27
  • 40
Mike S
  • 39,439
  • 11
  • 85
  • 82
  • 2
    It's worth mentioning that for memory visibility, you'll need to synchronize access to variableYouStoredTheValueIn (or make it volatile); otherwise there's no guarantee that a change made by one thread will become visible to another thread. – adv12 Aug 22 '14 at 13:26
  • @adv12, Mike S: How would I sync/make volatile my variable? – Matt Aug 26 '14 at 19:22
  • 1
    @Matt I update the answer with a (very) quick explanation of one method of data synchronization. – Mike S Aug 26 '14 at 19:36