In Swift Async Sequence extensions (part 1) I covered simple ways to create an async sequence using a custom factory method and binding a sequence to a UI control via a custom assign method.

This covered the “beginning” and “end” of the data stream (so to say) but what about processing or converting the data along the way? If you draw a parallel to Combine code — how would you build custom “operators” for your async sequence?

In this post I’ll show you exactly that — building a method that converts one async sequence into another.

Existing AsyncSequence methods

Plenty of the new Swift Concurrency code is freely available at Apple’s GitHub so should you want to peak in you can.

For example here’s the AsyncSequence.filter(_:) source code:

  • It defines an extension on AsyncSequence
  • Adds the filter method
  • And finally declares a type called AsyncFilterSequence — this is what the filter method returns.

This is very similar setup to what the Combine operators do — most often they are a method on the Publisher protocol and return some kind of custom publisher that implements the operator logic. So no surprises here.

If you want inspiration for build your own async sequence methods you can check out map, prefix, and so on.

Note: The whole repo is very interesting to read through. For example have a look at how Task.sleep(...) looks like.

Building a custom async sequence method

For this article I’ll build an alternative of the Publisher.replaceNil(_:) method from Combine. It’s a handy operator that replaces any nil values in the stream with a given constant.

Following the pattern established in the files I linked above, I’ll start by creating a custom async sequence type called AsyncReplaceNilSequence:

1
2
3
4
5
6
struct AsyncReplaceNilSequence<T, Base: AsyncSequence> :
  AsyncSequence where Base.Element == T? {

  typealias Element = T

}

This new sequence AsyncReplaceNilSequence is generic (T) over its elements. An additional where clause requires that the base sequence has optional values (T?). This way if the base sequence contains, for example, elements String? — the resulting new sequence will have all nil values replaced and the output elements will be of type String.

Next, I need a proper initializer that will take the base sequence and the constant to replace nil values with:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let base: Base
let replaceValue: T

init(
  _ base: Base,
  replaceValue: T
) {
  self.base = base
  self.replaceValue = replaceValue
}

The property base holds onto the “source” sequence (or the base sequence) and replaceValue is a constant to replace nil values with.

The final step in building the sequence is to define the sequence iterator. First I’ll add the sequence method that creates the iterator:

1
2
3
4
5
6
func makeAsyncIterator() -> Iterator {
  return Iterator(
    base.makeAsyncIterator(), 
    replaceValue: replaceValue
  )
}

And then I’ll add Iterator as a nested structure inside AsyncReplaceNilSequence:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct Iterator: AsyncIteratorProtocol {
  typealias Element = T

  var baseIterator: Base.AsyncIterator
  let replaceValue: Base.Element

  init(
    _ baseIterator: Base.AsyncIterator,
    replaceValue: Base.Element
  ) {
    self.baseIterator = baseIterator
    self.replaceValue = replaceValue
  }
}

Very similarly to the sequence type itself, the iterator also holds on to its base counterpart and the value to use for replacing.

The meaningful step here is defining the iterator next() method that will pull values from the base sequence and replay them while replacing any nil values:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
mutating func next() async throws -> T? {
  guard let element = try await baseIterator.next() else {
    return nil
  }

  guard element != nil else {
    return replaceValue
  }

  return element
}

The method fetches the next() value from its base iterator and in case the result is nil it returns the replace constant instead.

So now I have an async sequence and a working iterator. I only need the method on AsyncSequence before I wrap up. I chose to mimic the method signature of Combine’s replaceNil() like so:

1
2
3
4
5
6
7
extension AsyncSequence {
  func replaceNil<T>(with output: T) 
    -> AsyncReplaceNilSequence<T, Self> where Element == T? 
  {
    return AsyncReplaceNilSequence(self, replaceValue: output)
  }
}

The method is generic over its parameter type and that allows it to accept a String parameter if the sequence is String? or an Int parameter if the sequence is Int?, and not be available at all if the sequence is not of an optional element type.

Trying the code in a view

Let’s give the code a try. I’ll reuse my code from part 1 including my Array.spread() and AsyncSequence.assign() methods that create a sequence of values over time and bind them to a label on screen.

I’ll use an [Int?] array to display some numbers on screen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct ContentView: View {
  @State var counter = ""

  var body: some View {
    Text("Update: \(counter)")
      .task {

        [1, 2, nil, 4, nil, 6]
          .spread(delay: 1.5)
          .map { "\($0)" }
          .assign(to: \.counter, on: self)
      }
  }
}

I’m using map to convert the optional number to a string before binding it to the @State property. That displays the description of the optional instead of the value by default:

And this is one of the common use cases that replaceNil(_:) is good for — if you have a fallback value for nil you can unwrap the sequence by removing the optional from the generic and bind the sequence directly like so:

1
2
3
4
5
[1, 2, nil, 4, nil, 6]
  .spread(delay: 1.5)
  .replaceNil(with: 0)
  .map { "\($0)" }
  .assign(to: \.counter, on: self)

And that’s all for today! I this code and spelling out the steps was useful — you can find the complete source code below.

The completed source code

This is a whole bunch of code but including it below in case you’d like to copy it over and play with it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
extension AsyncSequence {
  func replaceNil<T>(with output: T)
    -> AsyncReplaceNilSequence<T, Self> where Element == T?
  {
    return AsyncReplaceNilSequence(self, replaceValue: output)
  }
}

struct AsyncReplaceNilSequence<T, Base: AsyncSequence>
  : AsyncSequence where Base.Element == T? {

  typealias Element = T
  typealias AsyncIterator = Iterator

  let base: Base
  let replaceValue: T

  init(
    _ base: Base,
    replaceValue: T
  ) {
    self.base = base
    self.replaceValue = replaceValue
  }

  func makeAsyncIterator() -> Iterator {
    return Iterator(
      base.makeAsyncIterator(),
      replaceValue: replaceValue
    )
  }

  struct Iterator: AsyncIteratorProtocol {
    typealias Element = T

    var baseIterator: Base.AsyncIterator
    let replaceValue: Base.Element

    init(
      _ baseIterator: Base.AsyncIterator,
      replaceValue: Base.Element
    ) {
      self.baseIterator = baseIterator
      self.replaceValue = replaceValue
    }

    mutating func next() async throws -> T? {
      guard let element = try await baseIterator.next() else {
        return nil
      }

      guard element != nil else {
        return replaceValue
      }

      return element
    }

  }
}

One final note — when you’re building a method like this for production it makes sense to optimize for performance and add inline-able decorators. If you want to get some insight on how Apple’s done that have a look at AsyncSequence.map’s source code.

Where to go from here?

If you’d like to support me, get my book on Swift Concurrency:

» swiftconcurrencybook.com «

Interested in discussing the new async/await Swift syntax and concurrency? Hit me up on twitter at https://twitter.com/icanzilb.