skip to Main Content

I have a class that adopts the ObservableObject protocol that I want to refactor. Instead of adopting the protocol the class should be marked with the @Observable macro. Now I’m encountering a problem with one of the class’s properties: @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate. When using the macro I get an error Property wrapper cannot be applied to a computed property. I’ve tried removing the property and accessing the AppDelegate in all places where it’s needed in the class using let appDelegate = UIApplication.shared.delegate as! AppDelegate but that causes a crash.

How can I access the AppDelegate from a class marked with the @Observable macro?

Edit

The AppDelegate class. I’m using the AppAuth to authenticate users. Following the examples in docs I’ve made an AppDelegate class with a property (currentAuthorizationFlow) to hold the session.

class AppDelegate: NSObject, UIApplicationDelegate {
    var currentAuthorizationFlow: OIDExternalUserAgentSession?
}

This is how the class I’m refactoring looked.

class AuthModel: ObservableObject {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    // Some @Published properties
    
    func login() async {
        // builds authentication request
        let request = OIDAuthorizationRequest(configuration: configuration,
                                              clientId: clientID,
                                              clientSecret: clientSecret,
                                              scopes: [OIDScopeOpenID, OIDScopeProfile],
                                              redirectURL: redirectURI,
                                              responseType: OIDResponseTypeCode,
                                              additionalParameters: nil)
        
        // performs authentication request
        print("Initiating authorization request with scope: (request.scope ?? "nil")")
        
        await withCheckedContinuation { continuation in
            appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, presenting: self) { authState, error in
                if let authState = authState {
                    self.setAuthState(authState)
                    print("Got authorization tokens. Access token: " + "(authState.lastTokenResponse?.accessToken ?? "nil")")
                } else {
                    print("Authorization error: (error?.localizedDescription ?? "Unknown error")")
                    self.setAuthState(nil)
                }
                continuation.resume()
            }
        }
    }
}

In the example of the library let appDelegate = UIApplication.shared.delegate as! AppDelegate is used to access the AppDelegate, to not have to repeat that line of code each time I needed to access the AppDelegate I instead opted to have the @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate property in my class. I also have @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate in the @main of my app, from the answers I understand that I shouldn’t have that line twice in my project.

I’m refactoring the AuthModel to use the @Observable macro.

@Observable
class AuthModel {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    // Removed the @Published macro from the properties

    func login() async {
        // builds authentication request
        let request = OIDAuthorizationRequest(configuration: configuration,
                                              clientId: clientID,
                                              clientSecret: clientSecret,
                                              scopes: [OIDScopeOpenID, OIDScopeProfile],
                                              redirectURL: redirectURI,
                                              responseType: OIDResponseTypeCode,
                                              additionalParameters: nil)
        
        // performs authentication request
        print("Initiating authorization request with scope: (request.scope ?? "nil")")
        
        await withCheckedContinuation { continuation in
            // I've tried to get the AppDelegate here in the same way the docs suggest, removing the AppDelegate property higher in the class
            // let appDelegate = UIApplication.shared.delegate as! AppDelegate
            appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, presenting: self) { authState, error in
                if let authState = authState {
                    self.setAuthState(authState)
                    print("Got authorization tokens. Access token: " + "(authState.lastTokenResponse?.accessToken ?? "nil")")
                } else {
                    print("Authorization error: (error?.localizedDescription ?? "Unknown error")")
                    self.setAuthState(nil)
                }
                continuation.resume()
            }
        }
    }
}

In the refactored class I get an error on the @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate property: Property wrapper cannot be applied to a computed property.
If I remove the property and try to access the AppDelegate in the login method using let appDelegate = UIApplication.shared.delegate as! AppDelegate the app crashes at runtime with: Thread 4: signal SIGABRT An abort signal terminated the process. Such crashes often happen because of an uncaught exception or unreacoverable error or calling the abort() function. There is also a warning that UIApplication.delegate must be used from the main thread only, but placing it in an Task { @MainActor } result in an error that local variable appDelegate cannot have a global actor.

2

