Skip to main content

How to create and use async properties

About 3 minSwiftArticle(s)bloghackingwithswift.comcrashcourseswiftxcodeappstore

How to create and use async properties 관련

Swift Concurrency by Example

Back to Home

How to create and use async properties | Swift Concurrency by Example

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.")
            }
        }
    }
}

Download this as an Xcode projectopen in new window

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.

Similar solutions…
How to call an async function using async let | Swift Concurrency by Example

How to call an async function using async let
Why can’t we call async functions using async var? | Swift Concurrency by Example

Why can’t we call async functions using async var?
What calls the first async function? | Swift Concurrency by Example

What calls the first async function?
How to create and call an async function | Swift Concurrency by Example

How to create and call an async function
How to fix the error “async call in a function that does not support concurrency” | Swift Concurrency by Example

How to fix the error “async call in a function that does not support concurrency”

이찬희 (MarkiiimarK)
Never Stop Learning.