When building apps, we often reach a point where it's important to be aware of transitions in the app lifecycle. We may need, for instance, to start timers or long-running tasks when the app comes into the foreground, to stop them when it becomes inactive, or to persistently store data when it enters the background. Quite often we rely on UIKit's AppDelegate to determine the current state of an app. However, SwiftUI offers a much cleaner and modern solution; the scenePhase environment value.
Let's get to know it.
The scenePhase and the scene lifecycle values
Usually, most SwiftUI apps are parted by a single scene with multiple views, although they can also contain multiple scenes. Depending on where we "read" it, the scenePhase
environment value reports either the state of one, or all scenes in the app —more about that next. If the app has one scene only, then changes to its state reflect changes to the app's state too.
Regardless, scenePhase
provides three different values that describe the current lifecycle state of an app's scene:
active
: The app is currently in the foreground and interactive.inactive
: The app is still in the foreground but not interacting. For example, when the device is receiving a call, or when transitioning to another app.background
: The app is no longer visible and is running in the background. According to Apple docs, it's safe to assume that the app might be terminated soon after it enters that state. Therefore, it's extremely important to persistently store any data remaining in memory, or perform other necessary finishing tasks.
We can use scenePhase
for a wide range of tasks, with the following being some common use cases:
- Saving data when the app moves to the background.
- Pausing animations or timers while inactive.
- Resuming long-running tasks when returning to the foreground.
Using the scenePhase environment value
Let's see how we can use the scenePhase
environment value. Just like all environment values in SwiftUI, we import it to any view with the following declaration:
1 2 3 |
@Environment(\.scenePhase) private var scenePhase |
For the sake of the demonstration, let's assume the following simple view:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
struct ContentView: View { @Environment(\.scenePhase) private var scenePhase @State private var phaseDescription = "Not Defined" var body: some View { VStack { Text("Current scene phase:") .font(.headline) Text(phaseDescription) .font(.largeTitle) .padding(.top) .foregroundStyle(.indigo) } .padding() } } |
The easiest way to detect changes to scenePhase
and react to them is to use the onChange
view modifier. Then, in a switch
statement we can handle each case separately, or use if
statements to handle only cases we are interested in.
For instance, the following code updates the statusText
state property according to the new phase value and prints it to the console:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
.onChange(of: scenePhase) { _, newPhase in switch newPhase { case .active: phaseDescription = "Active" // Perform necessary tasks // when the app becomes active. // i.e. start timers, long-running tasks, etc. case .inactive: phaseDescription = "Inactive" // Perform necessary tasks // when the app becomes inactive. // i.e, stop timers, animations, etc. case .background: phaseDescription = "Background" // Perform necessary tasks // when the app enters the background. // i.e., save data, sync, etc. @unknown default: phaseDescription = "Unknown phase" } print(phaseDescription) } |
Here's the above in action:
It's important to highlight the following:
When observing scenePhase
inside a SwiftUI view, just like it's demonstrated here, then we do so only for the scene that the view belongs to. That's fine if the app has one scene only. The next part explains what to do when having multiple scenes.
For your reference, here's the entire code of the view:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
struct ContentView: View { @Environment(\.scenePhase) private var scenePhase @State private var phaseDescription = "Not Defined" var body: some View { VStack { Text("Current scene phase:") .font(.headline) Text(phaseDescription) .font(.largeTitle) .padding(.top) .foregroundStyle(.indigo) } .padding() .onChange(of: scenePhase) { _, newPhase in switch newPhase { case .active: phaseDescription = "Active" // Perform necessary tasks // when the app becomes active. // i.e. start timers, long-running tasks, etc. case .inactive: phaseDescription = "Inactive" // Perform necessary tasks // when the app becomes inactive. // i.e, stop timers, animations, etc. case .background: phaseDescription = "Background" // Perform necessary tasks // when the app enters the background. // i.e., save data, sync, etc. @unknown default: phaseDescription = "Unknown phase" } print(phaseDescription) } } } |
Using scenePhase in the App struct
We can also monitor lifecycle changes at the app level, in the main App struct of the project. By doing that, we are not watching for changes to one scene only, but to all scenes in the app.
In that case, scenePhase
has:
- the
active
value when any of the scenes is active, - the
inactive
value when all scenes are inactive, - the
background
value when all scenes have entered the background.
The following example demonstrates that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
@main struct ScenePhaseDemoApp: App { @Environment(\.scenePhase) private var scenePhase var body: some Scene { WindowGroup { ContentView() } .onChange(of: scenePhase) { _, newPhase in switch newPhase { case .active: // At least one scene is active. break case .inactive: // All scenes are inactive. break case .background: // All scenes have gone to the background. // App is on the background too and it may // be terminated at any moment. break @unknown default: break } } } } |
Conclusion
The scenePhase
environment value gives us control over our app's lifecycle in an elegant and straightforward manner. It's a pure SwiftUI solution, so there is no need to resort to UIKit's AppDelegate and get into unnecessary hassle. The few simple examples of this post demonstrate the various phases, how scenePhase
is usually treated, and where to use it depending on whether an app has one or multiple scenes. Undeniably, it's a great tool for keeping our UI and data in sync with the app’s current state. Thank you for reading!