SwiftUI:状态栏颜色

88
有没有办法将SwiftUI视图的状态栏更改为白色?
我可能忽略了一些简单的内容,但我似乎找不到在SwiftUI中将状态栏更改为白色的方法。目前我只看到 .statusBar(hidden: Bool)

1
你是指状态栏的背景还是状态栏的文本? - graycampbell
1
状态栏文本,切换到浅色风格 - keegan3d
2
整个应用程序都可以,但知道如何仅更改一个视图会很好。 - keegan3d
对于全局更改,我可以使用 UIApplication.shared.statusBarStyle = .lightContent,但在 Xcode 中会收到警告,因为这在 iOS 13 中已被弃用。但是当我尝试在窗口场景上设置此属性时:windowScene.statusBarManager?.statusBarStyle = .light,我会收到一个错误:Cannot assign to property: 'statusBarStyle' is a get-only property - keegan3d
3
请参阅此问题:https://dev59.com/OmMm5IYBdhLWcg3wX98W#17768797该问题讨论了如何在iOS中更改状态栏文本颜色的方法。通过设置UIViewControllerBasedStatusBarAppearanceYES,可以使用preferredStatusBarStyle方法来更改状态栏的文本颜色。但是,这种方法只适用于单个视图控制器,并且在导航控制器等其他控制器中无效。要解决这个问题,您需要在每个视图控制器中实现preferredStatusBarStyle方法,或者您可以创建一个基类视图控制器并在其中实现该方法,然后使所有其他视图控制器继承自该基类。 - Vlad Lego
显示剩余3条评论
22个回答

70

通过使用 .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
}

3
谢谢。这就是实际的SwiftUI方法。 - Fahim Rahman
63
我相信这会完全改变色彩方案,不仅仅是状态栏文本颜色。如果您的应用程序使用深色主题,那么这并不是一个可行的解决方案。 - Emil
5
好的回答,但这可能会导致更多麻烦,因为这也会改变堆栈内部的列表和其他对象的颜色方案。仅更改UIStatusBar将避免这种情况,并且从技术上来说代码量较少。 - DaWiseguy

40

如同链接评论中所述,我编辑了这个问题

但是为了回答这个问题并帮助人们直接找到答案:

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())

to

window.rootViewController = HostingController(rootView: ContentView())

10
有没有办法在SwiftUI中更改状态栏颜色?比如,如果你显示需要不同状态栏的模态视图? - keegan3d
有了这个,我想你需要记住暗模式可能会影响此值。 - CMash
7
如果你有环境对象,那么这并不起作用。 - Richard Witherspoon
1
@RichardWitherspoon 我已发布了一个新答案,其中包含环境对象的可行解决方案。 - GRosay
10
新的SwiftUI生命周期如何? - Alex Giatrakis
显示剩余4条评论

30
在info.plist中,您可以简单地设置:
  • "状态栏样式"为"Light Content"
  • "基于视图控制器的状态栏外观"为NO
无需在代码中进行任何更改...

6
似乎在SwiftUI应用程序中没有任何作用。 - boxed
1
@boxed 确保将 "UIViewControllerBasedStatusBarAppearance" 也设置为 "NO",否则 SwiftUI 可能会覆盖它。 - Matt Gallagher
当您需要根据一个UIViewController选择状态栏样式时,这并不是一个解决方案。 - adnako
@adnako,这是回答原问题的。如果你想要实现不同的目标,答案自然也会不同 :) - Muvimotv
@Muvimotv,你能告诉我原始问题中与“一次性更改整个应用程序的状态栏颜色”有关的内容吗? - adnako
显示剩余2条评论

29

只需将此添加到 info.plist 中即可。

<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>

在 iOS 14、Xcode 12 上进行了测试。


这里有一个视频展示如何更新info.plist文件。https://youtu.be/w-I5I8POMSI - Richard H
如果您使用 xcodegen 生成 info.plist 文件,则可以添加以下行:properties: UIStatusBarStyle: UIStatusBarStyleLightContent UIViewControllerBasedStatusBarAppearance: false - Reza Dehnavi
当你必须根据一个视图控制器来选择状态栏样式时,这并不是一个解决方案。 - adnako
这只是我以不同方式呈现的重复内容 :P - Muvimotv

25

这个解决方案适用于使用新的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中将“View controller-based status bar appearance”设置为“YES”。


