Matthew's Dev Blog

Handling widgetPerformUpdate in an iOS Today extension

Apple's documentation about the NCWidgetProviding protocol, and widgetPerformUpdate: function in particular are rather sparse, and most posts on StackOverflow seem to have a very simplistic view about how this callback should be coded. So here are my tips.

Most examples you'll see for widgetPerformUpdate: look rather like this:

func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) {
	let resultModel = performExpensiveNetworkOperation()
	label.text = resultModel.someValue
	completionHandler(NCUpdateResult.newData)
}

... which would probably be OK, most of the time, but there are very few examples of expensive work being done asynchronously.

There are, however, three big hints that this is the right thing to do:

  1. Apple's documentation says "It's expected that the widget will perform the work to update asynchronously and off the main thread as much as possible". (The widgetPerformUpdate: call does indeed arrive on the main thread)
  2. the completionHandler block is annotated @escaping, implying that it will outlive the scope of the widgetPerformUpdate: function itself
  3. the function returns Void; if this were intended to be a blocking call, then the func would simply have an NCUpdateResult return value instead of providing a completion block

Doing it right

The correct way to do this depends on whether your long-running operation is synchronous (blocking), or asychronous with a completion block, or asynchronous with a delegate. Here are examples of all three.

Performing a synchronous (blocking) operation

class SynchronousCallExampleViewController: UIViewController, NCWidgetProviding {
	private let dataSource = DataSource()
	private var data: Data?

	private func updateUI() {
		DispatchQueue.main.async {
			// update UI components
		}
	}

	func widgetPerformUpdate(completionHandler: @escaping (NCUpdateResult) -> Void) {
		DispatchQueue.global().async {
			// fetch data on a background thread
			self.data = self.dataSource.fetchData()
			self.updateUI()
			completionHandler(.newData)
		}
	}
}

Performing an asynchronous operation with a completion block

class AsynchronousCallWithCompletionBlockExampleViewController: UIViewController, NCWidgetProviding {
	private let dataSource = DataSource()
	private var data: Data?

	private func updateUI() {
		DispatchQueue.main.async {
			// update UI components
		}
	}

	func widgetPerformUpdate(completionHandler: @escaping (NCUpdateResult) -> Void) {
		DispatchQueue.global().async {
			// fetch data on a background thread
			self.dataSource.fetchDataAsync({ [weak self] (data) in
				guard let self = self else { return }

				self.data = data
				self.updateUI()
				completionHandler(.newData)
			})
		}
	}
}

Performing an asynchronous operation with a delegate callback

class AsynchronousCallWithDelegateExampleViewController: UIViewController, NCWidgetProviding, DataSourceDelegate {
	private var dataSource = DataSource()
	private var widgetCompletionHandler: ((NCUpdateResult) -> Void)?
	private var data: Data?
	
	private func updateUI() {
		DispatchQueue.main.async {
			// update UI components
		}
	}
	
	func widgetPerformUpdate(completionHandler: @escaping (NCUpdateResult) -> Void) {
		widgetCompletionHandler = completionHandler
		
		dataSource.delegate = self
		DispatchQueue.global().async {
			// fetch data on a background thread
			self.dataSource.fetchDataAsync()
		}
	}
	
	func dataDidUpdate(dataSource: DataSource) {
		data = dataSource.data
		updateUI()
		
		if let completionHandler = widgetCompletionHandler {
			completionHandler(.newData)
			widgetCompletionHandler = nil
		}
	}
}
Tagged with:

First published 10 November 2018