davidgyoung
davidgyoung

Reputation: 65025

Determine background status with SwiftUI

When using SwiftUI with iOS 13+, the traditional means of determining background state no longer work. For example:

As an alternative, there are UIWindowSceneDelegate callbacks: sceneDidBecomeActive(_ scene: UIScene), sceneWillResignActive(_ scene: UIScene), sceneWillEnterForeground(_ scene: UIScene), sceneDidEnterBackground(_ scene: UIScene)

The problem with these replacements is that they are specific to one of multiple scenes that are entering and leaving the foreground. They do not provide a simple and clean way to determine if the entire app is in the foreground or background.

Determining app foreground/background status is important for reasons that have nothing to do with the user interface. Some iOS features fail silently when the app is not in the foreground (wildcard bluetooth scanning and iBeacon transmission are two examples.) I often develop iOS frameworks that have no user interface whatsoever, so I need a way to determine app background/foreground state that does not rely on pasting a bunch of boilerplate code in the UIWindowSceneDelegate -- it is not reasonable for me to ask somebody using my framework to do that.

Are there any straightforward ways to determine the apps's foreground/background status on iOS 13 with SwiftUI?

Upvotes: 4

Views: 3721

Answers (4)

Abdullah
Abdullah

Reputation: 1616

SwiftUI

While you are within any SwiftUI instances (View, App or custom Scene), Apple recommends using the 'scenePhase' Environment variable to access the current app's scene phase. The 'ScenePhase' enum currently contains following three cases. For a more comprehensive understanding, I suggest checking out the official documentation

  • Active
  • Inactive
  • Background.

[There's a minor change in the 'onChange' method behaviour. From iOS 17.*, you can now retrieve both the old and new values within the 'onChange' modifier.]

import SwiftUI

@main
struct ExampleApp: App {
    @Environment(\.scenePhase) private var scenePhase

    var body: some Scene {
        WindowGroup {
            ContentView()
                .onChange(of: scenePhase) { oldPhase, newPhase in
                    switch newPhase {
                    case .background:
                        print("ScenePhase: Background from \(oldPhase)")
                    case .inactive:
                        print("ScenePhase: Inactive from \(oldPhase)")
                    case .active:
                        print("ScenePhase: Active/Foreground from \(oldPhase)")
                    @unknown default:
                        print("ScenePhase: Unknown scene phase \(newPhase) from \(oldPhase)")
                    }
                }
        }
    }
}

Swift

However, if you find yourself within Swift instances in a SwiftUI project, you'll need to observe UIApplication notifications to capture the app's state.

NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { _ in
    // active
}

NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: .main) { _ in
    // inactive
}

NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { _ in
    // Background
}

NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { _ in
    // Foreground
}

Upvotes: 1

Hajji Daoud
Hajji Daoud

Reputation: 355

The correct way is not to use NotificationCenter at all but rather use the “scenePhase” Environment object. Can be used in any SwiftUI view that needs to see the state or the entire app.

@main

struct ExampleApp: App {
    
    @Environment(\.scenePhase) var scenePhase
    
    var body: some Scene {
        WindowGroup {
            MainView()
                .onChange(of: scenePhase) { phase in
                    switch phase {
                    case .background:
                        print("App is background")
                    case .inactive:
                        print("App is inactive")
                    case .active:
                        print("App is active")
                    @unknown default:
                        print("Phase is unknown")
                    }
                }
        }
    }
}

Upvotes: 8

Iaenhaall
Iaenhaall

Reputation: 512

I believe the correct way to handle events in SwiftUI is to use the onReceive method.

Text("Hello, World!")
    .onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
        // Action
    }

Upvotes: 5

pawello2222
pawello2222

Reputation: 54641

You can use the UIApplication notifications in SwiftUI as well:

  • didEnterBackgroundNotification
  • willEnterForegroundNotification
  • didBecomeActiveNotification
  • willResignActiveNotification

Here is an example:

NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { _ in
    // active
}

NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: .main) { _ in
    // inactive
}

Upvotes: 5

Related Questions