Categories
apple ios

Combine & CoreLocation, Part 1 — Publishers & Delegates — Learning Swift

Leo Dion

Most of the APIs from Apple come from an technology of Objective-C and the Delegation Pattern. With this in thoughts, the problem is figuring methods to adapt for SwiftUI. Specifically we wish to create Publishers from delegates the use of Combine.

For example, my app Heartwitch is an Apple Watch app for are living streamers. In this example, It makes use of HealthKit which implants the delegation development regularly. Additionally, I’m the use of more recent applied sciences equivalent to Vapor 4, Independent Watch Apps, and most significantly SwiftUI.

In this sequence of articles, I’d like to enter element in regards to the strategy of adapting an older API for Combine. Specifically we’re going to be construction a fundamental SwiftUI app which presentations your latitude and longitude with CoreLocation. This contains:

  • Creating Publishers from Delegates

For this phase, we’ll be coming into methods to create a Protocol and Class which can act as a go-between for the Delegation Pattern and the Reactive Functional Programming of SwiftUI and Combine.

For over a decade, Apple regularly used the Delegation Pattern for giving builders the power to reply, replace, and act instead of a UI object. This development has a plethora of advantages particularly in Objective-C. However, with Swift and particularly SwiftUI, this development turns into awkward.

This is the place it turns into essential to make a delegate reply in any such means that SwiftUI can maintain updates.

With Apple’s older APIs, we most often see this:

protocol NSDelegate : NSObjectProtocol {
func supervisor(_ supervisor: NSManager, finishedWith knowledge: AnyObject)
func supervisor(_ supervisor: NSManager, grantedPermission: Bool)
}
magnificence NSManager : NSObject {
vulnerable var delegate : NSDelegate?

func requestAuthorization() {}
func doThing () {}
}

In the case of CoreLocation we see this:

protocol CLLocationSupervisorDelegate : NSObjectProtocol {
func locationManager(_: CLLocationSupervisor, didUpdateLocations places: [CLLocation])
func locationManager(_: CLLocationSupervisor, didChangeAuthorization standing: CLAuthorizationStatus)
}
magnificence CLLocationSupervisor : NSObject {
vulnerable var delegate : CLLocationSupervisorDelegate?

func requestWhenInUseAuthorization() {}
func startUpdatingLocation () {}
}

In different phrases, we’ll wish to create Combine Publishers which our ObservableObject can concentrate or react to. Once the ObservableObject reacts correctly, then the View will replace accordingly. In the top, we must see this in our application:

Before we setup our publishers, let’s scaffold our View and ObservableObject.

Let’s first get started via construction our SwiftUI View. In this example, we’ll be making a SwiftUI view together with an ObservableObject.

struct LocationView: View {
// CLLocationSupervisor is principally a singleton so an EnvironmentObject ObservableObject is sensible
@EnvironmentObject var locationObject: CoreLocationObject
var frame: some View {
VStack {
// use our extension technique to show an outline of the standing
Text("(locationObject.authorizationStatus.description)")
.onTapGesture {
self.locationObject.authorize()
}
// use Optional.map to cover the Text if there is no location
self.locationObject.location.map {
Text($0.description)
}
}
}
}

This LocationView will merely show one line with an outline of the site with a line describing the CLAuthorizationStatus the use of this extension:

extension CLAuthorizationStatus: CustomizedStringConvertible {
public var description: String {
transfer self {
case .licensedAlways:
go back "Always Authorized"
case .licensedWhenInUse:
go back "Authorized When In Use"
case .denied:
go back "Denied"
case .no longerDetermined:
go back "Not Determined"
case .limited:
go back "Restricted"
@unknown default:
go back "🤷‍♂️"
}
}
}

Now let’s move forward and outline our ObservableObject, named CoreLocationObject:

import Combine
import CoreLocation
import SwiftUI
magnificence CoreLocationObject: ObservableObject {
@Published var authorizationStatus = CLAuthorizationStatus.no longerDetermined
@Published var location: CLLocation?
init() { }
}

Lastly, just remember to set the EnvironmentObject for your application the use of:

LocationView().environmentObject(CoreLocationObject())

Now, now we have our scaffolding setup, let’s plug-in CoreLocation.

With the Delegation Pattern, the Delegate (on this case CoreLocationSupervisorDelegate) will obtain location updates. Therefore it’s the splendid object to create publishers for our ObservableObject.

In order for our ObservableObject to react to CoreLocation adjustments, the delegate must create Publishers for us. With this in thoughts, I’ve prolonged to delegate to be a Publicist. That is to mention the Delegate may also be a Publisher Factory.

protocol CLLocationSupervisorCombineDelegate: CLLocationSupervisorDelegate {
func authorizationPublisher() -> AnyPublisher<CLAuthorizationStatus, Never>
func locationPublisher() -> AnyPublisher<[CLLocation], Never>
}

