AsyncSequence and effectful read-only properties combine to make something beautiful.
WWDC21 came packed with lots of new features for Swift, SwiftUI, Foundation, and more, but in all the WWDC videos I’ve watched only one made me do a double take – I had to rewind and rewatch it just to make sure I hadn’t misheard.
The feature sounds simple, but makes for quite astonishing code: it’s the lines
property on URL
, which downloads and returns lines of text from a URL as they are fetched. Internally this builds on a huge amount of functionality, not least effectful read-only properties and AsyncSequence, and the end result is quite remarkable in terms of its simplicity.
In its simplest form this means we can access string data directly from a URL, either local or remote. This is perfect for simpler APIs that vend data line by line, such as plain-text files or CSV – Swift even keeps the connection open as long as necessary so new data can be streamed in as needed.
SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Using this API, we could write a small app to fetch a collection of quotes from a server and display them in a list, all using hardly any code:
struct ContentView: View {
@State private var quotes = [String]()
var body: some View {
List(quotes, id: \.self, rowContent: Text.init)
.task {
do {
let url = URL(string: "https://hws.dev/quotes.txt")!
for try await quote in url.lines {
quotes.append(quote)
}
} catch {
// Stop adding quotes when an error is thrown
}
}
}
}
Perhaps you can see what made me do the double take in the first place – I was surprised to be able to access lines
directly on a URL
, rather than routing through URLSession
or similar. Even better, if multiple lines arrive at once, @State
is smart enough to batch its view reloads to avoid creating extra work.
When working with data in CSV format, you can split your data up by commas then assign them to properties in whatever data type you’re using. This is made particularly convenient thanks to AsyncSequence
providing a compactMap()
method that transforms elements as they arrive, so we can transfer a CSV line into a Swift struct using a custom initializer like this:
struct User: Identifiable {
let id: Int
let firstName: String
let lastName: String
let country: String
init?(csv: String) {
let fields = csv.components(separatedBy: ",")
guard fields.count == 4 else { return nil }
self.id = Int(fields[0]) ?? 0
self.firstName = fields[1]
self.lastName = fields[2]
self.country = fields[3]
}
}
struct ContentView: View {
@State private var users = [User]()
var body: some View {
List(users) { user in
VStack(alignment: .leading) {
Text("\(user.firstName) \(user.lastName)")
.font(.headline)
Text(user.country)
}
}
.task {
do {
let url = URL(string: "https://hws.dev/users.csv")!
let userData = url.lines.compactMap(User.init)
for try await user in userData {
users.append(user)
}
} catch {
// Stop adding users when an error is thrown
}
}
}
}
The power of compactMap()
is that it returns a new AsyncSequence
that automatically applies a transformation as the lines arrive – it won’t force us to await the data before transforming everything.
One of the most powerful features of URL.lines
is that its underlying AsyncSequence
will keep the remote connection open for as long as it takes for all the data to arrive. This means your server can deliver initial results as soon as they are available, then send more and more over time, until eventually the request is complete.
Important: When you’re using URL.lines
in this way, the system implements a 16KB buffer so that your results get batched up. This stops it from spinning over a loop for thousands of tiny lines, but also means sometimes it won’t execute your loop’s block immediately because it’s waiting for more data.
To try this out, I wrote a simple server-side script to send lines of information separated by one second each, and we can write a small SwiftUI app to see that data arriving piece by piece:
struct ContentView: View {
@State private var lines = [String]()
@State private var status = "Fetching…"
var body: some View {
VStack {
Text("Count: \(lines.count)")
Text("Status: \(status)")
}
.task {
do {
let url = URL(string: "https://hws.one/slow-fetch")!
for try await line in url.lines {
lines.append(line)
}
status = "Done!"
} catch {
status = "Error thrown."
}
}
}
}
When that runs you’ll see the line count move upwards until the loop finally finishes – and it’s all using await
so it won’t block the UI while the network fetch happens.
We’re using URL.lines
here, but really that’s just a helpful convenience wrapper around the data arriving asynchronously. If you want the data immediately, you can bypass the 16KB buffer entirely and read the URL’s resourceBytes
property – you’ll get every single byte delivered to you individually to do with as you please, a bit like a network hosepipe spraying the bytes freely until they stop coming:
for try await byte in url.resourceBytes {
let string = UnicodeScalar(byte)
print(string)
}
Of course, you can also go the other way and ask Swift to fetch all the data before handing it to you. This is best done using data(from:)
, like this:
let url = URL(string: "https://hws.one/slow-fetch")!
let (data, _) = try await URLSession.shared.data(from: url)
let string = String(decoding: data, as: UTF8.self)
lines = string.components(separatedBy: "\n")
Tip: As that waits for all the data to come back before continuing, our example app will appear to do nothing for 10 seconds.
Anyway, this was just a quick post to express my appreciation for URL.lines
– it’s a small change in the grand scheme of things, but I just love its simplicity and look forward to it being used in real apps.
SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Link copied to your pasteboard.