什么是 ScenePhase

在 iOS 应用程序中,ScenePhase 可以帮助我们确定应用程序当前处于哪个状态,并帮助我们在状态转换时做出响应。它主要用于多任务处理,特别是在多窗口应用程序中。

使用 ScenePhase 值可以帮助我们判断应用程序是否处于前台、后台或暂停状态,这有助于我们管理应用程序的资源和数据,并及时响应应用程序的状态变化。

ScenePhase 有四个可能的值:

  • active:表示场景目前处于前台并正在与用户进行交互。
  • background:表示场景目前在后台运行,并且用户无法看到其内容。
  • inactive:表示场景当前处于过渡状态。例如,当用户从另一个应用程序切换回我们的应用程序时,它将被设置为此状态。
  • unknown:表示我们尚不知道场景的当前状态。通常,这只会在场景刚刚启动时发生。

下面是一个简单的示例,演示了如何在 SwiftUI 中使用 ScenePhase:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct ContentView: View {
@Environment(\.scenePhase) private var scenePhase

var body: some View {
VStack {
Text("Hello, world!")
}
.onChange(of: scenePhase) { newScenePhase in
switch newScenePhase {
case .active:
print("App is active")
case .inactive:
print("App is inactive")
case .background:
print("App is in background")
@unknown default:
print("Unknown scene phase")
}
}
}
}

在这个示例中,我们使用了 @Environment(\.scenePhase) 属性包装器,它允许我们在视图中访问当前场景的 ScenePhase 值。我们还使用了 .onChange 修饰符,以便在场景状态变化时触发回调。

离谱的 Bug

在我的主力机上面,scenePhase的值运行非常正确且及时。但在我备用机上发生了离谱的Bug,当我的应用进入后台时,scenePhase并不会及时更新为.background而是在重新返回应用时,才会变为.background并快速变为.active,导致我的代码出现严重的逻辑错误。

在我花了n个小时排查后,才发现这是 iOS 15.6 中的一个已知问题😭(浪费时间白忙活……)

为了避免在 iOS 15.6 的设备上解决这个问题,我们需要使用 NotificationCenter 监听应用程序状态变化,并在状态变化时更新我们的 UI。

具体来说,我们可以使用以下代码:

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
//新建一个 extension

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
}

然后就可以在对应的视图中使用:

1
2
3
4
5
6
7
AppView()
.onBackground {
print("my background")
}
.onForeground {
print("my foreground")
}

这个 iOS15.6 的 Bug 导致 ScenePhase 的方法废了,因为你无法假设所有用户的系统到都是最新的,所以对于判断应用程序是否处于前台、后台或暂停状态的需求,使用上述的 extension 方法是再合适不过了。