I'm trying to execute some code I'd have previously put in my app delegate, such as saving my managed object context when entering the background. I put the call in the .onChange
for the scenePhase, but I'm not getting anything. Here's a sample project:
import SwiftUI
@main
struct PhaseApp: App {
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
Text("Hello, world.")
}
.onChange(of: scenePhase) { phase in
switch phase {
case .active:
print("Active")
case .background:
print("Background")
case .inactive:
print("Inactive")
@unknown default: break
}
}
}
}
I'd expect to get a print command in the Simulator or on my test device whenever I press Home or tap the app, but nothing happens.
Use inside scene root view (usually ContentView
)
Tested with Xcode 12 / iOS 14 as worked.
struct ContentView: View {
@Environment(\.scenePhase) private var scenePhase
var body: some View {
TestView()
.onChange(of: scenePhase) { phase in
switch phase {
case .active:
print(">> your code is here on scene become active")
case .inactive:
print(">> your code is here on scene become inactive")
case .background:
print(">> your code is here on scene go background")
default:
print(">> do something else in future")
}
}
}
}
I acknowledge this question is specifically about schenePhase
changes, however, on macOS I am not able to receive any .background
notifications when a user switches to a different app. The older NotificationCenter
strategy works as I expected, on both platforms. I'll add this to the mix for anyone who is just trying to execute some code, onForeground
/ onBackground
on iOS
and macOS
.
On any view, you can attach:
.onReceive(NotificationCenter.default.publisher(for: .willResignActiveNotification)) { _ in
doBackgroundThing()
}
The events you may care about are:
willResignActiveNotification
& willEnterForegroundNotification
willResignActiveNotification
& willBecomeActiveNotification
You can find all NotificationCenter
Name
s here.
I use will*
variants for background because I assume they'll be called early in the process, and I use did*
variants for foreground, because they are called regardless of whether the app is launched for the first time, or it's coming out of background.
I use this extension so I don't have to think about the platform differences:
extension View {
#if os(iOS)
func onBackground(_ f: @escaping () -> Void) -> some View {
self.onReceive(
NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification),
perform: { _ in f() }
)
}
func onForeground(_ f: @escaping () -> Void) -> some View {
self.onReceive(
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification),
perform: { _ in f() }
)
}
#else
func onBackground(_ f: @escaping () -> Void) -> some View {
self.onReceive(
NotificationCenter.default.publisher(for: NSApplication.willResignActiveNotification),
perform: { _ in f() }
)
}
func onForeground(_ f: @escaping () -> Void) -> some View {
self.onReceive(
NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification),
perform: { _ in f() }
)
}
#endif
}
As expected, I use it as such:
AppView()
.onBackground {
print("my background")
}
.onForeground {
print("my foreground")
}
I've been testing with Xcode 12 beta 3 and iOS/iPadOS 14 beta 3 and here's what I'm finding. Note that a lot of this involves supporting multiple windows, but the "SwiftUI lifecycle" projects default to turning that on, so I suspect you have it active already. In my original case I was porting an existing SwiftUI app from a SceneDelegate
to using the new App
struct, so I had multiple window support already active.
Here's the test View
I'm using in a new testing app:
struct ContentView: View {
@Environment(\.scenePhase) private var scenePhase
var body: some View {
Text("Hello, world!").padding()
.onChange(of: scenePhase) { phase in
switch phase {
case .background:
print("PHASECHANGE: View entered background")
case .active:
print("PHASECHANGE: View entered active")
case .inactive:
print("PHASECHANGE: View entered inactive")
@unknown default:
print("PHASECHANGE: View entered unknown phase.")
}
}
}
}
(I have identical code in the App
& Scene
but they never print anything.)
The ScenePhase documentation claims that you can declare onChange
inside the App
, a Scene
or a View
. I don't see the App
or Scene
level versions ever execute, under any circumstance I can engineer, and the View
level versions don't seem to execute completely correctly.
On hardware that doesn't support multiple windows (I use a 7th generation iPod touch) the EDIT (It did matter.)
On hardware running beta 2 that doesn't support multiple windows (a 7th generation iPod Touch) I see the app go into the background, back into the foreground, and so forth. On every app launch I'll see "View entered active" print.View
level closure executes every time. (Full disclosure, this iPod Touch is still running beta 2, but I don't think it's going to matter. Once I update it to b3 I'll mention it here if it matters.)
On hardware that does support multiple windows (I use an older iPad Pro with the Lightning connector) I don't see the initial scene creation happen. (The first run does not trigger a "View entered active" message.) I do see subsequent background/foreground transitions. If I create a new scene from the iPad multi-tasking UI the second scene will trigger a "View entered active" log. Unfortunately I hadn't run this test on the iPad against beta 2, so I can't say if the behavior changed with b3 or not.
On my iPod Touch running iOS 14 beta 3 I see the same behavior as the iPad: the first launch doesn't print any phase change messages from the view, but does report subsequent background/foreground changes.
On the simulator it always behaves like the iPad hardware, even when I'm simulating an iPod Touch. I suspect this is because the simulator is running under the hood on the Mac and gets multiple window "support" this way. But I do see messages when I put the app in the background while running in the simulator, I'm just missing the initial "View entered active" message that I get from the actual hardware.
One final note: when I return an app from the foreground I first see "View entered inactive" and then I see "View entered active". When I background the app I see "View entered inactive", followed by "View entered background". I think this is expected behavior, but since other parts seem broken I wanted to mention it.
TL;DR:
I think you should be able to see most ScenePhase
changes from a View
, but you'll miss the initial app launch on iPads or in the simulator. And hopefully they will show up as expected for App
and Scene
objects in a later beta?
You can use the following extension:
public extension View {
func onScenePhaseChange(phase: ScenePhase, action: @escaping () -> ()) -> some View {
self.modifier(OnScenePhaseChangeModifier(phase: phase, action: action))
}
}
public struct OnScenePhaseChangeModifier: ViewModifier {
@Environment(\.scenePhase) private var scenePhase
public let phase: ScenePhase
public let action: () -> ()
public func body(content: Content) -> some View {
content
.onChange(of: scenePhase) { phase in
if (self.phase == phase) {
action()
}
}
}
}
Final usage:
ContentView()
.onScenePhaseChange(phase: .active) { print("scene activated!") }
.onScenePhaseChange(phase: .background) { print("scene backgrounded!") }
.onScenePhaseChange(phase: .inactive) { print("scene inactive!") }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With