一般来说,只要我们的 App 不是特别的简陋,基本上不存在所有页面的状态栏都是同一个颜色的情况。在过去使用 UIKit + Storyboard 的时候,我们可以在项目设置中将状态栏颜色设置为自动,或者在每个 View Controller 中重写 preferredStatusBarStyle 属性。
但是对于完全基于 SwiftUI 的新工程而言,之前的解决方案都无法使用。在截至目前的 Xcode 版本里(11.3 beta 11C24b),状态栏颜色仍旧无法根据背景色自动变化。并且因为页面切换不再显式对应到不同的 View Controller,我们也无法通过重写每个页面的 preferredStatusBarStyle 属性来动态更改状态栏颜色。
不过事实上 SwiftUI 通过 UIHostingController 来包装用户编写的 View,而 UIHostingController 继承自 UIViewController。在 SceneDelegate 中,Xcode 自动生成了这段代码用来加载初始 View :
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHontingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
可以看到,基于 SwiftUI 的 App 的启动方式其实和基于 StoryBoard 的 App 非常相似,只不过初始的 View Controller 被替换成了 UIHostingController。这样一来,我们就可以通过创建一个自定义的 UIHostingController 子类来实现对 preferredStatusBarStyle 的重写,同时因为 SwiftUI 中的页面并不和不同的 UIHostingController 子类直接对应(是运行过程中动态创建的),我们需要在自己的 UIHostingController 子类中创建允许动态更改 preferredStatusBarStyle 返回值的方法。
首先,我们编写自定义的 MyHostingController 类:
class MyHontingController: UIHostingController<AnyView> {
var statusBarStyle: UIStatusBarStyle = .default {
didSet {
self.setNeedsStatusBarAppearanceUpdate()
}
}
@objc override var preferredStatusBarStyle: UIStatusBarStyle {
return statusBarStyle
}
}
由于 preferredStatusBarStyle
是只读属性,我们不能在程序中动态修改它的值,因此在这里我们创建一个新的属性 statusBarStyle
,并在 preferredStatusBarStyle
的 getter
中返回这个值。在我们设置的 statusBarStyle
属性发生变化后,我们希望状态栏样式能及时变化,因此我们在 statusBarStyle
的 didSet
闭包中调用 setNeedsStatusBarAppearanceUpdate()
方法来触发更新。
接下来,将 SceneDelegate 中的 UIHostingController 替换为我们刚刚编写的 MyHostingController :
window.rootViewController = MyHontingController(rootView: AnyView(contentView))
此时,就可以在自己需要的地方通过修改当前 MyHontingController 的 statusBarStyle 属性来触发状态栏样式的刷新了,例如:
.onAppear {
print("Home On Appear")
let controller = UIApplication.shared.windows[0].rootViewController as? MyHontingController
controller?.statusBarStyle = .lightContent
}
最终效果如图所示:
Tips
创建 currentWindow
对象
虽然实现了状态栏颜色的动态修改,但是我们可以通过一些优化来让它更好用,例如,创建一个全局的 UIWindow 对象,记录下在 SceneDelegate 中创建的 window,这样就不需要每次通过 UIApplication.shared.windows[0]
来获取当前 window 了:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = HomeView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = MyHontingController(rootView: AnyView(contentView))
self.window = window
currentWindow = window // 其中 currentWindow 是全局变量
window.makeKeyAndVisible()
}
}
在调用时只需要:
let controller = currentWindow?.rootViewController as? MyHontingController
controller?.statusBarStyle = .lightContent
兼容 Dark Mode
如果需要使得状态栏样式随系统 Dark Mode 状态自动变化,可以使用 .default
。但是要注意的是,这时就无法手动指定状态栏的颜色了。如果你需要随系统 Dark Mode 状态自动变化并且和 .default
确定的颜色恰好相反,可以通过对 EnvironmentObject 的判断来获取当前系统 Dark Mode 状态:
@Environment (.colorScheme) var colorScheme:ColorScheme
滑动返回
当你使用 NavigationLink 跳转到新页面的过程中,新页面只要有任何一部分出现在屏幕上,就会触发 onAppear
,而原页面只有在完全被遮挡住时才会触发 onDisappear
(换句话说就是 SwiftUI 暂时还没有 DidAppear 之类的方法)。在滑动返回时,这一特性就会成为问题:当你从屏幕左侧滑动当前页面时,只要上一级页面有任何区域露出,就会触发上级页面的 onAppear
,但是由于此时当前页面并没有消失(即没有触发 onDisappear
),所以即使你此时将当前页面划回原位,取消了返回动作,也不会再次触发当前页面的 onAppear
。
因此,如果仅仅在每个页面的 onAppear
中指定状态栏颜色,就会出现及时并未真的返回上一级页面,当前页面状态栏颜色也被改变的情况。为了解决这个问题,我们可以在页面的 onDisappear
中也手动设置状态栏颜色,这样在滑动返回被取消后,上级页面重新消失时,依然会有一次上级页面的 onDisappear
被触发,我们就可以在这里将状态栏颜色设置为正确的值。
可能遇到的问题
在 Xcode 11.2.1 以及 Xcode 11.3 Beta 11C24b 版本中,NavigationLink 在滑动返回时可能不会触发 onDisappear
。
本文作者:李星佑
未经授权严禁转载