In a previous post, I showed how we can convert delegates to a closure pattern. The limitations of delegates quickly reveal itself when you need multiple delegates. They’re also pretty awkward in a functional programming language like Swift. Let’s use this pattern to wrap CLLocationManager to allow callers to subscribe to events instead of using shared delegate functions.
Out of the Box
First to contrast, see how we can use CLLocationManager
 in a view controller:
class ViewController: UIViewController { @IBOutlet weak var resultLabel: UILabel! lazy var locationManager: CLLocationManager = { $0.delegate = self return $0 }(CLLocationManager()) @IBAction func requestAuthorizationTapped(_ sender: Any) { locationManager.requestWhenInUseAuthorization() } @IBAction func requestLocationTapped(_ sender: Any) { locationManager.requestLocation() } @IBAction func startLocationTapped(_ sender: Any) { locationManager.startUpdatingLocation() resultLabel.text = "startUpdating" } @IBAction func stopLocationTapped(_ sender: Any) { locationManager.stopUpdatingLocation() resultLabel.text = "stopUpdating" } } extension ViewController: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last else { return } resultLabel.text = "Who triggered a location update?: \(location)" } func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { resultLabel.text = "request authorization: \(status.rawValue)" guard CLLocationManager.locationServicesEnabled(), [.authorizedAlways, .authorizedWhenInUse].contains(CLLocationManager.authorizationStatus()) else { return } locationManager.startUpdatingLocation() } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { debugPrint(error) } }
There’s nothing special here; just a few IBActions
 to request authorization, request current location, and toggle the location updates. This calls the underlying CLLocationManager
 to perform the actions as normal. Then in the delegate functions, we handle the response from the location services.
Delegate to Closure Pattern
Now let’s add some sugar and spice to our CLLocationManager
. In the end, we can do something like this:
class ViewController: UIViewController { lazy var locationManager: LocationManager = { return $0 }(LocationManager()) lazy var locationObserver: LocationObserver = Observer { print("subscribe location from observer property: \($0)") } override func viewDidLoad() { super.viewDidLoad() locationManager.requestAuthorization { print("request authorization: \($0)") } locationManager.requestLocation { print("request location: \($0)") } locationManager.didUpdateLocations += locationObserver } deinit { locationManager.didUpdateLocations -= locationObserver } }
Notice no more delegate functions! Instead, the trailing closures are added to the request authorization and location calls:
locationManager.requestLocation { print("request location: \($0)") }
Also, the custom LocationManager
 allows subscriptions to be registered for later execution when ready in an observable manner. For example, we can subscribe to location updates by appending the closure to the “delegate queue”:
lazy var locationObserver: LocationObserver = Observer { print("subscribe location from observer property: \($0)") } ... locationManager.didUpdateLocations += locationObserver
This will fire any time location updates come in. It’s not just Swifty, but very powerful. Several subscriptions can be registered from anywhere and will all be executed.
Also notice there is an Observer
 type that is used to wrap the closures. The reason for this is there isn’t a way to make closures Equatable
 in Swift. However, this is necessary so the observers can be “unsubscribed” from the observable (or the queue). With this in place, unsubscribing from the observable becomes as simple as this:
locationManager.didUpdateLocations -= locationObserver
Show Me the Money!
The custom LocationManager
 is implemented below. Notice it is using thread-safe queues to store the closures for later execution by the location manager services.