Answers


  1. @UIApplicationDelegateAdaptor should just be used in one place, e.g. in the App struct:

    struct MyApp: App {
    
        @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    

    Then if you want to access it in other places first you should conform it to ObservableObject, e.g.

    class AppDelegate: UIResponder, ObservableObject {
    
    }
    

    Then in your View models you can do:

    struct ContentView: View {
        @EnvironmentObject var appDelegate: AppDelegate
    

    If you need the AppDelegate in an @Observable object then you could init the other object inside a lazy var inside AppDelegate and pass in weak self so it can access the delegate without a retain cycle.

    Then if you wanted to observe that in a View model then in body you could init a child View model and pass in appDelegate.someObservable which would then use let someObservable and then if you access its properties in body the fact it is @Observable should take care of monitoring for update automatically, e.g.

    @Observable
    class MyObject {
        weak var appDelegate: AppDelegate?
        
        init(appDelegate: AppDelegate? = nil) {
            self.appDelegate = appDelegate
        }
    }
    
    class AppDelegate: AppDelegateSuperclass, ObservableObject {
        lazy var myObject: MyObject = MyObject(appDelegate: self)
        
    }
    
    struct SomeView: {
        let myObject: MyObject
    
        var body: some View {
            Text(myObject.text) // body should be called when text changes
        }
    }
    
    struct ContentView: View {
        @EnvironmentObject var appDelegate: AppDelegate
    
        var body: some View {
            SomeView(myObject: appDelegate.myObject)
        }
    }
    

    As you can see, objects are a bit of a nightmare to manage, so best stick to funcs and structs whenever you can in Swift/SwiftUI.

    Login or Signup to reply.
  2. It seems to me that you have been copy pasting code from the AppAuth docs without really understanding it. It is not clear whether you actually got something working at all.

    From what I can gather, the AppAuth lib doesn’t seem to be designed with SwiftUI in mind. It is a bit old fashion and requires hooking into the AppDelegate. A good starting point is to understand how to do this in a SwiftUI app. This is well explained in the doc for @UIApplicationDelegateAdaptor.

    Using code from the AppAuth docs, here is a simple SwiftUI implementation :

    import SwiftUI
    import AppAuth
    
    @main
    struct MyApp: App {
        
        @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
        }
    }
    
    struct ContentView: View {
        
        @EnvironmentObject private var appDelegate: AppDelegate
        
        var body: some View {
            Button {
                appDelegate.login()
            } label: {
                Text("Login")
            }
        }
    }
    
    class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
        
        var currentAuthorizationFlow: OIDExternalUserAgentSession?
        var authState: OIDAuthState?
        
        func login() {
            
            let authorizationEndpoint = URL(string: "https://accounts.google.com/o/oauth2/v2/auth")!
            let tokenEndpoint = URL(string: "https://www.googleapis.com/oauth2/v4/token")!
            let configuration = OIDServiceConfiguration(authorizationEndpoint: authorizationEndpoint, tokenEndpoint: tokenEndpoint)
            
            let request = OIDAuthorizationRequest(configuration: configuration,
                                                  clientId: "<clientID>",
                                                  clientSecret: "<clientSecret>",
                                                  scopes: [OIDScopeOpenID, OIDScopeProfile],
                                                  redirectURL: URL(string: "<redirectURL>")!,
                                                  responseType: OIDResponseTypeCode,
                                                  additionalParameters: nil)
            
            print("Initiating authorization request with scope: (request.scope ?? "nil")")
            
            let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
            let vc = scene!.keyWindow!.rootViewController!
            
            currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, presenting: vc) { authState, error in
                
                if let authState = authState {
                    self.authState = authState
                    print("Got authorization tokens. Access token: " +
                          "(authState.lastTokenResponse?.accessToken ?? "nil")")
                } else {
                    print("Authorization error: (error?.localizedDescription ?? "Unknown error")")
                    self.authState = nil
                }
            }
        }
        
        func application(_ app: UIApplication,
                         open url: URL,
                         options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
            
            if let authorizationFlow = self.currentAuthorizationFlow,
               authorizationFlow.resumeExternalUserAgentFlow(with: url) {
                
                self.currentAuthorizationFlow = nil
                return true
            }
            
            return false
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search