Network Activity Indicator

May 22, 2016

Anthony Mattox nerd sniped me on Jonathan Wight’s Swift Slack the other day.

Does anyone have any suggestions for the best way to manage the system network activity indicator? In previous projects I’ve implemented a simple singleton class with ‘begin task’ and ‘end task’ functions which just modify an internal counter. This works fine, but it’s easy to make mistakes.

I put together a little manager class that includes the begin/end task methods, but it also uses weak references to make ending automatic. Besides being something you can drop into your own projects, it’s a nice example of using weak references in Swift.

The full source of the class, with comments, is available in this gist. I’ll walk through the pieces here.

The class is a singleton, and I don’t feel bad about that. There’s only one network activity indicator on your iPhone after all. We use a fairly standard Swiftism for vending the singleton instance:

public class NetworkIndicatorManager {
    public static var sharedManager = NetworkIndicatorManager()

Beyond getting the shared instance, there’s only one method that clients need to use, beganNetworkTask(withToken token: AnyObject). Before diving into the implementation, let’s look at how client code would use the API.

let task = NSBlockOperation() {
    /* do some network work here */
}
NetworkIndicatorManager.sharedManager.beganNetworkTask(withToken: task)
backgroundQueue.addOperation(task)

This code creates an NSOperation instance, task, to do some async work. It then gets the shared NetworkIndicatorManager and begins a network task passing the operation instance as the token. Then the code enqueues the task operation on a background queue and we’re done.

The queue will retain task until it finishes executing. The NetworkIndicatorManager just holds a weak reference to task. It notices when that weak reference becomes nil, and if no other network tasks are active, hides the indicator. We don’t have to remember to end the task. We just have to avoid leaking a reference to it, which we would need to do anyway. Easy peasy.

So how do we implement that? Let’s look at beganNetworkTask(withToken token: AnyObject) first:

    public func beganNetworkTask(withToken token: AnyObject) {
        let wrappedToken = WeakWrapper(wrapping: token)
        keepAliveTokens.append(wrappedToken)
        updateIndicator()
    }

    private var keepAliveTokens: [WeakWrapper<AnyObject>] = []

Notice that token has to be an object type so that we can store a weak reference to it. Then to store token in a Swift array, we need to wrap it in another class. Swift arrays can’t (yet?) hold weak references directly. After wrapping the token, we store it in the keepAliveTokens array. Then we call a helper method to update the activity indicator.

Before looking at that helper method, here’s WeakWrapper:

private class WeakWrapper<Wrapped: AnyObject> {
    weak var wrapped: Wrapped?

    /// Becomes true when `wrapped` is deallocated.
    var isCleared: Bool {
        return wrapped == nil
    }

    init(wrapping: Wrapped) {
        self.wrapped = wrapping
    }
}

This generic class stores a weak reference to a single object. Since it’s weak, the wrapped property has to have an optional type. Unlike many such properties, this one starts out with a value; the initializer has a non-optional argument. The property type has to be optional because it can become nil later, when all strong references to the wrapped object are released.

For convenience in implementing our updateIndicator() function, we give WeakWrapper a computed property, isCleared, that lets us check if the wrapped object has been deallocated.

Turning to updateIndicator() we’ll find the meat of the implementation:

    private func updateIndicator() {
        keepAliveTokens = keepAliveTokens.filter { wrappedToken in
            !wrappedToken.isCleared
        }
        let shouldShowActivity = !keepAliveTokens.isEmpty
        UIApplication.sharedApplication().networkActivityIndicatorVisible = shouldShowActivity
        ...

First we filter the array of wrapped tokens to remove wrappers whose tokens have been cleared. There’s no mechanism in Swift (or Objective-C) to get a notification when the target of a weak reference goes away. The only way to check is to dereference and see if we get nil back. (See this excellent Mike Ash post for the gory details.) The isCleared computed property handles this check.

As this point, keepAliveTokens only contains wrappers for live objects, so if the array is not empty, we should show the activity indicator.

If we don’t get a notification when the tokens go away, how do we know to update the indicator? The rest of the updateIndicator() method handles this:

        if !isPollingConfigured && shouldShowActivity {
            pollingTimer = NSTimer.scheduledTimerWithTimeInterval(completionCheckInterval, target: self, selector: #selector(timerFired(_:)), userInfo: nil, repeats: true)
        } else if isPollingConfigured && !shouldShowActivity {
            pollingTimer?.invalidate()
            pollingTimer = nil
        }
    }

    private var pollingTimer: NSTimer? = nil
    private var isPollingConfigured: Bool { return pollingTimer != nil }

    public var completionCheckInterval: NSTimeInterval = 1.0

We need to use a timer to periodically update the indicator. The if statement ensures the this pollingTimer is configured if there is any activity and is torn down when all activity ends.

I chose to expose completionCheckInterval as a public property so the client app can adjust it if necessary. One second seems like a reasonable choice though. The indicator will always spin long enough for the user to notice it, but we won’t spend too much time processing the keepAliveTokens array. I suppose if your use case had thousands of active network tasks, this processing might get expensive, but you should probably be rethinking your life choices at that point anyway.

The pollingTimer is configured to call a timerFired(_:) method. That’s dead simple:

    private dynamic func timerFired(timer: NSTimer) {
        updateIndicator()
    }

The dynamic modifier marks the method as dispatching through the Objective-C runtime so that the NSTimer instance can call it via performSelector(). It might be nice to have the timer invoke updateIndicator() directly, but NSTimer expects a specific type signature for the target method.

That’s the essence of the class, though the full version in the gist has just a few other wrinkles. There’s a no-arg beganNetworkTask() method that returns an opaque token object. That’s useful if you don’t have any natural token object like an NSOperation in your code. You can just hang on to a reference to the returned opaque token to keep the operation alive. There’s also a finishedNetworkTask(forToken:) method if you need to explicitly signal the end of a task. Finally, there are some guard statements that I omitted above for clarity. It’s useful to look at one of these.

The full implementations of beganNetworkTask(withToken:), updateIndicator(), and finishedNetworkTask(forToken:) take pains to do their work on the main queue. They do this using a guard statement like so:

    public func beganNetworkTask(withToken token: AnyObject) {
        // Redispatch async to main queue so access to `keepAliveTokens` is single threaded
        guard NSThread.isMainThread() else {
            NSOperationQueue.mainQueue().addOperationWithBlock {
                self.beganNetworkTask(withToken: token)
            }
            return
        }
        let wrappedToken = WeakWrapper(wrapping: token)
        keepAliveTokens.append(wrappedToken)
        updateIndicator()
    }

The guard ensures that the method was invoked on the main thread. If not it re-dispatches asynchronously to the main queue, calling the same method again. This ensures that access to the keepAliveTokens array is single-threaded, so we don’t have any race conditions. The similar guard in updateIndicator() also keeps our pollingTimer scheduled on the main run loop.

I didn’t bother creating a micro-framework for NetworkIndicatorManager. I’m unlikely to maintain it, but please feel free to copy and paste any or all of it that you find useful.

Share and enjoy!