Modern Concurrency: Beyond the Basics

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

Part 1: AsyncStream & Continuations

03. Using AsyncStream to Count Down

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: 02. AsyncStream Next episode: 04. Using AsyncStream for Notifications

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.

Notes: 03. Using AsyncStream to Count Down

This video uses Xcode 14’s Task.sleep(until:clock:). If you use Xcode 13, replace this with Task.sleep(nanoseconds: 1_000_000_000).

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

Final Blabber app

For the rest of this part of the course, you’ll work on this messaging app. In the next two episodes, you’ll add a countdown timer and show notifications of users arriving and leaving. And in episodes 7 and 8, you’ll implement this Location button.

Run the course server

Most of the projects in this course interact with a server. It’s included in the course materials.

Starter Blabber app

In the course materials, locate the Blabber starter project and open it.

Starter code

In BlabberModel, the MainActor chat() method already has the usual URLSession code:

let (stream, response) = try await liveURLSession.bytes(from: url)
try await withTaskCancellationHandler {
  print("End live updates")
  messages = []
} operation: {
  try await readMessages(stream: stream)
}

Parsing the server responses

Scroll down a little to readMessages(stream:):

var iterator = stream.lines.makeAsyncIterator()
guard let first = try await iterator.next() else {
  throw "No response from server"
}
guard
  let data = first.data(using: .utf8),
    let status = try? JSONDecoder()
    .decode(ServerStatus.self, from: data) else {
      throw "Invalid response from server"
    }

Storing and using the chat information

There’s also a Message data model. It has a convenience initializer that only needs a message value.

messages.append(
  Message(
    message: "\(status.activeUsers) active users"
  )
)
for try await line in stream.lines {
  if let data = line.data(using: .utf8),
    let update = try? JSONDecoder().decode(Message.self, from: data) {
    messages.append(update)
  }
}

Creating an asynchronous timer with AsyncStream

In the simulator, the chat view has two buttons next to the Message field: You’ll implement the show-location button in episode 7, and you’ll implement this countdown button now.

Button(action: {
  Task {
    do {
      let countdownMessage = message
      message = ""
      try await model.countdown(to: countdownMessage)
    } catch {
      lastErrorMessage = error.localizedDescription
    }
  }
}, label: {
  Image(systemName: "timer")
    .font(.title)
    .foregroundColor(Color.gray)
})
var countdown = 3
let counter = AsyncStream<String> { 
  
}
do {
  try await Task.sleep(until: .now + .seconds(1),
                       clock: .continuous)
} catch {
  return nil
}
switch countdown {
case (1...): return "\(countdown)..."
case 0: return "🎉 " + message
default: return nil
}
defer { countdown -= 1 }
🟥
switch countdown {
case (1...): return "\(countdown)..."
case 0: return "🎉 " + message
default: return nil
}
for await countdownMessage in counter {
  try await say(countdownMessage)
}