In the case of our application, we’re showing the authorization standing of Core Location in addition to the latitude and longitude. Therefore, we handiest want two strategies carried out for our publishers.

Here is the implementation of our new protocol:

magnificence CLLocationSupervisorPublicist: NSObject, CLLocationSupervisorCombineDelegate {
let authorizationSubject = PassthroughSubject<CLAuthorizationStatus, Never>()
let locationSubject = PassthroughSubject<[CLLocation], Never>()
func authorizationPublisher() -> AnyPublisher<CLAuthorizationStatus, Never> {
go back Just(CLLocationSupervisor.authorizationStatus())
.merge(with:
authorizationSubject.compactMap { $0 }
).eraseToAnyPublisher()
}
func locationPublisher() -> AnyPublisher<[CLLocation], Never> {
go back locationSubject.eraseToAnyPublisher()
}
func locationManager(_: CLLocationSupervisor, didUpdateLocations places: [CLLocation]) {
locationSubject.ship(places)
}
func locationManager(_: CLLocationSupervisor, didFailWithError _: Error) {
// Implement to keep away from crashes
// Extra Credit: Create a writer for mistakes :/
}
func locationManager(_: CLLocationSupervisor, didChangeAuthorization standing: CLAuthorizationStatus) {
authorizationSubject.ship(standing)
}
}

Let’s breakdown how this magnificence works.

Our Publicist doesn’t wish to grasp onto any values. In the top, it only serves the aim of reworking knowledge from the CoreLocationSupervisor to the ObservableObject. For this reason why, we will be the use of a PassthroughSubject for the CLLocation and CLAuthorizationStatus. That is to mention, PassthroughSubject does not grasp any values as they obtain values however passes them on.

With the PassthroughSubject houses in position, our delegate can ship the values won from the delegate the best way to the topics.

Creating our first writer for CLLocation is reasonably easy:

magnificence CLLocationSupervisorPublicist: NSObject, CLLocationSupervisorCombineDelegate {
...
let locationSubject = PassthroughSubject<[CLLocation], Never>()
func locationPublisher() -> AnyPublisher<[CLLocation], Never> {
go back locationSubject.eraseToAnyPublisher()
}
func locationManager(_: CLLocationSupervisor, didUpdateLocations places: [CLLocation]) {
locationSubject.ship(places)
}
...
}

What’s vital to comprehend is that we wish to put in force sort erasure the use of eraseToAnyPublisher. The advent of SwiftUI and Combine integrated enhancements to Swift. These growth permit for robust transformations which can lead to reasonably complicated Generic Types. For example our authorizationPublisher has a go back sort AnyPublisher:

func authorizationPublisher() -> AnyPublisher<CLAuthorizationStatus, Never> {
go back Just(CLLocationSupervisor.authorizationStatus())
.merge(with:
authorizationSubject.compactMap { $0 }
).eraseToAnyPublisher()
}

Without eraseToAnyPublisher, the go back sort can be:

Publishers.Merge<Just<CLAuthorizationStatus>, Publishers.CompactMap<PassthroughSubject<CLAuthorizationStatus, Never>, CLAuthorizationStatus>>

Likewise with the locationPublisher, the go back sort can be:

PassthroughSubject<[CLLocation], Never>

In the top, this makes growing Protocols and Return Types reasonably complicated. As a long way because the ObservableObject is anxious, it does not care how the writer is reworked however the end result varieties returned.

Therefore our Protocol handiest wishes a AnyPublisher go back sort. In the top, we will be able to each simplify and conceal the process of purposeful transformation the use of eraseToAnyPublisher. Likewise, the implementation calls eraseToAnyPublisher to scale back the go back sort and fit the Protocol’s means signature.

Now that we’ve got discovered methods to growing matching Publisher varieties, let’s turn into CLAuthorizationStatu s so it’s usable inside the view.

While our locationSubject displays the values from CoreLocation, the authorizationSubject will probably be out of sync from the truth of CoreLocation‘s standing. For this reason why, we will wish to write some code come with the preliminary standing together with regardless of the PassthroughSubject receives.

func authorizationPublisher() -> AnyPublisher<CLAuthorizationStatus, Never> {
go back Just(CLLocationSupervisor.authorizationStatus())
.merge(with:
authorizationSubject.compactMap { $0 }
).eraseToAnyPublisher()
}

While CoreLocationSupervisorDelegate sends updates to authorizationStatus, we wish to have get entry to to the preliminary standing by the use of CLLocationSupervisor.authorizedStatus. Luckily, Combine features a integrated Publisher for unmarried values the use of Just.

Just provides us the preliminary price printed then again we wish to come with the remainder printed values from our PassthroughSubject. For this reason why, we will be able to use merge to enroll in the intial price with the outcome from the our authorizationSubject:

We have the writer factories setup now!

In our subsequent phase on this sequence, we’ll discover ways to use this implementation in our ObservableObject CoreLocationObject. Specifically, we will be coming into energy of serve as programming inside Reactive Functional Programming. Enjoy!