public class LocationManager: NSObject, CLLocationManagerDelegate { /// Internal Core Location manager fileprivate lazy var manager: CLLocationManager = { $0.delegate = self if let value = self.desiredAccuracy { $0.desiredAccuracy = value } if let value = self.distanceFilter { $0.distanceFilter = value } if let value = self.activityType { $0.activityType = value } return $0 }(CLLocationManager()) /// Default location manager options fileprivate let desiredAccuracy: CLLocationAccuracy? fileprivate let distanceFilter: Double? fileprivate let activityType: CLActivityType? public init( desiredAccuracy: CLLocationAccuracy? = nil, distanceFilter: Double? = nil, activityType: CLActivityType? = nil) { // Assign values to location manager options self.desiredAccuracy = desiredAccuracy self.distanceFilter = distanceFilter self.activityType = activityType super.init() } /// Subscribes to receive new location data when available. public var didUpdateLocations = SynchronizedArray<LocationObserver>() fileprivate var didUpdateLocationsSingle = SynchronizedArray<LocationHandler>() /// Subscribes to receive new authorization data when available. public var didChangeAuthorization = SynchronizedArray<AuthorizationObserver>() fileprivate var didChangeAuthorizationSingle = SynchronizedArray<AuthorizationHandler>() deinit { // Empty task queues of references didUpdateLocations.removeAll() didUpdateLocationsSingle.removeAll() didChangeAuthorization.removeAll() didChangeAuthorizationSingle.removeAll() } } // MARK: - Nested types public extension LocationManager { /// Location handler queue type. typealias LocationObserver = Observer<LocationHandler> typealias LocationHandler = (CLLocation) -> Void // Authorization handler queue type. typealias AuthorizationObserver = Observer<AuthorizationHandler> typealias AuthorizationHandler = (Bool) -> Void /// Permission types to use location services. /// /// - whenInUse: While the app is in the foreground. /// - always: Whenever the app is running. enum AuthorizationType { case whenInUse, always } } // CLLocationManagerDelegate functions public extension LocationManager { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last else { return } // Trigger and empty queues didUpdateLocations.forEach { $0.handler(location) } didUpdateLocationsSingle.removeAll { $0.forEach { $0(location) } } } func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { guard status != .notDetermined else { return } // Trigger and empty queues didChangeAuthorization.forEach { $0.handler(isAuthorized) } didChangeAuthorizationSingle.removeAll { $0.forEach { $0(self.isAuthorized) } } } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { // TODO: Injectable logger debugPrint(error) } } // MARK: - CLLocationManager wrappers public extension LocationManager { /// A Boolean value indicating whether the app wants to receive location updates when suspended. var allowsBackgroundLocationUpdates: Bool { get { return manager.allowsBackgroundLocationUpdates } set { manager.allowsBackgroundLocationUpdates = newValue } } /// Determines if location services is enabled and authorized for always or when in use. var isAuthorized: Bool { return CLLocationManager.locationServicesEnabled() && [.authorizedAlways, .authorizedWhenInUse].contains( CLLocationManager.authorizationStatus()) } /// Determines if location services is enabled and authorized for the specified authorization type. func isAuthorized(for type: AuthorizationType) -> Bool { guard CLLocationManager.locationServicesEnabled() else { return false } return (type == .whenInUse && CLLocationManager.authorizationStatus() == .authorizedWhenInUse) || (type == .always && CLLocationManager.authorizationStatus() == .authorizedAlways) } /// Starts the generation of updates that report the user’s current location. func startUpdating(enableBackground: Bool = false) { manager.allowsBackgroundLocationUpdates = enableBackground manager.startUpdatingLocation() } /// Stops the generation of location updates. func stopUpdating() { manager.allowsBackgroundLocationUpdates = false manager.stopUpdatingLocation() } } // MARK: - Single requests public extension LocationManager { /// Requests permission to use location services. /// /// - Parameters: /// - type: Type of permission required, whether in the foreground (.whenInUse) or while running (.always). /// - startUpdating: Starts the generation of updates that report the user’s current location. /// - completion: True if the authorization succeeded for the authorization type, false otherwise. func requestAuthorization(for type: AuthorizationType = .whenInUse, startUpdating: Bool = false, completion: AuthorizationHandler? = nil) { // Handle authorized and exit guard !isAuthorized(for: type) else { if startUpdating { self.startUpdating() } completion?(true) return } // Request appropiate authorization before exit defer { switch type { case .whenInUse: manager.requestWhenInUseAuthorization() case .always: manager.requestAlwaysAuthorization() } } // Handle mismatched allowed and exit guard !isAuthorized else { if startUpdating { self.startUpdating() } // Process callback in case authorization dialog not launched by OS // since user will be notified first time only and inored subsequently completion?(false) return } if startUpdating { didChangeAuthorizationSingle += { _ in self.startUpdating() } } // Handle denied and exit guard CLLocationManager.authorizationStatus() == .notDetermined else { completion?(false); return } if let completion = completion { didChangeAuthorizationSingle += completion } } /// Request the one-time delivery of the user’s current location. /// /// - Parameter completion: The completion with the location object. func requestLocation(completion: @escaping LocationHandler) { didUpdateLocationsSingle += completion manager.requestLocation() } }
And the simple Observer
 type to workaround equatable closures:
public struct Observer<T> { let id: UUID let handler: T public init(_ id: UUID = UUID(), handler: T) { self.id = id self.handler = handler } } extension Observer: Equatable { public static func ==(lhs: Observer, rhs: Observer) -> Bool { return lhs.id == rhs.id } }
Breaking It Down
In the custom LocationManager
, the CLLocationManager
 is kept private internally. Then we route the internal delegate calls to the closure queues for execution, which will notify the listeners who’ve subscribed.
/// Subscribes to receive new location data when available. var didUpdateLocations = SynchronizedArray<LocationObserver>() var didUpdateLocationsSingle = SynchronizedArray<LocationHandler>() /// Subscribes to receive new authorization data when available. var didChangeAuthorization = SynchronizedArray<AuthorizationObserver>() var didChangeAuthorizationSingle = SynchronizedArray<AuthorizationHandler>()
We are keeping a queue of closures to execute all the time in an observable manner, and the other queue is for single requests like requestLocation,
 which will called and removed to prevent duplicate firing. The delegates execute the closures in the arrays:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last else { return } // Trigger and empty queues didUpdateLocations.forEach { $0.handler(location) } didUpdateLocationsSingle.removeAll { $0.forEach { $0(location) } } } func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { guard status != .notDetermined else { return } // Trigger and empty queues didChangeAuthorization.forEach { $0.handler(isAuthorized) } didChangeAuthorizationSingle.removeAll { $0.forEach { $0(self.isAuthorized) } } }
With these in place, the closures can be added to the location manager to be held in queue until the delegate functions fire them:
locationManager.requestLocation { // Single execution } locationManager.didUpdateLocations += Observer { // Multiple execution }
Requesting Authorization
A tricky area is requesting authorization since there are several states the user’s authorization can fall into. For this reason, this custom LocationManager
 has a smarter authorization request implementation:
func requestAuthorization(for type: AuthorizationType = .whenInUse, startUpdating: Bool = false, completion: AuthorizationHandler? = nil) { // Handle authorized and exit guard !isAuthorized(for: type) else { if startUpdating { self.startUpdating() } completion?(true) return } // Request appropiate authorization before exit defer { switch type { case .whenInUse: manager.requestWhenInUseAuthorization() case .always: manager.requestAlwaysAuthorization() } } // Handle mismatched allowed and exit guard !isAuthorized else { if startUpdating { self.startUpdating() } // Process callback in case authorization dialog not launched by OS // since user will be notified first time only and inored subsequently completion?(false) return } if startUpdating { didChangeAuthorizationSingle += { _ in self.startUpdating() } } // Handle denied and exit guard CLLocationManager.authorizationStatus() == .notDetermined else { completion?(false); return } if let completion = completion { didChangeAuthorizationSingle += completion } }
A few things come into play with this extended requestAuthorization
 function:
- The permission type needed is handled by an enum parameter:
requestAuthorization(for: .whenInUse)
. This way, calling `requestWhenInUseAuthorization` or `requestAlwaysAuthorization` is abstracted away. - The
startUpdating
 boolean parameter automatically starts the location update services after authorization is offered to the user. It is queued up in the `didChangeAuthorization` when needed to ensure it is called after authorization has been given. - If the required authorization is already given, everything is ignored and start updates occur if specified.
- If the authorization is already given, but is not the level requested, it will start updates immediately since this can still occur, but will return false in the callback to specify the requested authorization is not given.
- If the authorization is denied, it will queue up start updates for later execution.
The request authorization can now conveniently be used like this:
locationManager.requestAuthorization(for: .whenInUse, startUpdating: true) { // Presents OS dialog to user if applicable // Then start location updates when authorized guard !$0 else { return } // Present alert to route user to app settings if needed }
Shared Singleton
Many times, a single `CoreLocation` manager is needed for your entire app. This helps in conserving power and simplifying data flow. For this reason, the following pattern was created. In the `AppDelegate`, we add the static location manager:
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? /// Global location manager static var locationManager: LocationManager = { return $0 }(LocationManager()) }
With this global location manager in place and lazily loaded, view controllers can reference the location manager like this:
class LocationController: UIViewController { var locationManager: LocationManager { return AppDelegate.locationManager } ... }
Conclusion
The CLLocationManager
 has been wrapped in a modern, Swifty closure-based pattern. Subscribing to updates instead of using the one-size-fit-all delegate functions seems more natural and scales better. For a working sample, you can download the code here.
Happy Coding!!
**UPDATE**
In favor of a more simplified, native-like experience, I’ve updated the API to add observers from this:
locationManager.didUpdateLocations += locationObserver
To this:
locationManager.addObserver(locationObserver)
This abstracts away the inner workings of the observer arrays. For a complete sample, see the below example. In addition, LocationManager
 has been pulled into the ZamzamKit library and will be maintained there going forward. The latest version of the Swifty location manager can be tracked here.
import UIKit import CoreLocation import ZamzamKit class SecondViewController: UIViewController { @IBOutlet weak var outputLabel: UILabel! var locationManager: LocationManager = { return LocationManager( desiredAccuracy: kCLLocationAccuracyThreeKilometers, distanceFilter: 1000 ) }() var locationObserver = Observer<LocationManager.LocationHandler> { print($0.description) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) locationManager.addObserver(locationObserver) locationManager.addObserver(headingObserver) locationManager.requestAuthorization( for: .whenInUse, startUpdatingLocation: true) { guard $0 else { return self.present( alert: "Allow “My App” to Access Your Current Location?".localized, message: "Coordinates needed to calculate your location.".localized, buttonText: "Allow".localized, includeCancelAction: true, handler: { guard let settings = URL(string: UIApplicationOpenSettingsURLString) else { return } UIApplication.shared.open(settings) } ) } self.locationManager.startUpdatingHeading() } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) locationManager.removeObservers() } deinit { locationManager.removeObservers() } } extension SecondViewController { var headingObserver: Observer<LocationManager.HeadingHandler> { return Observer { print($0.description) } } }
I agree that all of the examples in these closure/delegate blogs are valid to be “modernized” with closures, and it is true that many uses of the delegate can be replaced with closures (some in the SDK are legacy because Objective-C did not have blocks until 2014), however I disagree with the implied assertions that it is best in the general case and that closures are a 1-to-1 more modern replacement for the delegate pattern. Take for example, a single call or instantiation that results in some process, of which you want to receive a large number of lifecycle calls and it is likely someone will be interested in most of them (see UIApplication delegate). In my opinion it is still cleaner to use a delegate and to have a nice clean protocol implementation in one place, rather than to have to register for every lifecycle event you want and possibly have that code spread out everywhere. I cannot imagine having to debug an iOS application lifecycle with UIApplicationDelegate callbacks being registered everywhere throughout the codebase. Depending on your needs, with a delegate you can require implementation and get compiler help for users of your API. So, I don’t believe it is the case that Apple hasn’t gotten around to replacing all of the delegates, some of them still make sense.
Hey Lance!
Yes I like the simplicity and elegance that delegates offer and they do still have their place, UIApplication and UIViewController are great examples as you mention. I think the observable nature of CoreLocation, CoreBluetooth, CoreData and few others are a perfect fit for closures though and I would go so far as to say they should be overhauled with a closure pattern. This brings us closer to a functional reactive world, which can and should co-exist with the delegate paradigm. You bring up really good points I plan to keep in mind, there should be a clearer distinction of when to use closures and delegates. From this discussion, perhaps a good rule of thumb is closures are better suited for observables and delegates for lifecycles..