A publisher is nothing more than a sequence of elements over time. Your application typically performs an action when a publisher emits an element. In some scenarios, the action you take requires the elements the publisher emitted up until that point. That is where the scan
and tryScan
operators come in useful. This may sound confusing so let's take a look at an example.
Calculating Distance with the Scan Operator
Let's say you are building an application for runners and cyclists. It uses the location services (GPS, Wi-Fi, Bluetooth) to track the user's location. Every time the location services emit a location, a set of coordinates, your application calculates the distance the user has run or cycled up until that point. Let's put that into code.
We create an array of CLLocation
instances and convert the array to a publisher using the computed publisher
property. The locations publisher mocks or mimics the behavior of the location services of the device in this example.
let locations: [CLLocation] = [
.init(latitude: 50.88194, longitude: 4.68139),
.init(latitude: 50.88083, longitude: 4.67972),
.init(latitude: 50.87707, longitude: 4.68471),
.init(latitude: 50.87569, longitude: 4.68539),
.init(latitude: 50.87584, longitude: 4.68528),
.init(latitude: 50.87882, longitude: 4.67535),
.init(latitude: 50.87921, longitude: 4.67374)
]
locations.publisher
We apply the scan
operator to the locations publisher to calculate the distance the user has run or cycled. The signature of the scan(_:_:)
method may remind you of that of the reduce(_:_:)
method. It too accepts two arguments, the initial result and a closure that accepts the previous result and the element the publisher emits. Don't worry if this doesn't make sense. Let's continue with the implementation to put the pieces of the puzzle together.
Every time the locations publisher emits an element, a location, we need to take two actions. First, we calculate the distance between the current location and the previous location. Second, we add that distance to the distance between the previous locations. Let's create a simple type that makes this a bit easier to manage.
Declare a struct with name Route
at the top. The Route
struct declares two properties, locations
of type [CLLocation]
and distance
of type CLLocationDistance
. CLLocationDistance
is defined by the Core Location framework and nothing more than a type alias for Double
.
struct Route {
// MARK: - Properties
let locations: [CLLocation]
let distance: CLLocationDistance
}
The initial result we pass to the scan(_:_:)
method is a route with no locations and a distance of 0.0
.
Route(locations: [], distance: 0.0)
The second argument we pass to the scan(_:_:)
method is a closure that takes the previous partial result, a Route
object, and the element, a CLLocation
instance, the upstream publisher emits. The upstream publisher is the locations publisher to which the scan
operator is applied. Let's put that into code.
locations.publisher
.scan(Route(locations: [], distance: 0.0)) { route, currentLocation in
}
To calculate the distance between currentLocation
and the previous location, we ask the previous partial result for the last element of its array of CLLocation
instances. We use a guard
statement to safely access the previous location. In the else
clause of the guard
statement, we return a Route
object with the current location and a distance of 0.0
.
locations.publisher
.scan(Route(locations: [], distance: 0.0)) { route, currentLocation in
guard let previousLocation = route.locations.last else {
return Route(locations: [currentLocation], distance: 0.0)
}
}
Calculating the distance between the current location and the previous location is simple thanks to the Core Location framework. We invoke the distance(from:)
method on the previous location, passing in the current location as an argument. The method returns a value of type CLLocationDistance
, a distance in meters.
locations.publisher
.scan(Route(locations: [], distance: 0.0)) { route, currentLocation in
guard let previousLocation = route.locations.last else {
return Route(locations: [currentLocation], distance: 0.0)
}
let distance = previousLocation.distance(from: currentLocation)
}
With the distance between the previous location and the current location calculated, we can create a Route
object. We append the current location to the array of locations of the previous route and add the value stored in distance
to the value of the previous route's distance
property.
locations.publisher
.scan(Route(locations: [], distance: 0.0)) { route, currentLocation in
guard let previousLocation = route.locations.last else {
return Route(locations: [currentLocation], distance: 0.0)
}
let distance = previousLocation.distance(from: currentLocation)
return Route(
locations: route.locations + [currentLocation],
distance: route.distance + distance
)
}
Let's subscribe to the publisher the scan
operator returns by invoking the sink(_:)
method. In the value handler, we print the value of the distance
property to the console. The output shows the distance the user has run or cycled.
let locations: [CLLocation] = [
.init(latitude: 50.88194, longitude: 4.68139),
.init(latitude: 50.88083, longitude: 4.67972),
.init(latitude: 50.87707, longitude: 4.68471),
.init(latitude: 50.87569, longitude: 4.68539),
.init(latitude: 50.87584, longitude: 4.68528),
.init(latitude: 50.87882, longitude: 4.67535),
.init(latitude: 50.87921, longitude: 4.67374)
]
locations.publisher
.scan(Route(locations: [], distance: 0.0)) { route, currentLocation in
guard let previousLocation = route.locations.last else {
return Route(locations: [currentLocation], distance: 0.0)
}
let distance = previousLocation.distance(from: currentLocation)
return Route(
locations: route.locations + [currentLocation],
distance: route.distance + distance
)
}.sink { route in
print(route.distance)
}.store(in: &subscriptions)
0.0
170.47310410765783
716.6313763818551
877.4369483034849
895.832492767225
1669.3916673867675
1790.7329854058826
Endless Possibilities
The scan
operator, and its throwing sibling tryScan
, are quite powerful and I feel they are somewhat underused. Let's take the example of this episode. Most developers might be inclined to subscribe to the locations publisher, store the locations in a property, and calculate the distance of the path every time the publisher emits a location.
That is a viable approach and it isn't necessarily wrong. That said, the less state your application manages the better and that is one of the benefits of reactive programming. Your application doesn't hold on to state if it doesn't have to. It simply observes a stream or sequence of elements and takes actions when appropriate.
What's Next?
It is important to mindful of memory consumption when using the scan
and tryScan
operators. In the example of this episode, we create a Route
object every time the locations publisher emits a location. The Route
object stores the locations the locations publisher emits and that array grows over time. The longer the runner or cycler exercises, the larger the array of locations becomes. If you work with complex objects that take up a non-trivial amount of memory, then you need to be mindful of memory consumption and performance.