Leave a rating/review
Refresh your browser to make sure the course server is running or restart the server in Terminal.
Continue with your project from episode 3 or open the starter project for this episode. Build and run.
Async sequence
In episode 3, you used Swift concurrency to fetch this list of available stock symbols.
You called availableSymbols in SymbolListView. The app needs to do this only once so you called availableSymbols only if symbols is empty.
In the simulator, select a stock symbol then tap Live ticker.
This screen needs to display the selected stock symbols and what appears to be a live feed of stock prices. Stop the run.
Go to LittleJohnModel and locate startTicker(). You need to add code to this method to fetch a continuous sequence of values from the server. In fact, you’ll fetch an asynchronous sequence, which lets you iterate over its elements asynchronously as more elements become available over time. You’ll learn more about asynchronous sequences in episode 9.
First, go to TickerView and call startTicker(_:): Scroll down to the TODO below padding and add this code:
.padding
// TODO...
🟩
.task {
do {
try await model.startTicker(selectedSymbols)
} catch {
lastErrorMessage = error.localizedDescription
}
}
This is the same task-do-catch code block you used to call availableSymbols() in SymbolListView but here, you don’t have to check for existing values. Also, startTicker(_:) doesn’t return a result. TickerView handles continuous updates, not a single return value.
Now go back to LittleJohnModel to make startTicker(_:) work.
Like availableSymbols(), startTicker(_:) creates a server endpoint in a guard-throw statement. It doesn’t return anything…
Scroll up to the declaration of tickerSymbols. startTicker will store what it fetches in tickerSymbols.
Like availableSymbols(), the next step is to call an async URLSession method. After the guard statement, add this line:
let (stream, response) = try await liveURLSession.bytes(from: url)
bytes(from:) returns an asynchronous sequence instead of a Data instance. You store this in stream.
liveURLSession is a custom session, defined here. It makes requests that never expire or time out so the app can keep receiving from the server indefinitely.
Unlike when you fetched the stock symbols list, you can’t wait for the request to complete and only then display the data. The data must keep coming in indefinitely so the app can keep updating prices. The server will send you a single long-living response, adding more and more text to it over time.
Next, in startTicker(_:), check the URLResponse status code:
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
throw "The server responded with an error."
}
This is the same status code check as in availableSymbols().
And now comes the best part. Add this for loop:
for try await line in stream.lines {
print(line)
}
stream is a sequence of bytes from the server. lines is an abstraction of this sequence — an AsyncLineSequence — that gives you lines of text, one by one. You print each line in the console, to see what it looks like. Spoiler: JSON.
So you’ll iterate over lines and JSONDecode each line.
Now, add this code in the for loop:
for try await line in stream.lines {
print(line)
🟩
let sortedSymbols = try JSONDecoder()
.decode([Stock].self, from: Data(line.utf8))
.sorted(by: { $0.name < $1.name })
tickerSymbols = sortedSymbols
🟥
}
Now you’re all set. Build and run, select a few stock symbols, then tap Live ticker.
In the console, you see that each line of text is a complete JSON array.
Update UI on main thread
It works! But there are warnings interspersed and also purple warnings next to tickerSymbols = [] and tickerSymbols = sortedSymbols.
You probably know what’s wrong. You might’ve been feeling a little uneasy all this time because setting tickerSymbols updates the UI, and you’re used to dispatching UI updates to the main queue. And yes, the purple errors come from the Main Thread Checker, which is enabled in Xcode by default.
But over in SymbolListView you called availableSymbols() — why didn’t you get this error there?
This line of code is in a SwiftUI view, updating a State property, and SwiftUI makes sure updates happen on the main thread.
Back here in LittleJohnModel, you’re updating the Published property tickerSymbols in an asynchronous context, which usually runs on a background thread.
Fortunately, Swift concurrency has a solution for this.
Wrap the tickerSymbols lines in some code:
await MainActor.run {
tickerSymbols = []
}
and
await MainActor.run {
tickerSymbols = sortedSymbols
}
You remember MainActor from episode 2: It’s a type that runs code only on the main thread.
In the next course Beyond the basics, you’ll create other actors, to make your objects thread-safe.
To help you check your updates are coming through, add a print statement:
await MainActor.run {
tickerSymbols = sortedSymbols
🟩print("Updated: \(Date())")🟥
}
- Build and run again, select a stock symbol and tap Live ticker.
Everything’s working now! There’s just one last detail to handle … Don’t stop simulator; just keep the app running
Task hierarchy
First, a word about structured concurrency: click back to TickerView.
The task(_:) modifier in TickerView calls startTicker(_:) asynchronously.
Jump to the definition of startTicker. This is the top of a task hierarchy because startTicker(_:) asynchronously awaits URLSession.bytes(from:delegate:), which returns an async line sequence that you iterate over.
At each suspension point — that is, every time you see the await keyword — the thread could change.
Go back to TickerView. Since you start the entire process inside task(_:), this async task is the parent of all those other tasks, regardless of their execution thread or suspension state.
The task(_:) view modifier takes care of canceling your asynchronous code when its view goes away. Thanks to structured concurrency, all asynchronous tasks are also canceled when the user navigates out of this screen.
Handle cancellation errors
Keep an eye on the Xcode console while you tap the Back button.
TickerView disappears and the task(_:) view modifier’s task is canceled. This cancels all child tasks, including the call to startTicker(_:). As a result, the debug logs in the console stop as well, verifying that all execution ends!
However, SwiftUI doesn’t like that your code is trying to present an alert after you dismiss the ticker view.
In TickerView, locate the lastErrorMessage block. Like SymbolListView, TickerView also shows an alert when its lastErrorMessage property changes. And when the runtime cancels the call to startTicker(_:), the ongoing URLSession that’s fetching the live updates throws a URLError.
You don’t want to set lastErrorMessage to this URLError so you need to handle this cancellation error specially.
In TickerView, in the task modifier, insert this code in the catch closure:
} catch {
🟩
if let error = error as? URLError,
error.code == .cancelled {
return
}
🟥
lastErrorMessage = error.localizedDescription
}
The URLSession API throws custom errors and has a dedicated cancellation error code. Other asynchronous APIs throw a CancellationError.
You catch the URLSession cancellation error and don’t set lastErrorMessage.
Build and run, select stocks, tap live ticker, and tap back: And now there’s no runtime warning!
Congratulations, you’ve completed the first project in this course. The next episode challenges you to handle one more error. What if the server stops while you’re in the live ticker view?