NEW: My new book Pro SwiftUI is out now – level up your SwiftUI skills today! >>

How to create and use async properties

Paul Hudson    @twostraws   

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.

Hacking with Swift is sponsored by Essential Developer

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!

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Similar solutions…

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 5.0/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.