How to create and use async properties
How to create and use async properties êŽë š
Updated for Xcode 15
Just as Swiftâs functions can be asynchronous, computed properties can also be asynchronous: attempting to access them must also use await
or similar, and may also need throws
if errors can be thrown when computing the property. This is what allows things like the value
property of Task
to work â itâs a simple property, but we must access it using await
because it might not have completed yet.
Important
This is only possible on read-only computed properties â attempting to provide a setter will cause a compile error.
To demonstrate this, we could create a RemoteFile
struct that stores a URL and a type that conforms to Decodable
. This struct wonât actually fetch the URL when the struct is created, but will instead dynamically fetch the contentâs of the URL every time the property is requested so that we can update our UI dynamically.
Tips
If you use URLSession.shared
to fetch your data it will automatically be cached, so weâre going to create a custom URL session that always ignores local and remote caches to make sure our remote file is always fetched.
Hereâs the code:
extension URLSession {
static let noCacheSession: URLSession = {
let config = URLSessionConfiguration.default
config.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
return URLSession(configuration: config)
}()
}
// Now our struct that will fetch and decode a URL every
// time we read its `contents` property
struct RemoteFile<T: Decodable> {
let url: URL
let type: T.Type
var contents: T {
get async throws {
let (data, _) = try await URLSession.noCacheSession.data(from: url)
return try JSONDecoder().decode(T.self, from: data)
}
}
}
So, weâre fetching the URLâs contents every time contents
is accessed, as opposed to storing the URLâs contents when a RemoteFile
instance is created. As a result, the property is marked both async
and throws
so that callers must use await
or similar when accessing it.
To try that out with some real SwiftUI code, we could write a view that fetches messages. We donât ever want stale data, so weâre going to point our RemoteFile
struct at a particular URL and tell it to expect an array of Message
objects to come back, then let it take care of fetching and decoding those while also bypassing the URLSession
ache:
// First, a URLSession instance that never uses caches
extension URLSession {
static let noCacheSession: URLSession = {
let config = URLSessionConfiguration.default
config.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
return URLSession(configuration: config)
}()
}
// Now our struct that will fetch and decode a URL every
// time we read its `contents` property
struct RemoteFile<T: Decodable> {
let url: URL
let type: T.Type
var contents: T {
get async throws {
let (data, _) = try await URLSession.noCacheSession.data(from: url)
return try JSONDecoder().decode(T.self, from: data)
}
}
}
struct Message: Decodable, Identifiable {
let id: Int
let user: String
let text: String
}
struct ContentView: View {
let source = RemoteFile(url: URL(string: "https://hws.dev/inbox.json")!, type: [Message].self)
@State private var messages = [Message]()
var body: some View {
NavigationView {
List(messages) { message in
VStack(alignment: .leading) {
Text(message.user)
.font(.headline)
Text(message.text)
}
}
.navigationTitle("Inbox")
.toolbar {
Button(action: refresh) {
Label("Refresh", systemImage: "arrow.clockwise")
}
}
.onAppear(perform: refresh)
}
}
func refresh() {
Task {
do {
// Access the property asynchronously
messages = try await source.contents
} catch {
print("Message update failed.")
}
}
}
}
That call to source.contents
is where the real action happens â itâs a property, yes, but it must also be accessed asynchronously so that it can do its work of fetching and decoding without blocking the UI.