< What’s the performance cost of calling an async function? | How to call an async function using async let > |
Updated for Xcode 14.2
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.
Tip: 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:
// 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)
}
}
}
So, we’re fetching the URL’s contents every time contents
is access, 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
cache:
// 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.")
}
}
}
}
Download this as an Xcode project
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.
SPONSORED From March 20th to 26th, you can join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer!
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.