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.