6
这是我在一个SwiftUI生命周期应用程序中使用的唯一有效方法。感谢分享! - Ruben Martinez Jr.
4
注意:这会导致iOS15中的“onOpenURL”功能失效。 - Nicolas
对我来说,在Info.plist中不需要将“View controller-based status bar appearance”设置为“YES”。 - wristbands
2
当我使用这个解决方案运行我的应用程序时,控制台会显示错误Unbalanced calls to begin/end appearance transitions for <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x14cf0e3a0>.。有人知道如何解决吗? - wristbands
在iOS 16中可以工作,但会出现警告 --> 'windows' 在iOS 15.0中已被弃用。 - mystride
显示剩余6条评论

25
现有的回答涵盖了只需更改状态栏颜色一次的情况(例如,在整个应用程序中使用浅色内容),但如果您想以编程方式执行此操作,那么首选项键是实现这一目标的一种方式。
完整示例可以在下面找到,但是这里是我们要做的事情的描述:
  • 定义符合PreferenceKey的结构体,这将由View用于设置其首选状态栏样式
  • 创建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")
  }
}

View Extension

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)

注意事项

这个答案中,使用HostingController子类来观察首选项键的更改是一个解决方案。我之前使用了@EnvironmentObject,但它有很多缺点,而首选项键似乎更适合这个问题。

这是解决这个问题的正确方法吗?我不确定。可能会有一些边缘情况无法处理,例如,如果层次结构中有多个视图指定了首选项键,那么我没有进行全面测试以查看哪个视图具有优先级。在我的使用中,我有两个互斥的视图指定其首选状态栏样式,因此我没有遇到过这种情况。因此,您可能需要修改此内容以适应您的需求(例如,可以使用元组来指定样式和优先级,然后让您的HostingController在覆盖之前检查其先前的优先级)。


当您在每个下一个屏幕上具有不同样式的状态栏时,此解决方案最有价值。 - A.Kant
@Arkcann 感谢您的回答,它帮了我很大的忙。我认为你可以在 PreferenceKey.reduce 方法中定义自定义逻辑,以使它变得如你所愿。如果您想要更深入的值优先,则可以将值附加到数组并仅使用第一个(或最后一个)项目。 - Orkhan Alikhanov
5
这适用于仍在使用UIKit应用程序生命周期的应用程序。您有解决方案适用于使用新的SwiftUI App主入口的应用程序吗? - Edward
@Edward,不幸的是我没有 - 我希望其他人能够提供这样的解决方案,或者苹果最终能够解决。就目前而言,这阻止了我采用新的SwiftUI生命周期。 - Arkcann
你可以创建一个 SceneDelegateAppDelegate,并将 AppDelegate 放入你的 App 中,并将 SceneDelegate 连接到 AppDelegate。 这对其他问题也起作用。我需要在单个视图中动态切换状态栏文本颜色,因此我将尝试这个方法。 - David Reich
显示剩余3条评论

16

仅限于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())

2
这个可以用来将文本颜色从黑色改为白色,但我如何改变状态栏的背景颜色呢? - MobileMon
4
你可以使用.edgesIgnoringSafeArea(.top)来让你的背景延伸到屏幕顶部,并且你可以设置它的颜色。直接在View上添加即可。 - Sverrisson
"Model()"是什么?你没有定义它。 - Richard Witherspoon
@RichardWitherspoon Model是我的数据模型对象,但在这个解决方案中不需要它。为了避免混淆,我将删除它。 - Sverrisson
我喜欢使用泛型来将控制器与特定类型分离(不像最受欢迎的答案中所做的那样),但是,在这种特定用例中使用“@objc”和“dynamic open”的意义是什么? - Repose

14

@Dan Sandland的答案对我有用,但在我的情况下需要保持界面处于.light模式。

ZStack {
    Rectangle()...
    
    VStack(spacing: 0) {
        ...
    }.colorScheme(.light)
}
.preferredColorScheme(.dark)

1
这是最简单和最优雅的解决方案。谢谢。 - Sergio Viudes

9

这是我亲自尝试过的方法。将以下代码添加到您的info.plist文件中。 您需要切换顶部设置(View controller-based status bar appearance),以确定您要查找的内容。

在此输入图片描述


1
哇,这真的起作用了,而且代码更少,并且可以与新的SwiftUI生命周期一起使用。这就是正确的方式! - Burgler-dev
当你必须根据一个UIViewController来选择状态栏样式时,这并不是一个解决方案。 - adnako

6
更新:看起来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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接