Not Receiving scenePhase Changes

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'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.)

  1. 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.

  2. On hardware that doesn't support multiple windows (I use a 7th generation iPod touch) the 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.) 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.

  3. 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.

  4. 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.

  5. 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!") }