scenePhase stays active when cover watch to close/hide app

I want to return to the main screen of my SwiftUI watchOS app whenever the app goes from "not shown" to "shown", but I am having difficulty accomplishing this for the case when I cover my watch to close/hide my app.

Background: I use sheets to show the views because I'm not using a list/detail pattern, and send the binding for each sheet to the sheet's view, and then the sheet's view has code like:

.onChange(of: scenePhase) { phase in
    if (phase != .active) {  isShowing = false  }
}

At some point, I'll need to deal with what happens with always-on mode, but that's not my concern right now -- first I'm having trouble with what happens if the app is showing a non-root view and I cover the watch with my hand to go back to the watch face, then tap the complication to return to the app. It returns to that non-root view.

I put in some diagnostic prints on a switch(phase) in the .onChange() and it looks like the app isn't notified about this type of event, and when I tap my complication/widget to re-open the app, it also doesn't trigger a change of scenePhase (my last print from before I covered the watch just says "PHASE ACTIVE", and there's not a new print to the console when I restart the app afterwards).

If I cover the app with my hand to close it, I'd really like to be able to return to the root view when I "restart" the app.

Thanks in advance for any insight!

Answered by 42KM in 742260022

I have found a solution.

Apparently using @Environment(\.scenePhase) var scenePhase in the modal sheet's view does not work by itself. The scenePhase inside the sheet's view just seems to stay equal to .active even if the app is closed (by, for example, covering the watch with your hand).

However, I found a solution at https://stackoverflow.com/questions/72620039/trigger-an-action-when-scenephase-changes-in-a-sheet

If I "send" the scenePhase value that the root view pulls from the environment explicitly to the sheet view or one of its ancestors using the modifier .environment(\.scenePhase, scenePhase), then the @Environment(\.scenePhase) var scenePhase in the sheet view gets the actual current phase and can monitor its changes. It also works to send it as a parameter to the sheet's view. However, since I want to access it in multiple sheets opened from yet another sheet, putting it back into the environment for the full child view hierarchy seemed more appropriate.

Ah - I see my root view indeed gets a scenePhase of inactive and then background when I cover the watch when the root view is showing. Checking scenePhase changes in the non-root views appears to not do anything at all (my onChange(of: scenePhase) code in non-root views seems to be never executed).

So I'm still not sure (yet) how to detect changes in scene phase while a sheet is being shown. So a slightly different problem than I thought I had. Time for more research...

Accepted Answer

I have found a solution.

Apparently using @Environment(\.scenePhase) var scenePhase in the modal sheet's view does not work by itself. The scenePhase inside the sheet's view just seems to stay equal to .active even if the app is closed (by, for example, covering the watch with your hand).

However, I found a solution at https://stackoverflow.com/questions/72620039/trigger-an-action-when-scenephase-changes-in-a-sheet

If I "send" the scenePhase value that the root view pulls from the environment explicitly to the sheet view or one of its ancestors using the modifier .environment(\.scenePhase, scenePhase), then the @Environment(\.scenePhase) var scenePhase in the sheet view gets the actual current phase and can monitor its changes. It also works to send it as a parameter to the sheet's view. However, since I want to access it in multiple sheets opened from yet another sheet, putting it back into the environment for the full child view hierarchy seemed more appropriate.

Upon further review, reintroducing the value into the environment seems to work for some parts of my view hierarchy but not others. However, passing by a parameter works for all my views (thus far).

There's some pretty weird stuff I see when I try to figure out what's happening with the environment. I assume the problem is that I don't fully understand how the Environment works and maybe side-effects that certain code has on Environment values...

As an example of some of the weirdness I see... here's a simplified version of my code (target is Apple Watch):

import SwiftUI

struct ContentView: View {
    @Environment(\.scenePhase) var scenePhase
    var body: some View {
            Child(paramScenePhase: scenePhase)
    }
}

struct Child: View {
var paramScenePhase: ScenePhase
    var body: some View {
        ChildButton(paramScenePhase: paramScenePhase)
    }
}

struct ChildButton: View {
    var paramScenePhase: ScenePhase
    @State private var isPresentingProblemView: Bool = false
    var body: some View {
        Button("Press me") {
            isPresentingProblemView = true
        }
        .sheet(isPresented: $isPresentingProblemView) {
            ProblemView(paramScenePhase: paramScenePhase)
        }
    }
}

struct ProblemView: View {
    @Environment(\.scenePhase) var enviroScenePhase
    var paramScenePhase: ScenePhase
    var body: some View {
        Text("Hi")
            .onChange(of: enviroScenePhase) { phase in
                print("enviroScenePhase " + scenePhaseString(phase))
            }
            .onChange(of: paramScenePhase) { phase in
                print("paramScenePhase " + scenePhaseString(phase))
            }
      }
}

func scenePhaseString(_ phase: ScenePhase) -> String {
    switch(phase) {
    case .active:
        return "phase: ACTIVE"
    case .inactive:
        return "phase: INACTIVE"
    case .background:
        return "phase: BACKGROUND"
    default:
        return "phase: UNKNOWN"
    }
}

When I run this simplified code in the simulator, press the "Press me" button, and then press the "always on" button in the watch simulator twice (to go inactive and then active again), I get the following in the console (and this is the expected behavior):

enviroScenePhase phase: INACTIVE
paramScenePhase phase: INACTIVE
enviroScenePhase phase: ACTIVE
paramScenePhase phase: ACTIVE

I then commented out var paramScenePhase: ScenePhase within ProblemView (and then comment out only the other parts of the code to get rid of compilation errors). Running the same experiment, this is what I get in the console:

enviroScenePhase phase: INACTIVE

This is not the expected behavior. And that's all I see no matter how many times I press the always-on button to toggle between inactive and active phases.

If I then comment out var paramScenePhase: ScenePhase in the ChildButton view (and change the ChildButton instantiation to ChildButton()), then nothing prints to the console at all no matter how many times I press the always-on simulator button.

I just don't understand how changing what parameters are used in a view also changes the behavior of an environment variable.

scenePhase stays active when cover watch to close/hide app
 
 
Q