UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

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 Essential Developer

SPONSORED 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! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

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.