Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Not Receiving scenePhase Changes

Tags:

swiftui

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.

like image 520
davextreme Avatar asked Jul 10 '20 18:07

davextreme


4 Answers

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")
                }
            }
    }
}
like image 127
Asperi Avatar answered Oct 21 '22 15:10

Asperi


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:

  • iOS: willResignActiveNotification & willEnterForegroundNotification
  • macOS: willResignActiveNotification & willBecomeActiveNotification

You can find all NotificationCenter Names 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")
   }
like image 10
Parth Mehrotra Avatar answered Oct 21 '22 16:10

Parth Mehrotra


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?

like image 6
Timothy Sanders Avatar answered Oct 21 '22 14:10

Timothy Sanders


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!") }
like image 2
Andrew Avatar answered Oct 21 '22 16:10

Andrew