How to add an AppDelegate and a SceneDelegate to a SwiftUI app

Besides SwiftUI, in 2019, Apple introduced the concept of multiple windows, where each window, more appropriately called scene, represents a UI instance of our app.

Fast forward one year later, and SwiftUI has a brand new life-cycle, dropping both UIKit's app and scene delegates entirely. Here's what's left:

@main
struct FSApp: App {
  var body: some Scene {
    WindowGroup {
      ContentView()
    }
  }
}

While the SwiftUI team keeps moving forward at a neck-breaking speed, not all third party libraries and, admittedly, not even all Apple's API, are ready for the big shift:
to this day, many APIs still require either an AppDelegate or an UIScene to operate.

Does it mean that we can't use the new SwiftUI life-cycle? No! The SwiftUI team has thought about these scenarios and provided us with the right tools. Let's dig in.

Add an app delegate to a SwiftUI app

Along with the new SwiftUI life-cycle, the @UIApplicationDelegateAdaptor property wrapper has been introduced, letting us associate an app delegate to a SwiftUI app.

First, let's define our UIApplicationDelegate:

class FSAppDelegate: NSObject, UIApplicationDelegate {
  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {
    // ...
    return true
  }
}

Then we can add it to our App:

@main
struct FSApp: App {
  @UIApplicationDelegateAdaptor var delegate: FSAppDelegate 

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

SwiftUI will both initialize and manage our delegate lifetime. No further work is necessary.

App delegate access via the environment

Environment is one of the most powerful SwiftUI features, if we conform our app delegate to ObservableObject, it will be accessible anywhere in our app:

class FSAppDelegate: NSObject, UIApplicationDelegate, ObservableObject { // 👈🏻
  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {
    // ...
    return true
  }

  // ...
}

With just this change, we can access our app delegate like any other environment object:

struct ContentView: View {
  @EnvironmentObject var appDelegate: FSAppDelegate
  
  var body: some View {
    Button("Tap me") {
      // appDelegate. ...
    }
  }
}

We didn't have to inject the app delegate ourselves.

Besides receiving the app life-cycle events, we can add to our app delegate any @Published properties, and views depending on it will produce a new body whenever changes are published, for example:

class FSAppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
  @Published var date: Date = .now // 👈🏻

  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {
    // 👇🏻 Publishes every second
    Timer
      .publish(every: 1, on: .main, in: .default)
      .autoconnect()
      .assign(to: &$date)
    return true
  }
}

struct ContentView: View {
  @EnvironmentObject var appDelegate: FSAppDelegate // 👈🏻
  
  var body: some View {
    Text(appDelegate.date.formatted(date: .omitted, time: .standard))
  }
}

Add a scene delegate to a SwiftUI app

Similar to app delegate, some legacy APIs require access to the UIWindowSceneDelegate or to the UIScene in general.

We have mainly two ways to add a scene delegate to our app. Let's create the delegate:

class FSSceneDelegate: NSObject, UIWindowSceneDelegate {
  func sceneWillEnterForeground(_ scene: UIScene) {
    // ...
  }

  func sceneDidBecomeActive(_ scene: UIScene) {
    // ...
  }

  func sceneWillResignActive(_ scene: UIScene) {
    // ...
  }

  // ...
}

The first way is via our app delegate, thanks to the application(_:configurationForConnecting:options:) method, which lets us return a new UISceneConfiguration, where we can also declare the scene delegate class:

class FSAppDelegate: NSObject, UIApplicationDelegate {

  func application(
    _ application: UIApplication,
    configurationForConnecting connectingSceneSession: UISceneSession,
    options: UIScene.ConnectionOptions
  ) -> UISceneConfiguration {
    let sceneConfig = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
    sceneConfig.delegateClass = FSSceneDelegate.self // 👈🏻
    return sceneConfig
  }
}

With this change, our scene delegate will start receiving all the scene events.

The second way, which doesn't require an app delegate, is via the main app info.plist, where we can update the Application Scene Manifest:

<dict>
  <key>UISceneConfigurations</key>
  <dict>
    <key>UIWindowSceneSessionRoleApplication</key>
    <array>
      <dict>
        <key>UISceneDelegateClassName</key>
        <string>$(PRODUCT_MODULE_NAME).FSSceneDelegate</string>
      </dict>
    </array>
  </dict>
  <key>UIApplicationSupportsMultipleScenes</key>
  <true/>
</dict>

The most crucial bit is the Delegate Class Name definition, a.k.a. the value of key UISceneDelegateClassName, where we specify the scene delegate to be used.

Once this is set, our scene delegate will start receiving all the scene events.

If we implement both ways, the app delegate approach takes priority.

Scene delegate access via the environment

Regardless of which we way we use, like for app delegate, if we make our scene delegate conform to ObservableObject, it will automatically be injected in SwiftUI's environment:

class FSSceneDelegate: NSObject, UIWindowSceneDelegate, ObservableObject { // 👈🏻
  func sceneWillEnterForeground(_ scene: UIScene) {
    // ...
  }

  // ...
}

Exactly like for the app delegate, we can add dynamic properties and publish things, and all views depending on it will update accordingly:

class FSSceneDelegate: NSObject, UIWindowSceneDelegate, ObservableObject {
  @Published var date: Date = .now // 👈🏻

  func scene(
    _ scene: UIScene,
    willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions
  ) {
    // 👇🏻 Publishes every second
    Timer
      .publish(every: 1, on: .main, in: .default)
      .autoconnect()
      .assign(to: &$date)
  }
}

struct ContentView: View {
  @EnvironmentObject var sceneDelegate: FSSceneDelegate // 👈🏻
  
  var body: some View {
    Text(sceneDelegate.date.formatted(date: .omitted, time: .standard))
  }
}

This delegate will have its separate UISceneSession, meaning that we won't get access to SwiftUI's @SceneStorage storage.

To be clear, it's completely fine having a view depending on both delegates:

struct FSView: View {
  @EnvironmentObject var appDelegate: FSAppDelegate
  @EnvironmentObject var sceneDelegate: FSSceneDelegate

  var body: some View {
    ...
  }
}

Conclusions

Since SwiftUI's introduction, one of its flagship features has been compatibility with previous UI frameworks:

  • Is something not available in SwiftUI yet? A collection of Representable protocols will help you fill in the gap (UIViewRepresentable, NSViewRepresentable, WKInterfaceObjectRepresentable, ...).
  • Does something still require an app delegate or an UIScene (looking at you, SKStoreReviewController, FB9301675)? @UIApplicationDelegateAdaptor and the Scene Manifest have you covered.

As time passes, we will reach for these fallbacks less and less. Until then, we're glad they're here.

Where do you find yourself going back to UIKit/AppKit/WatchKit? Let me know via email or Twitter!

⭑⭑⭑⭑⭑

Further Reading

Explore SwiftUI

Browse all