iosswiftuiappauth

How can I access the AppDelegate in an @Observable class?


In my app I have an AppDelegate with a field that I need to access. When I try to access the AppDelegate from an @Observable annotated class I get an error at runtime:

Could not cast value of type 'SwiftUI.AppDelegate' to 'MyApp.AppDelegate'.

All documentation and answers I can find online seem to suggest that what I'm doing should work. What am I doing wrong?

My AppDelegate.

class CustomAppDelegate: NSObject, UIApplicationDelegate {
    var currentAuthorizationFlow: OIDExternalUserAgentSession? // The field I need to access

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        print("AppDelegate: App has finished launching") // This does get printed at startup
        return true
    }
}

The main of my app.

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

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

I've also tried using @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate in the main. The print from the AppDelegate fires then but I still get the same crash.

How I try to access the AppDelegate.

@MainActor
@Observable
class MyModel {
    func login() async {
        // The app crashes here at runtime
        let appDelegate = UIApplication.shared.delegate as! AppDelegate

        await withCheckedContinuation { continuation in
            appDelegate.currentAuthorizationFlow = OIDAuthState.authState()
        }
    }
}

Solution

  • Even with a UIApplicationDelegateAdaptor, UIApplication.shared.delegate is still going to be a delegate implementation internal to SwiftUI. UIApplicationDelegateAdaptor simply forwards delegate calls to your own delegate.

    You can access your own delegate via an @EnvironmentObject in a view, if you conform your app delegate to ObservableObject. You can have login take the delegate as a parameter, and pass it from a view.

    class CustomAppDelegate: NSObject, UIApplicationDelegate, ObservableObject { ... }
    
    struct ContentView: View {
        @EnvironmentObject var delegate: CustomAppDelegate
    
        @State private var model = MyModel()
    
        var body: some View {
            // ...
    
            await model.login(delegate: delegate)
    
            // ...
        }
    }
    
    @MainActor
    @Observable
    class MyModel {
        func login(delegate: CustomAppDelegate) async {
            await withCheckedContinuation { continuation in
                delegate.currentAuthorizationFlow = OIDAuthState.authState()
                // ...
            }
        }
    }
    

    That said, I think it's easier to just declare currentAuthorizationFlow as a static property of some top level type, or a global property. Then it can be accessed from anywhere.

    enum Auth {
        @MainActor
        static var currentAuthorizationFlow: OIDExternalUserAgentSession? = nil
    }