TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

Important: Do not use an actor for your SwiftUI data models

Forums > Articles

https://www.hackingwithswift.com/quick-start/concurrency/important-do-not-use-an-actor-for-your-swiftui-data-models

In my app I have an actor that manages auth tokens. Getting a token is an async operation sincee it may need to use a refresh token to get a new valid token. At the same time I want to observe the auth state, so I use a @Published @MainActor prop for the authState inside my actor. This seems to work very good so its unclear to me why using actors as ObservableObjects is a bad idea (as long as any @Published properties are also marked with @MainActor..).

Here is my code: (feedback is much appreciated)

actor AuthClient: ObservableObject {
    enum State {
        case loading
        case signedOut
        case authenticated
    }

    static let shared = AuthClient()

    @Published @MainActor public var authState: State = .loading

    private var authToken: AuthToken?
    private var refreshTask: Task<String, Error>?

    // Keychain
    private let keychainAuthTokenRefreshToken = "refresh_auth_token"
    private let keychain = Keychain(service: "com.larsjk.Quiz-ask-token")
        .synchronizable(true)
        .accessibility(.always)

    private let client: APIClient

    private init(client: APIClient) {
        self.client = client
    }

    convenience init() {
        self.init(client: Network.shared.client)

        if let refreshToken = keychain[keychainAuthTokenRefreshToken] {
            Task {
                try await self.refreshToken(refreshToken: refreshToken)
            }
        } else {
            Task {
                await MainActor.run {
                    authState = .signedOut
                }
            }
        }
    }

    public func validToken() async throws -> String {
        if let handle = refreshTask {
            return try await handle.value
        }
        guard let authToken = authToken else {
            throw AuthError.missingToken
        }

        if authToken.isValid {
            return authToken.rawValue
        }

        guard let refreshToken = keychain[keychainAuthTokenRefreshToken] else {
            throw AuthError.missingToken
        }

        return try await self.refreshToken(refreshToken: refreshToken)
    }

    public func login(username: String, password: String) async throws {
        let response = try await client.send(Resources.login(username: username, password: password))
        handleTokenResponse(response)
    }

    public func logout() async {
        keychain[keychainAuthTokenRefreshToken] = nil
        Task {
            await MainActor.run {
                authState = .signedOut
            }
        }
    }

    private func refreshToken(refreshToken: String) async throws -> String {
        if let handle = refreshTask {
            return try await handle.value
        }

        let task = Task { () throws -> String in
            defer { refreshTask = nil }

            do {
                let response = try await client.send(Resources.refreshToken(refreshToken))
                return handleTokenResponse(response)
            } catch {
                Task {
                    await MainActor.run {
                        authState = .signedOut
                    }
                }
                throw error
            }
        }

        self.refreshTask = task

        return try await task.value
    }

    @discardableResult
    private func handleTokenResponse(_ response: Response<TokenData>) -> String {
        keychain[keychainAuthTokenRefreshToken] = response.value.refreshToken

        authToken = AuthToken(rawValue: response.value.token)

        Task {
            await MainActor.run {
                authState = .authenticated
            }
        }

        return response.value.token
    }
}

4      

Hacking with Swift is sponsored by String Catalog.

SPONSORED Get accurate app localizations in minutes using AI. Choose your languages & receive translations for 40+ markets!

Localize My App

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

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.