21

I'm using SwiftUI's new app lifecycle coming in iOS 14.

However, I'm stuck at how to access my AppState (single source of truth) object in the AppDelegate. I need the AppDelegate to run code on startup and register for notifications (didFinishLaunchingWithOptions, didRegisterForRemoteNotificationsWithDeviceToken, didReceiveRemoteNotification) etc.

I am aware of @UIApplicationDelegateAdaptor but then I can not e.g. pass an object through to the AppDelegate with a constructor. I guess the other way round (creating the AppState in the AppDelegate and then accessing it in MyApp) does not work either.

@main
struct MyApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    @State var appState = AppState()
    
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(appState)
        }
    }
}
class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        // access appState here...
        return true
    }

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        // ...and access appState here
    }
}
class AppState: ObservableObject {
    // Singe source of truth...
    @Published var user: User()
}

Any help is appreciated. Maybe there is currently no way to achieve this, and I need to convert my app to use the old UIKit lifecycle?

Mister22
  • 213
  • 2
  • 6

2 Answers2

27

Use shared instance for AppState

class AppState: ObservableObject {
    static let shared = AppState()    // << here !!

    // Singe source of truth...
    @Published var user = User()
}

so you can use it everywhere

struct MyApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    @StateObject var appState = AppState.shared

    // ... other code
}

and

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        // ...and access appState here

        AppState.shared.user = ...
    }
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • That's a nice approach, what about manipulating the shared object inside the AppDelegate, does the @StateObject variable emit the updates to the views? – Mister22 Aug 26 '20 at 12:53
  • 4
    Yes, SwiftUI 2.0 StateObject is exactly for that purpose. – Asperi Aug 26 '20 at 12:59
4

Is there a reason why you need to run the code in app delegate? If you are using new app lifecycle, why not trigger your code from WindowGroup.onChange()

struct MyScene: Scene {
    @Environment(\.scenePhase) private var scenePhase
    @StateObject private var cache = DataCache()

    var body: some Scene {
        WindowGroup {
            MyRootView()
        }
        .onChange(of: scenePhase) { newScenePhase in
            if newScenePhase == .background {
                cache.empty()
            }
        }
    }
}

Apple Documentation Link

Managing scenes in SwiftUI by Majid

svena
  • 2,769
  • 20
  • 25
  • Thanks for the comment! I need to run code for `didRegisterForRemoteNotificationsWithDeviceToken` and `didReceiveRemoteNotification`, which are only available in the AppDelegate if I am not mistaken? – Mister22 Aug 26 '20 at 12:51
  • @Mister22, if that's your use case, I think Asperi's solution is a sound one. I think I personally would also take that route. – svena Aug 26 '20 at 13:06