In a View I can access the environment variables with the #Environment property wrapper like this:
struct MyView: View {
#Environment(\.colorScheme) var colorScheme: Color
var body: some View {
Text("Hello")
.foregroundColor(self.colorScheme == .light ? .black : .white)
}
}
In an extension, this is not possible, because I cannot put variables to an extension. How can I access the #Environment values in an extension, for example if I want to extend Color?
Until now I found out, that I can use Environment with the wrappedValue property like this:
extension Color {
static var darkModeText: Color {
if Environment(\.colorScheme).wrappedValue == .light {
return .black
} else {
return .white
}
}
}
The problem is, that this doesn't gets updated and always contains .light. I also tried using #Environment as a global variable, but this is (not yet) supported by property wrappers.
Related
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.
My apps adapt layout based on horizontal size class and ContentSizeCategory. So, I typically have code like this:
#Environment(\.horizontalSizeClass) var horizontalSizeClass
#Environment(\.sizeCategory) var sizeCategory: ContentSizeCategory
private var isHorCompactLayout: Bool {
horizontalSizeClass == .compact || sizeCategory.isAccessibilityCategory
}
which I use like this:
var body: some Scene {
if isHorCompactLayout {
Text("CompactLayout()")
} else {
Text("NormalLayout()")
}
I'd like to refactor the first chunk of code to avoid repeating it in all views where I adapt the layout. How can this be done?
I suppose I could create a new view, pass it the two views and render the correct one based on the result of isHorCompactLayout. But it would still be good to get the value of isHorCompactLayout when needed; for example, to adjust padding.
A possible approach is to have own environment value and set at the very root level of your content view, so all other subviews will have it update on changes at root level, like
struct ContentView: View {
#Environment(\.horizontalSizeClass) var horizontalSizeClass
#Environment(\.sizeCategory) var sizeCategory
var body: some View {
VStack { // some top-most container or root view
// ... all app hierarchy
}
.environment(\.isHorCompactLayout, horizontalSizeClass == .compact || sizeCategory.isAccessibilityCategory)
}
}
struct HorCompactLayoutKey: EnvironmentKey {
static let defaultValue: Bool = false
}
extension EnvironmentValues {
var isHorCompactLayout: Bool {
get { self[HorCompactLayoutKey.self] }
set { self[HorCompactLayoutKey.self] = newValue }
}
}
Tested with Xcode 13.4
Is there a better way to do this? Is there a way to access the UserDefaults in the environment?? I did the following:
struct ContentView: View {
#AppStorage("darkMode") var darkMode = false
var body: some View {
SubView(darkMode: $darkMode)
}
}
}
struct SubView: View {
#Binding var darkMode: Bool
var body: some View {
Text("Dark Mode is \(darkMode == true ? "on" : "off")")
}
}
By using #AppStorage in different views you still access the same UserDefaults.standard storage (unless you explicitly specify the suiteName).
Which means you can just use the #AppStorage directly in the subview.
struct ContentView: View {
#AppStorage("darkMode") var darkMode = DefaultSettings.darkMode
var body: some View {
VStack {
Button("Toggle dark mode") {
self.darkMode.toggle()
}
SubView()
}
.colorScheme(darkMode ? .dark : .light)
.preferredColorScheme(darkMode ? .dark : .light)
}
}
struct SubView: View {
#AppStorage("darkMode") var darkMode = DefaultSettings.darkMode
var body: some View {
Text("Dark Mode is \(darkMode == true ? "on" : "off")")
}
}
enum DefaultSettings {
static let darkMode = false
}
Note: the default darkMode value is extracted (to the DefaultSettings enum) so you don't repeat false in each view.
Alternatively you can inject #AppStorage directly to the environment. See:
Can #AppStorage be used in the Environment in SwiftUI?
According to my understanding, if you define a view yourself (as a struct that implements View), then you can declare some var to be an Environment variable, like this:
#Environment(\.isEnabled) var isEnabled
This will give you access to the EnvironmentValues.isEnabled field.
However, it seems like this is only possible within the view definition itself.
Is it possible, given some view v, to get the environment object of that view? or get specific environment values?
I assume that taking into account that SwiftUI is reactive state managed framework then yes, directly you cannot ask for internal view environment value, actually because you can't be sure when that environment is set up for this specific view. However you can ask that view to tell you about its internal state, when view is definitely knowns about it... via binding.
An example code (a weird a bit, but just for demo) how this can be done see below.
struct SampleView: View {
#Environment(\.isEnabled) private var isEnabled
var myState: Binding<Bool>?
var body: some View {
VStack {
Button(action: {}) { Text("I'm \(isEnabled ? "Enabled" : "Disabled")") }
report()
}
}
func report() -> some View {
DispatchQueue.main.async {
self.myState?.wrappedValue = self.isEnabled
}
return EmptyView()
}
}
struct TestEnvironmentVar: View {
#State private var isDisabled = false
#State private var sampleState = true
var body: some View {
VStack {
Button(action: {
self.isDisabled.toggle()
}) {
Text("Toggle")
}
Divider()
sampleView()
.disabled(isDisabled)
}
}
private func sampleView() -> some View {
VStack {
SampleView(myState: $sampleState)
Text("Sample is \(sampleState ? "Enabled" : "Disabled")")
}
}
}
struct TestEnvironmentVar_Previews: PreviewProvider {
static var previews: some View {
TestEnvironmentVar()
}
}
Is there a way to have a global state variable in SwiftUI? It would be nice to be able to have all my views subscribe to the same state. Is there some reason not to do this?
When I tried to declare a global variable with the #State decorator, the swift compiler crashed (beta software, am I right?).
#State is only for managing local variables. The wrapper you're looking for is #EnvironmentObject. You could use this for theme color, orientation, subscribed or non subscribed users etc etc.
var globalBool: Bool = false {
didSet {
// This will get called
NSLog("Did Set" + globalBool.description)
}
}
struct GlobalUser : View {
#Binding var bool: Bool
var body: some View {
VStack {
Text("State: \(self.bool.description)") // This will never update
Button("Toggle") { self.bool.toggle() }
}
}
}
...
static var previews: some View {
GlobalUser(bool: Binding<Bool>(getValue: { globalBool }, setValue: {
globalBool = $0 }))
}