SwiftUI:状态栏颜色
Posted
技术标签:
【中文标题】SwiftUI:状态栏颜色【英文标题】:SwiftUI: Status bar color 【发布时间】:2019-11-25 12:53:52 【问题描述】:有没有办法将 SwiftUI 视图的状态栏更改为白色?
我可能遗漏了一些简单的东西,但我似乎找不到在 SwiftUI 中将状态栏更改为白色的方法。到目前为止,我只看到.statusBar(hidden: Bool)
。
【问题讨论】:
你是指状态栏背景还是状态栏文字? 状态栏文字,切换到灯光样式 您是要为整个应用更改它还是只为一个视图更改它? 整个应用程序都可以,但知道如何只更改一个视图会很高兴 看到这个问题:***.com/questions/17678881/… 【参考方案1】:通过使用.preferredColorScheme(_ colorScheme: ColorScheme?)
设置View
的.dark
或.light
模式配色方案,可以将状态栏文本/色调/前景色设置为白色。
层次结构中使用此方法的第一个视图将优先。
例如:
var body: some View
ZStack ...
.preferredColorScheme(.dark) // white tint on status bar
var body: some View
ZStack ...
.preferredColorScheme(.light) // black tint on status bar
【讨论】:
谢谢。这是真正的 SwiftUI 方式。 我相信这会完全改变配色方案,而不仅仅是状态栏文本颜色。如果您的应用使用深色主题,那么这不是一个可行的解决方案 很好的答案,但它可能会导致更多的麻烦,因为这也会改变堆栈内列表和其他对象的配色方案。仅更改 UIStatusBar 将避免这种情况,并且在技术上减少代码。【参考方案2】:在链接到我编辑的 cmets this question here
但是要回答这个问题并帮助人们直接找到答案:
Swift 5 和 SwiftUI
为 SwiftUI 创建一个名为 HostingController.swift 的新 swift 文件
import SwiftUI
class HostingController<ContentView>: UIHostingController<ContentView> where ContentView : View
override var preferredStatusBarStyle: UIStatusBarStyle
return .lightContent
然后在 SceneDelegate.swift 中更改以下代码行
window.rootViewController = UIHostingController(rootView: ContentView())
到
window.rootViewController = HostingController(rootView: ContentView())
【讨论】:
无论如何要从 SwiftUI 更改状态栏颜色? IE。如果您显示需要不同状态栏的模式? 有了这个我想你需要记住暗模式可能需要影响这个值 如果你有环境对象,这不起作用。 @RichardWitherspoon 我已经发布了一个新的答案,其中包含环境对象的有效解决方案 新的 SwiftUI 生命周期呢?【参考方案3】:在info.plist中,你可以简单地设置
“状态栏样式”改为“轻量级内容” “查看基于控制器的状态栏外观”为否无需对代码进行任何更改...
【讨论】:
似乎对 SwiftUI 应用没有任何作用。 @boxed 确保将“UIViewControllerBasedStatusBarAppearance”也设置为“NO”,否则 SwiftUI 可能会覆盖。【参考方案4】:只需将其添加到 info.plist
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
在 ios 14、xcode 12 上测试
【讨论】:
这是一个视频,展示了如何更新 info.plist youtu.be/w-I5I8POMSI 伙计,你救了我的命【参考方案5】:现有答案涵盖了您只想更改一次状态栏颜色的情况(例如,在整个应用程序中使用浅色内容),但如果您想以编程方式执行此操作,则首选项键是实现此目的的一种方法。
可以在下面找到完整的示例,但这里是对我们将要做的事情的描述:
定义一个符合PreferenceKey
的结构,View
s 将使用它来设置他们喜欢的状态栏样式
创建UIHostingController
的子类,它可以检测偏好更改并将它们桥接到相关的 UIKit 代码
添加扩展 View
以获得几乎看起来是官方的 API
偏好键一致性
struct StatusBarStyleKey: PreferenceKey
static var defaultValue: UIStatusBarStyle = .default
static func reduce(value: inout UIStatusBarStyle, nextValue: () -> UIStatusBarStyle)
value = nextValue()
UIHostingController 子类
class HostingController: UIHostingController<AnyView>
var statusBarStyle = UIStatusBarStyle.default
//UIKit seems to observe changes on this, perhaps with KVO?
//In any case, I found changing `statusBarStyle` was sufficient
//and no other method calls were needed to force the status bar to update
override var preferredStatusBarStyle: UIStatusBarStyle
statusBarStyle
init<T: View>(wrappedView: T)
// This observer is necessary to break a dependency cycle - without it
// onPreferenceChange would need to use self but self can't be used until
// super.init is called, which can't be done until after onPreferenceChange is set up etc.
let observer = Observer()
let observedView = AnyView(wrappedView.onPreferenceChange(StatusBarStyleKey.self) style in
observer.value?.statusBarStyle = style
)
super.init(rootView: observedView)
observer.value = self
private class Observer
weak var value: HostingController?
init()
@available(*, unavailable) required init?(coder aDecoder: NSCoder)
// We aren't using storyboards, so this is unnecessary
fatalError("Unavailable")
查看扩展
extension View
func statusBar(style: UIStatusBarStyle) -> some View
preference(key: StatusBarStyleKey.self, value: style)
用法
首先,在您的 SceneDelegate
中,您需要将 UIHostingController
替换为您的子类:
//Previously: window.rootViewController = UIHostingController(rootView: rootView)
window.rootViewController = HostingController(wrappedView: rootView)
现在任何视图都可以使用您的扩展来指定他们的偏好:
VStack
Text("Something")
.statusBar(style: .lightContent)
注意事项
this answer 对另一个问题提出了使用 HostingController 子类来观察偏好键变化的解决方案 - 我之前使用过有很多缺点的 @EnvironmentObject,偏好键似乎更适合这个问题。
这是解决此问题的正确方法吗?我不知道。可能存在无法处理的边缘情况,例如,如果层次结构中的多个视图指定了首选项键,我还没有彻底测试以查看哪个视图获得优先级。在我自己的使用中,我有两个相互排斥的视图,它们指定了它们首选的状态栏样式,所以我不必处理这个问题。因此,您可能需要对其进行修改以满足您的需要(例如,可能使用一个元组来指定样式和优先级,然后让您的 HostingController
在覆盖之前检查它的先前优先级)。
【讨论】:
当您在每个下一个屏幕上都有不同样式的栏状态时,此解决方案最有价值。 你需要告诉appkit这个值已经改变了let observedView = AnyView(rootView.onPreferenceChange(StatusBarStyleKey.self) style in observer.value?.statusBarStyle = style observer.value?.setNeedsStatusBarAppearanceUpdate() )
@KlajdDeda 在我不需要的实验中,我认为 AppKit 正在使用一些 KVO 来确定已经进行了更改。也就是说,我已经发现这里描述的方法存在一些限制——特别是 SwiftUI 中的偏好从最浅到最深的视图应用,浅视图的偏好具有更高的优先级。因此,如果您的根视图更喜欢浅色内容,而更深层次的视图更喜欢深色内容,那么根视图将“获胜”。我不知道解决这个问题的好方法。
@Arkcann 谢谢你的回答帮助了我。我认为您可以在 PreferenceKey.reduce
方法中定义自定义逻辑,使其随心所欲。如果您希望更深层次的值优先,您可以将值附加到数组并仅使用第一项(或最后一项)。
这适用于仍在使用 UIKit 应用程序生命周期的应用程序。你有使用新的 SwiftUI App main 的应用解决方案吗?【参考方案6】:
仅限 SwiftUI 1 和 2!
创建一个主机控制器DarkHostingController
并在其上设置preferredStatusBarStyle
:
class DarkHostingController<ContentView> : UIHostingController<ContentView> where ContentView : View
override dynamic open var preferredStatusBarStyle: UIStatusBarStyle
.lightContent
并包含SceneDelegate
:
window.rootViewController = DarkHostingController(rootView: ContentView())
【讨论】:
这适用于将文本颜色从黑色更改为白色,但如何更改状态栏的背景颜色? @MobileMon 您使用 .edgesIgnoringSafeArea(.top) 让您的背景一直到顶部,您可以设置该颜色。你把它直接放在视图上。 什么是“Model()”?你没有定义它。 @RichardWitherspoon 模型是我的数据模型对象,您不需要它用于此解决方案。我会删除它,以防止混淆。 我喜欢使用泛型将 Controller 与特定类型分离(不像在投票最多的答案中所做的那样),但是,使用 '@objc' 和 'dynamic在这个特定的用例中打开'?【参考方案7】:此解决方案适用于使用新的 SwiftUI 生命周期的应用:
我需要动态更改状态栏文本,但无法访问 window.rootViewController
,因为 SwiftUI 生命周期中不存在 SceneDelegate
。
我终于找到了 Xavier Donnellon 的这个简单的解决方案:https://github.com/xavierdonnellon/swiftui-statusbarstyle
将StatusBarController.swift
文件复制到您的项目中,并将您的主视图包装到RootView
:
@main
struct ProjectApp: App
var body: some Scene
WindowGroup
//wrap main view in RootView
RootView
//Put the view you want your app to present here
ContentView()
//add necessary environment objects here
然后您可以使用.statusBarStyle(.darkContent)
或.statusBarStyle(.lightContent)
视图修饰符或调用例如直接UIApplication.setStatusBarStyle(.lightContent)
。
不要忘记在 Info.plist 中将“基于控制器的状态栏外观”设置为“YES”。
【讨论】:
这是唯一对我有用的 SwiftUI 生命周期应用程序。感谢分享! 警告:这会破坏 iOS15 中的onOpenURL
。
对我来说,我不需要在 Info.plist 中将“基于视图控制器的状态栏外观”设置为“YES”。
使用此解决方案运行我的应用程序时,我在控制台中收到错误 Unbalanced calls to begin/end appearance transitions for <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x14cf0e3a0>.
。有谁知道如何解决这个问题?
这适用于我的 iOS 14 设备,但在 iOS 15 中,它不会将状态栏更改应用于 fullScreenCover 内的视图。相反,它会在 fullScreenCover 内返回到亮模式或暗模式的默认状态栏样式,并且只有在 fullScreenCover 被解除后才返回使用我选择的样式。【参考方案8】:
这对我有用。将这些行添加到您的 info.plist 文件中。
您需要切换顶部设置 (View controller-based status bar appearance
) 以确定您要查找的内容。
【讨论】:
哇,这确实有效,而且代码更少,并且适用于新的 SwiftUI 生命周期。就是这样!【参考方案9】:创建一个名为HostingController
的新类:
import SwiftUI
final class HostingController<T: View>: UIHostingController<T>
override var preferredStatusBarStyle: UIStatusBarStyle
.lightContent
在您的SceneDelegate.swift
中,将所有出现的UIHostingController
替换为HostingController
。
【讨论】:
【参考方案10】:更新:看起来 Hannes Sverrisson 上面的答案是最接近的,但我们的答案略有不同。
上面写的 UIHostingController 子类的答案在 XCode 11.3.1 中不起作用。
对于子类(也处理 ContentView 环境设置),以下对我有用:
import SwiftUI
class HostingController<Content>: UIHostingController<Content> where Content : View
override var preferredStatusBarStyle: UIStatusBarStyle
return .lightContent
然后在 SceneDelegate.swift 中,更改 window.rootViewController
设置确实有效:
window.rootViewController = HostingController(rootView: contentView)
【讨论】:
【参考方案11】:@Dan Sandland 的回答对我有用,但在我的情况下,需要将界面保持在 .light
模式
ZStack
Rectangle()...
VStack(spacing: 0)
...
.colorScheme(.light)
.preferredColorScheme(.dark)
【讨论】:
【参考方案12】:如果您使用environmentObject
,您可以使用this answer 中提出的解决方案。
新建一个文件并粘贴以下代码
import SwiftUI
class HostingController: UIHostingController<AnyView>
override var preferredStatusBarStyle: UIStatusBarStyle
return .lightContent
这里的区别是我们使用AnyView
而不是ContentView
,这允许我们替换这个:
window.rootViewController = UIHostingController(rootView:contentView.environmentObject(settings))
通过这个:
window.rootViewController = HostingController(rootView: AnyView(contentView.environmentObject(settings)))
【讨论】:
性能如何?我看到有人提到使用 AnyView 会对性能产生负面影响。 我的应用并不重,所以我没有发现任何差异,但我无法确认是否有更重的应用【参考方案13】:上述解决方案适用于状态栏样式。如果要为状态栏应用背景颜色,则需要使用忽略顶部保存区域的 VStack。
GeometryReadergeometry in
VStack
Rectangle().frame(width: geometry.size.width, height: 20, alignment: .center).foregroundColor(.red)
Spacer()
Your content view goes here
.frame(width: geometry.size.width, height: geometry.size.height)
.edgesIgnoringSafeArea(.top)
您可以使用实际的状态栏高度而不是固定的 20。请参考下面的链接获取状态栏高度。 Status bar height in Swift
【讨论】:
【参考方案14】:我正在使用类似的东西
extension UIApplication
enum ColorMode
case dark, light
class func setStatusBarTextColor(_ mode: ColorMode)
if #available(iOS 13.0, *)
var style: UIUserInterfaceStyle
switch mode
case .dark:
style = .dark
default:
style = .light
if let window = Self.activeSceneDelegate?.window as? UIWindow
window.overrideUserInterfaceStyle = style
window.setNeedsDisplay()
class var activeSceneDelegate: UIWindowSceneDelegate?
(Self.activeScene)?.delegate as? UIWindowSceneDelegate
【讨论】:
错误:类型“Self”没有成员“activeScene”。也请与我们分享您的 UIApplication 扩展。谢谢。【参考方案15】:Arkcann's answer 很棒,但不幸的是对我不起作用,因为 StatusBarStyleKey.defaultValue
占据了优先地位(我想知道他是如何做到的)。我将其设为Optional
并仅在显式设置时才覆盖先前设置的值。 (我是在 iOS 14.3 的真机上测试)
struct StatusBarStyleKey: PreferenceKey
static func reduce(value: inout UIStatusBarStyle?, nextValue: () -> UIStatusBarStyle?)
guard let v = nextValue() else
return
value = v
extension View
func statusBar(style: UIStatusBarStyle?) -> some View
return preference(key: StatusBarStyleKey.self, value: style)
我在创建HostingController
时也采取了一些不同的方法,我将状态栏样式存储在全局范围内。
private var appStatusBarStyle: UIStatusBarStyle?
private class HostingController<ContentView: View>: UIHostingController<ContentView>
override var preferredStatusBarStyle: UIStatusBarStyle
return appStatusBarStyle ?? .default
func createHostingController<T: View>(rootView :T) -> UIViewController
let view = rootView.onPreferenceChange(StatusBarStyleKey.self)
appStatusBarStyle = $0
return HostingController(rootView: view)
用法:
window.rootViewController = createHostingController(rootView: MyApp())
【讨论】:
【参考方案16】:-
为通知创建枚举(或任何您喜欢的用户):
enum NotificationCenterEnum: String
case changeStatusToDark
case changeStatusToLight
var notification: Notification.Name
return Notification.Name(self.rawValue)
-
创建自定义 HostingController
class HostingController<Content: View>: UIHostingController<Content>
override init(rootView: Content)
super.init(rootView: rootView)
NotificationCenter.default.addObserver(forName: NotificationCenterEnum.changeStatusToDark.notification, object: nil, queue: .main) _ in self.statusBarEnterDarkBackground()
NotificationCenter.default.addObserver(forName: NotificationCenterEnum.changeStatusToLight.notification, object: nil, queue: .main) _ in self.statusBarEnterLightBackground()
@objc required dynamic init?(coder aDecoder: NSCoder)
fatalError("init(coder:) has not been implemented")
var isDarkContent = true
func statusBarEnterLightBackground()
isDarkContent = false
UIView.animate(withDuration: 0.3) [weak self] in
self?.setNeedsStatusBarAppearanceUpdate()
func statusBarEnterDarkBackground()
isDarkContent = true
UIView.animate(withDuration: 0.3) [weak self] in
self?.setNeedsStatusBarAppearanceUpdate()
override var preferredStatusBarStyle: UIStatusBarStyle
if isDarkContent
return .lightContent
else
return .darkContent
在场景代理中
window.rootViewController = HostingController(rootView: ContentView())
鉴于您有选择:
A.如果您只需要一个视图,请使用 .onAppear/.onDisappear。
.onAppear NotificationCenter.default.post(name: NotificationCenterEnum.changeStatusToLight.notification, object: nil)
.onDisappear NotificationCenter.default.post(name: NotificationCenterEnum.changeStatusToDark.notification, object: nil)
B.如果您需要多个视图一个接一个:像在 A 中一样使用 .onAppear,但在 backAction 上触发更改:
private func backAction()
NotificationCenter.default.post(name: NotificationCenterEnum.changeStatusToDark.notification, object: nil)
presentation.wrappedValue.dismiss()
C.你可以像这样创建修饰符:
struct StatusBarModifier: ViewModifier
func body(content: Content) -> some View
content
.onAppear NotificationCenter.default.post(name: NotificationCenterEnum.changeStatusToLight.notification, object: nil)
.onDisappear NotificationCenter.default.post(name: NotificationCenterEnum.changeStatusToDark.notification, object: nil)
并使用它:
.modifier(StatusBarModifier())
【讨论】:
【参考方案17】:静态(仅适用于使用旧的 UIWindowSceneDelegate
生命周期的项目)和动态替换键窗口的 UIHostingController
都会产生不良副作用(例如 onOpenURL
中断)。
这是一种不同的方法,它涉及将preferredStatusBarStyle
转换为指向计算变量。
extension UIViewController
fileprivate enum Holder
static var statusBarStyleStack: [UIStatusBarStyle] = .init()
fileprivate func interpose() -> Bool
let sel1: Selector = #selector(
getter: preferredStatusBarStyle
)
let sel2: Selector = #selector(
getter: preferredStatusBarStyleModified
)
let original = class_getInstanceMethod(Self.self, sel1)
let new = class_getInstanceMethod(Self.self, sel2)
if let original = original, let new = new
method_exchangeImplementations(original, new)
return true
return false
@objc dynamic var preferredStatusBarStyleModified: UIStatusBarStyle
Holder.statusBarStyleStack.last ?? .default
通过一些额外的脚手架,这可用于实现 .statusBarStyle
视图修饰符。
enum Interposed
case pending
case successful
case failed
struct InterposedKey: EnvironmentKey
static let defaultValue: Interposed = .pending
extension EnvironmentValues
fileprivate(set) var interposed: Interposed
get self[InterposedKey.self]
set self[InterposedKey.self] = newValue
/// `UIApplication.keyWindow` is deprecated
extension UIApplication
var keyWindow: UIWindow?
connectedScenes
.compactMap $0 as? UIWindowScene
.flatMap(\.windows)
.first
$0.isKeyWindow
extension UIViewController
fileprivate enum Holder
static var statusBarStyleStack: [UIStatusBarStyle] = .init()
fileprivate func interpose() -> Bool
let sel1: Selector = #selector(
getter: preferredStatusBarStyle
)
let sel2: Selector = #selector(
getter: preferredStatusBarStyleModified
)
let original = class_getInstanceMethod(Self.self, sel1)
let new = class_getInstanceMethod(Self.self, sel2)
if let original = original, let new = new
method_exchangeImplementations(original, new)
return true
return false
@objc dynamic var preferredStatusBarStyleModified: UIStatusBarStyle
Holder.statusBarStyleStack.last ?? .default
struct StatusBarStyle: ViewModifier
@Environment(\.interposed) private var interposed
let statusBarStyle: UIStatusBarStyle
let animationDuration: TimeInterval
private func setStatusBarStyle(_ statusBarStyle: UIStatusBarStyle)
UIViewController.Holder.statusBarStyleStack.append(statusBarStyle)
UIView.animate(withDuration: animationDuration)
UIApplication.shared.keyWindow?.rootViewController?.setNeedsStatusBarAppearanceUpdate()
func body(content: Content) -> some View
content
.onAppear
setStatusBarStyle(statusBarStyle)
.onChange(of: statusBarStyle)
setStatusBarStyle($0)
UIViewController.Holder.statusBarStyleStack.removeFirst(1)
.onDisappear
UIViewController.Holder.statusBarStyleStack.removeFirst(1)
UIView.animate(withDuration: animationDuration)
UIApplication.shared.keyWindow?.rootViewController?.setNeedsStatusBarAppearanceUpdate()
// Interposing might still be pending on initial render
.onChange(of: interposed) _ in
UIView.animate(withDuration: animationDuration)
UIApplication.shared.keyWindow?.rootViewController?.setNeedsStatusBarAppearanceUpdate()
extension View
func statusBarStyle(
_ statusBarStyle: UIStatusBarStyle,
animationDuration: TimeInterval = 0.3
) -> some View
modifier(StatusBarStyle(statusBarStyle: statusBarStyle, animationDuration: animationDuration))
@main
struct YourApp: App
@Environment(\.scenePhase) private var scenePhase
/// Ensures that interposing only occurs once
private var interposeLock = NSLock()
@State private var interposed: Interposed = .pending
var body: some Scene
WindowGroup
VStack
Text("Hello, world!")
.padding()
.statusBarStyle(.lightContent)
.environment(\.interposed, interposed)
.onChange(of: scenePhase) phase in
/// `keyWindow` isn't set before first `scenePhase` transition
if case .active = phase
interposeLock.lock()
if case .pending = interposed,
case true = UIApplication.shared.keyWindow?.rootViewController?.interpose()
interposed = .successful
else
interposed = .failed
interposeLock.unlock()
一些额外的context。
【讨论】:
【参考方案18】:创建一个名为 HostingController.swift 的新 swift 文件,或者在现有的 swift 文件中添加这个类
class HostingController: UIHostingController<ContentView>
override var preferredStatusBarStyle: UIStatusBarStyle
return .darkContent //or .lightContent
然后更改 SceneDelegate.swift 中的代码行
window.rootViewController = UIHostingController(rootView: contentView)
到
window.rootViewController = HostingController(rootView: contentView)
【讨论】:
这与一年前的答案有何不同?【参考方案19】:在所有提议的解决方案中,侵入性较小、最直接、实际上唯一对我们有用的是 Michał Ziobro 提出的解决方案: https://***.com/a/60188583/944839
在我们的应用程序中,我们需要将屏幕显示为带有深色状态栏的sheet
。这两种简单的解决方案(比如设置preferredColorScheme
)都没有为我们工作。但是,手动强制应用程序配色方案在显示为工作表的屏幕的onAppear
中并在onDisappear
中恢复它就可以了。
这是完整的扩展代码:
import SwiftUI
import UIKit
extension ColorScheme
var interfaceStyle: UIUserInterfaceStyle
switch self
case .dark: return .dark
case .light: return .light
@unknown default: return .light
extension SceneDelegate
static var current: Self?
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
return windowScene?.delegate as? Self
extension UIApplication
static func setColorScheme(_ colorScheme: ColorScheme)
if let window = SceneDelegate.current?.window
window.overrideUserInterfaceStyle = colorScheme.interfaceStyle
window.setNeedsDisplay()
P.S.为了让屏幕本身仍然使用light
配色方案,我们将colorScheme(.light)
修饰符应用于body
的内容。
【讨论】:
以上是关于SwiftUI:状态栏颜色的主要内容,如果未能解决你的问题,请参考以下文章