Modern Concurrency: Getting Started

Oct 18 2022 · Swift 5.5, iOS 15, Xcode 13.4

Part 1: Asynchronous Code

04. Asynchronous Sequences

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 03. Your First Asynchronous App Next episode: 05. Using AsyncSequence in Views

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Heads up... You've reached locked video content where the transcript will be shown as obfuscated text.

In this episode, you’ll learn more about async/await, then start exploring asynchronous sequences. You’ll need this to implement the live ticker view in the LittleJohn app.

Part 1: Asynchronous properties and subscripts

In the course materials, locate the starter playground and open it.

Async computed property

You can mark read-only computed properties with async and throws.

extension Domains {
  static var domains: [Domain] {
    get async throws {
      try await fetchDomains()
    }
  }
}
for domain in try await Domains.domains {

Async subscript

Now, scoll up to the second extension:

extension Domains {
  enum Error: Swift.Error { case outOfRange }
  static subscript(_ index: Int) -> String {
    get async throws {
      return ""
    }
  }
}
extension Domains {
  enum Error: Swift.Error { case outOfRange }
  static subscript(_ index: Int) -> String {
    get async throws {
      🟩
      let domains = try await Self.domains
      guard domains.indices.contains(index) else {
        throw Error.outOfRange
      }
      return domains[index].attributes.name
      🟥
    }
  }
}
Task {
  dump(try await Domains[4])  
}
- "Game Tech"

Part 2: Asynchronous sequences

Another powerful abstraction that Swift concurrency gives you is the asynchronous sequence. It’s like the standard Swift sequence, except getting each element may cause the task to suspend.

func findTitle(url: URL) async throws -> String? {
  for try await line in url.lines {
    if line.contains("<title>") {
      return line.trimmingCharacters(in: .whitespaces)
    }
  }
  return nil
}
Task {
  let url = URL(string: "https://www.raywenderlich.com")!
  if let title = try await findTitle(url: url) {
    print(title)
  }
}
<title>raywenderlich.com | High quality programming tutorials: iOS, Android, Swift, Kotlin, Flutter, Server Side Swift, Unity, and more!</title>

Sequence iterator

The for-in loop is really a while loop calling the next() method of an iterator for the sequence url.lines. You can create the iterator yourself, if you only want a fixed number of elements.

var iterator = url.lines.makeAsyncIterator()
if let next = try await iterator.next() {
  print("\n\(next)")
}
<title>raywenderlich.com | High quality programming tutorials: iOS, Android, Swift, Kotlin, Flutter, Server Side Swift, Unity, and more!</title>

<!DOCTYPE html>

Custom AsyncSequence

URL and some other built-in types have built-in async sequences, but how do you create your own async sequence? Take a closer look at AsyncSequence:

struct Typewriter: AsyncSequence {
}
struct Typewriter: AsyncSequence {
  typealias AsyncIterator = <#type#>
  typealias Element = <#type#> 
}
struct Typewriter: AsyncSequence {
  typealias AsyncIterator = 🟩TypewriterIterator🟥
  typealias Element = 🟩String🟥
}
struct TypewriterIterator: AsyncIteratorProtocol {
  typealias Element = String
  
  mutating func next() async throws -> String? {
    return ""
  }
}
func makeAsyncIterator() -> TypewriterIterator {
  return TypewriterIterator()
}
struct Typewriter: AsyncSequence {
  typealias Element = String
  🟩let phrase: String🟥

  func makeAsyncIterator() -> TypewriterIterator {
    return TypewriterIterator(🟩phrase🟥)
  }
}
struct TypewriterIterator: AsyncIteratorProtocol {
  typealias Element = String
  🟩let phrase: String
  var index: String.Index

  init(_ phrase: String) {
    self.phrase = phrase
    self.index = phrase.startIndex
  }
  🟥
  mutating func next() async throws -> String? {
    return ""
  }
}
mutating func next() async throws -> String? {
  🟩
  guard index < phrase.endIndex else {
    return nil
  }
  try await Task.sleep(until: .now + .seconds(1),
                       clock: .continuous)
  defer {
    index = phrase.index(after: index)
  }
  return String(phrase[phrase.startIndex...index])
  🟥
}
Task {
  for try await item in Typewriter(phrase: "Hello, world!") {
    print(item)
  }
  print("Done")
}
He
Hel
Hell
Hello
Hello,
Hello, 
Hello, w
Hello, wo
Hello, wor
Hello, worl
Hello, world
Hello, world!
Done

Part 3: Cancel a task

To finish this episode, learn how to cancel a task. Here are the two tasks from episode 2:

Task {
  print("\nDoing some work on an unnamed task")
  let sum = (1...100000).reduce(0, +)
  print("Unnamed task done: 1 + 2 + 3 ... 100000 = \(sum)")
}
print("Doing some work on the main queue")
print("Doing more work on the main queue")

// This task runs after previous task finishes
let task = Task {
  print("\nDoing some work on a named task")
  // TODO: Check for cancellation before doing work

  let sum = (1...100000).reduce(0, +)
  print("Named task done: 1 + 2 + 3 ... 100000 = \(sum)")
}
print("Doing yet more work on the main queue")
task.cancel()
print("\nCanceled task")
let task = Task {
  print("\nDoing some work on a named task")
  // Check for cancellation before doing work
   🟩try Task.checkCancellation()🟥
  let sum = (1... 100000).reduce(0, +)
  print("Named task done: 1 + 2 + 3 ... 100000 = \(sum)")
}
print("Doing yet more work on the main queue")

task.cancel()
Doing some work on an unnamed task
Doing some work on the main actor
Doing more work on the main actor
Doing yet more work on the main queue
Canceled task
Unnamed task done: 1 + 2 + 3 ... 100000 = 5000050000

Doing some work on a named task
if Task.isCancelled {
  print("Task canceled")
  throw CancellationError()
}