Is it possible to extract logic that depends on the SwiftUI environment outside of the views?
For example, consider the scenario where you have a Theme struct that computes a color depending on a property in the environment, something akin to the sample code below.
What I'd like to do is extract out the logic that computes a color so that it can be used in multiple places. Ideally I'd like to use #Environment in the Theme struct so that I only have to retrieve the value it in once place - the alternative is that I retrieve from the environment at the call site of my Theme computation and inject the value in. That alternative works fine, but I'd like to avoid the need to retrieve the environment value all over the place.
/// A structure to encapsulate common logic, but the logic depends on values in the environment.
struct Theme {
#Environment(\.isEnabled) var isEnabled: Bool
var color: Color {
isEnabled ? .blue : .gray
}
}
/// One of many views that require the logic above
struct MyView: View {
let theme: Theme
var body: some View {
theme.color
}
}
/// A little app to simulate the environment values changing
struct MyApp: App {
#State var disabled: Bool
var body: some Scene {
WindowGroup {
VStack {
Toggle("Disabled", isOn: $disabled)
MyView(theme: Theme())
.disabled(disabled)
}
}
}
}
The Sample code above doesn't work, ie if you toggle the switch in the app the View's color does not change. This sample code only serves to show how I'd ideally like it to work, particularly because it doesn't require me to litter #Environment throughout MyView and similar views just to retrieve the value and pass it into a shared function.
One thing that I thought could be causing the problem is that the Theme is created outside of the scope where the Environment is changing, but if I construct a Theme inside MyView the behaviour doesn't change.
My confusion here indicates that I'm missing something fundamental in my understanding of the SwiftUI Environment. I'd love to understand why that sample code doesn't work. If Theme were a View with the color logic in its body, it would be updating, so why doesn't it cause an update in it's current setup?
The approach should be different, view parts in views, model parts in models, "separate and rule"
struct Theme { // << view layer independent !!
func color(for enabled: Bool) -> Color { // << dependency injection !!
enabled ? .blue : .gray
}
}
struct MyView: View {
#Environment(\.isEnabled) var isEnabled: Bool
let theme: Theme
var body: some View {
theme.color(for: isEnabled) // << here !!
}
}
I feel like you're mixing a few things, so let me tell you how I'd structure this.
First of all, I don't think a theme should have state, it should be a repository of colors.
struct Theme {
var enabledColor: Color = .blue
var disabledColor: Color = .gray
}
Your View is where you should have your state
struct MyView: View {
#Environment(\.isEnabled) var isEnabled: Bool
var body: some View {
// ??
}
}
What I would suggest is that you create your own EnvironmentKey and inject your theme into the environment:
struct ThemeKey: EnvironmentKey {
static let defaultValue: Theme = .init()
}
extension EnvironmentValues {
var theme: Theme {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}
Now your View can use the environment to read the theme. I like to keep my views light of logic, but presentation logic in them makes sense to me.
struct MyView: View {
#Environment(\.theme) var theme
#Environment(\.isEnabled) var isEnabled: Bool
var body: some View {
isEnabled ? theme.enabledColor : theme.disabledColor
}
}
You'll need to inject your theme at some point in your app, you should add it near the top of the view hierarchy to make sure all views get access to it.
struct MyApp: App {
#State var disabled: Bool
#State var theme: Theme = .init()
var body: some Scene {
WindowGroup {
VStack {
Toggle("Disabled", isOn: $disabled)
MyView()
.disabled(disabled)
}
.environment(\.theme, theme)
}
}
}
I wrote this article about using the environment to provide values to the view hierarchy, you may find it useful.
I know how to change the background color of a SwiftUI Views list, but I cannot find the default color. I've tried using the MacOS 'Digital Color Meter', but it just doesn't pick it up right.
As you can see in this image, I've tried to set the background color of a list row (using .listRowBackground to the exact same of the surrounding list based off of the values from Digital Color Meter.
Does anyone actually know what the default background color actually is?
Short answer: it looks like it is UIColor.secondarySystemBackground
Long answer:
I have tried Digital Color Meter app the sam as you did. It shows this RGB values: 242, 242, 247.
I have created such color:
Here is my code:
import SwiftUI
extension Color {
public static let ListBGColor = Color("ListBGColor")
}
struct ContentView: View {
var body: some View {
List {
ForEach(0..<5) {_ in
Text("Hello, world!")
.padding()
}
.listRowBackground(Color.ListBGColor)
}
.listStyle(InsetGroupedListStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Result: the same color as the background (testes on Simulator and iPhone):
The default color is different for each ListStyle and platform and I don't think SwiftUI exposes these colors. You could technically Introspect the list view and get it's background color through UIKit.
If you're talking about List{ }, it is .systemGroupedBackground. Check in dark mode: .systemSecondaryBackground does something different.
I've created a custom sheet in SwiftUI with the background color White .background(Color.white)
Now I want the background color to change to black when the user turns on the dark mode on iOS.
But I can't find a dynamic color for background like Color.primary for colors of the text etc.
So is there any way to change the background color to black when dark mode turns on?
To elaborate on the two existing answers, there are a couple of approaches to making the background change based on light or dark mode (aka colorScheme) depending on what you're trying to achieve.
If you set the background color to white because that's the default background color, and you want the system to be able to update it when the user switches to dark mode, change .background(Color.white) to .background(Color(UIColor.systemBackground)) (umayanga's answer).
e.g.
// Use default background color based on light/dark mode
struct ContentView: View {
...
var body: some View {
// ... to any view
.background(Color(UIColor.systemBackground))
}
If you want to customize the color of a view based on the device being in light or dark mode, you can do this (from Asperi's answer):
// Use custom background color based on light/dark mode
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme
...
var body: some View {
// ... to any view
.background(colorScheme == .dark ? Color.black : Color.white)
}
Note that many SwiftUI views set their background color to .systemBackground by default, so if you're using a ScrollView, List, Form, etc, they'll use the default system background color and you won't need to use .background unless you want to customize it.
If you want something that works directly from Color (like you're doing with Color.primary), and functions on both iOS and macOS (UIColor won't work on macOS), you can use the following simple Color extension, which uses conditional compilation to work correctly on either OS.
You then simply access these from elsewhere in your code like any other SwiftUI Color. For example:
let backgroundColor = Color.background
No need to check colorScheme or userInterfaceStyle with this approach: The OS will switch automatically when the user moves between Light & Dark mode.
I've also included 'secondary' & 'tertiary' colors, which are a little subjective on macOS, but you can always change them to some of the other NSColor properties if you want.
Swift v5.2:
import SwiftUI
public extension Color {
#if os(macOS)
static let background = Color(NSColor.windowBackgroundColor)
static let secondaryBackground = Color(NSColor.underPageBackgroundColor)
static let tertiaryBackground = Color(NSColor.controlBackgroundColor)
#else
static let background = Color(UIColor.systemBackground)
static let secondaryBackground = Color(UIColor.secondarySystemBackground)
static let tertiaryBackground = Color(UIColor.tertiarySystemBackground)
#endif
}
Change the .background(Color.white) to .background(Color(UIColor.systemBackground))
We can also change the color automatically by adding them to the Assets folder.
Add a new color set in the Assets folder
After you add a color set, you can name it as per your convenience and you can configure your color for Any Appearance, Dark Appearance, Light Appearance.
To access your newly added color set, you need to follow the following initializer syntax of Color
Color("your_color_set_name")
For best practice you would not want your code filled with string values of your Color set name. You can create an extension to make usage more pragmatic and ordered.
extension Color {
static var tableViewBackground: Color {
Color("tableViewBackground")
}
}
Here is possible approach (for any color)
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme
...
var body: some View {
// ... to any view
.background(colorScheme == .dark ? Color.black : Color.white)
}
}
If you wish to use custom background colour for light/dark mode then I would recommend creating New Colour set in your Assets folder with custom colour values for different Appearances.
That way background colour will change automatically when display mode is switched without the need of adding a single line of code.
And then using this color of the colour list for Controller View background.
Personally I don't like to create a color set in the Assets folder.
I prefer it to be in the code so the best practices for this are as follows:
extension Color {
static var primaryColor: Color {
Color(UIColor { $0.userInterfaceStyle == .dark ? UIColor(red: 255, green: 255, blue: 255, alpha: 1) : UIColor(red: 200, green: 200, blue: 200, alpha: 1) })
}
}
Using:
.background(Color.primaryColor)
Edit #2
Here are 2 completes solutions:
Giving a control of mode status (Light or Dark) at Application Level
Giving a control of mode status at View Level
Both are updated on the fly when Mode status changes.
Both work in Preview and on true device.
Both manage a Color Theme based on Mode status (inspired from JetPack Compose).
In solution 1, OSModeTheme & OSModeThemeUpdater work together to provide the right mode status and color theme as static values, offering the possibility to define colors at the top level, eg: in default values of a view init parameters.
init(color: Color = OSModeTheme.colors.primary) { ... }
In solution 2, OSModeThemeProvider is a View Wrapper providing a local variable containing the right ColorTheme according to the Mode status.
OSModeThemeProvider { colors in
Text("Foo bar")
.foregroundColor(colors.primary)
}
// Commun part
protocol Palette {
static var primary: Color { get }
static var primaryVariant: Color { get }
static var secondary: Color { get }
static var secondaryVariant: Color { get }
static var accentColor: Color { get }
static var background: Color { get }
static var frame: Color { get }
static var error: Color { get }
}
struct LightColorPalette: Palette {
static var primary = ColorPalette.black
static var primaryVariant = ColorPalette.grayDark
static var secondary = ColorPalette.grayMid
static var secondaryVariant = ColorPalette.grayLight
static var accentColor = ColorPalette.blue
static var background = ColorPalette.white
static var frame = ColorPalette.grayDark
static var error = ColorPalette.orange
}
struct DarkColorPalette: Palette {
static var primary = ColorPalette.white
static var primaryVariant = ColorPalette.grayLight
static var secondary = ColorPalette.grayLight
static var secondaryVariant = ColorPalette.grayMid
static var accentColor = ColorPalette.blue
static var background = ColorPalette.black
static var frame = ColorPalette.grayLight
static var error = ColorPalette.orange
}
// Solution 1
class OSModeTheme {
static var colorScheme: ColorScheme = .light
static var colors: Palette.Type = LightColorPalette.self
static func update(mode: ColorScheme) {
colorScheme = mode
colors = colorScheme == .dark ? DarkColorPalette.self : LightColorPalette.self
}
}
struct OSModeThemeUpdater<Content>: View where Content: View {
#Environment(\.colorScheme) var colorScheme
let content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
OSModeTheme.update(mode: colorScheme)
return content()
}
}
struct OSModeThemeDemo: View {
var body: some View {
OSModeThemeUpdater {
ZStack {
Rectangle()
.fill(OSModeTheme.colors.background)
VStack {
Group {
Text("primary")
.foregroundColor(OSModeTheme.colors.primary)
Text("primaryVariant")
.foregroundColor(OSModeTheme.colors.primaryVariant)
Text("Secondary")
.foregroundColor(OSModeTheme.colors.secondary)
Text("secondaryVariant")
.foregroundColor(OSModeTheme.colors.secondaryVariant)
Text("accentColor")
.foregroundColor(OSModeTheme.colors.accentColor)
Text("background")
.foregroundColor(OSModeTheme.colors.background)
Text("frame")
.foregroundColor(OSModeTheme.colors.frame)
Text("error")
.foregroundColor(OSModeTheme.colors.error)
}
}
}
}
}
}
// Solution 2
struct OSModeThemeProvider<Content>: View where Content: View {
#Environment(\.colorScheme) var colorScheme
let content: (Palette.Type) -> Content
init(#ViewBuilder content: #escaping (Palette.Type) -> Content) {
self.content = content
}
var body: some View {
content(colorScheme == .dark ? DarkColorPalette.self : LightColorPalette.self)
}
}
struct OSModeThemeProviderDemo: View {
var body: some View {
OSModeThemeProvider { palette in
ZStack {
Rectangle()
.fill(palette.background)
VStack {
Text("primary")
.foregroundColor(palette.primary)
Text("primaryVariant")
.foregroundColor(palette.primaryVariant)
Text("Secondary")
.foregroundColor(palette.secondary)
Text("secondaryVariant")
.foregroundColor(palette.secondaryVariant)
Text("accentColor")
.foregroundColor(palette.accentColor)
Text("background")
.foregroundColor(palette.background)
Text("frame")
.foregroundColor(palette.frame)
Text("error")
.foregroundColor(palette.error)
}
}
}
}
}
You can extend UIColor as shown below
extension UIColor{
struct Custom {
static var black: UIColor{
if #available(iOS 13, *) {
return UIColor.init { (trait) -> UIColor in
return trait.userInterfaceStyle == .dark ? UIColor.white : UIColor.black
}
}
return UIColor.black
}
}
}
Then use as .background(Color(UIColor.Custom.black))
Your view will update the color when dark move is enabled/disabled
Check out this page for recommended system colors for various UI elements. Using these should take care of dark/light mode switching.
UI Element Colors
Here is my solution. Inspired by Luc-Oliver's solution 2.
extension ColorScheme {
typealias ColorSelector = (Color, Color) -> Color
private var colorSelector: ColorSelector { color(_:_:) }
func color(_ light: Color, _ dark: Color) -> Color {
switch self {
case .light: return light
case .dark: return dark
#unknown default: fatalError()
}
}
struct Provider<Content>: View where Content: View
{
typealias ColorSelector = ColorScheme.ColorSelector
typealias ContentGetter = (ColorSelector) -> Content
#Environment(\.colorScheme) var colorScheme
let content: ContentGetter
init(#ViewBuilder content: #escaping ContentGetter) {
self.content = content
}
var body: some View {
content(colorScheme.colorSelector)
}
}
}
Use like:
struct DemoView: View {
var body: some View {
ColorScheme.Provider { color in
HStack {
Rectangle().fill(color(.blue, .red))
Rectangle().fill(color(.green, .orange))
}
}
}
}
I built a bridge between UIColor and Color:
fileprivate extension UIColor {
static func dynamicColor(light: UIColor, dark: UIColor) -> UIColor {
UIColor {
switch $0.userInterfaceStyle {
case .dark:
return dark
default:
return light
}
}
}
}
extension Color {
static var itemTextColor: Color = Color(uiColor: .dynamicColor(
light: UIColor(red:0.278, green:0.278, blue:0.278, alpha: 1.0),
dark: UIColor(red:0.800, green:0.800, blue:0.800, alpha:1.000))
)
}
If you want to switch between black and white color, make sure you don't set the
.foregroundColor(.black)
or
.foregroundColor(.white)
because it will force the color to be black on both dark and light mode.
If you remove it, the dark mode will have white color, and light mode will have black color.
SwiftUI has colors that adapt automatically to the color scheme. For example, you can use .background(Color(.textBackgroundColor)) to get an appropriate background color for rendering text regardless of color scheme.
This question already has answers here:
SwiftUI List color background
(9 answers)
Closed 3 years ago.
I am trying on setting a view background color to black with the following code
struct RuleList: View {[![enter image description here][1]][1]
private var presenter: ConfigurationPresenter?
#ObservedObject
private var viewModel: RowListViewModel
init(presenter: ConfigurationPresenter?, viewModel: RowListViewModel) {
self.presenter = presenter
self.viewModel = viewModel
}
var body: some View {
List(viewModel.configurations) { configuration in
RuleListRow(website: configuration.website).background(Color.black)
}.background(Color.black)
}
}
struct RuleListRow: View {
var website: Website
#State private var websiteState = 0
var body: some View {
VStack {
Text(website.id).foregroundColor(.white)
Picker(website.id, selection: $websiteState) {
Text("Permis").tag(0)
Text("Ascuns").tag(1)
Text("Blocat").tag(2)
}.pickerStyle(SegmentedPickerStyle()).background(Color.crimson)
}.listRowBackground(Color.green)
}
}
The view is hosted in a mixed UIKit - SwiftUI storyboard, so this specific view is embed in a Hosting controller
class ConfigurationHostingController: UIHostingController<RuleList> {
private var presenter: ConfigurationPresenter = ConfigurationPresenter()
required init?(coder: NSCoder) {
super.init(rootView: RuleList(presenter: presenter, viewModel: presenter.rowListViewModel))
}
}
I've tried any combination of .background, .listRowBackground(Color.black) and .colorMultiply(.black) I could think of, and the best I got is this
iOS 16
Since Xcode 14 beta 3, You can change the background of all lists and scrollable contents using this modifier:
.scrollContentBackground(.hidden)
You can pass in .hidden to make it transparent. So you can see the color or image underneath.
iOS 14
In iOS 14, you may consider using LazyVStack instead of list for this:
ScrollView {
LazyVStack {
ForEach((1...100), id: \.self) {
Text("Placeholder \($0)")
}
}
.background(Color.yellow)
}
Keep in mind that LazyVStack is lazy and doesn't render all rows all the time. So they are very performant and suggested by Apple itself in WWDC 2020.
iOS 13
All SwiftUI's Lists are backed by a UITableViewin iOS. so you need to change the background color of the tableView. But since Color and UIColor values are slightly different, you can get rid of the UIColor.
struct ContentView: View {
init() {
/// These could be anywhere before the list has loaded.
UITableView.appearance().backgroundColor = .clear // tableview background
UITableViewCell.appearance().backgroundColor = .clear // cell background
}
var body: some View {
List {
,,,
}
.background(Color.yellow)
}
}
Now you can use Any background (including all Colors) you want
Note that those top and bottom white areas are safe are and you can use .edgesIgnoringSafeArea() modifier to get rid of them.
⚠️ Important note!
Apple is on its way to deprecate all UIKit tricks that we are using in the SwiftUI (like tweaking the UIAppearance). So you may want to consider adapting your code to the latest iOS always
iOS 13 introduces semantic colors: a way of specifying what a color's purpose is rather than its actual value. This allows the color to automatically adapt when dark mode is enabled.
In UIKit, these colors can be easily accessed via static members on UIColor (e.g. UIColor.label(), UIColor.secondaryLabel(), etc.). An extensive list of all the available semantic colors can be found on this documentation page.
However, SwiftUI's Color type does not have the equivalent static members. Therefore, this would be invalid:
// Error: Type 'Color?' has no member 'secondaryLabel'
var body: some View {
Text("Hello World!")
.color(.secondaryLabel)
}
How would I go about accessing these semantic colors in SwiftUI?
The Color struct in SwiftUI has a UIColor initialiser. So for example you could change the Text foregroundColor using the label UIColor.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
Text("Hello World")
.foregroundColor(Color(.label))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
ContentView()
.environment(\.colorScheme, .dark)
}
}
}
While Color does not have static members for these semantic colors, according to the documentation, Color does have an initializer which allows you to specify an AppearanceColorName.
However, I say "according to the documentation" because, as of now, this initializer appears to be unavailable in Xcode. In a subsequent beta release, when this initializer becomes available, it can be used like this:
var body: some View {
Text("Hello World!")
.color(Color(apperanceName: .secondaryLabel))
}