Help me find the best way to conditionally override SwiftUI.View.accentColor(...)

My view has two Bool conditions: condtion1 and condition2. They can both be false, or only one true. When both conditions are false, just inherit the default accentColor from the parent view. If condition1 is true, override the accentColor to color1, else if condtion2 is true, override the accentColor to color2.

Unfortunately, the .accentColor() modifier doesn't seem to have any way to tell it to use the default from parent view, both these do not result in the default from parent:

.accentColor(.accentColor)
.accentColor(nil)

these two set the accentColor to blue. So a conditional modifier is needed. I came up with three ways but is there some better way? Here is my very simple sample code:

import SwiftUI

// copy from https://fivestars.blog/swiftui/conditional-modifiers.html
extension View {
    @warn_unqualified_access
    @ViewBuilder
    func `if`<Transform: View>(_ condition: Bool, transform: (Self) -> Transform) -> some View {
        if condition {
            transform(self)
        } else {
            self
        }
    }
}

struct ChildAAA: View {
    let condition1: Bool
    let condition2: Bool
    let color1 = Color.green
    let color2 = Color.yellow

    var body: some View {
        Button("Help Me!!!") {
        }
        // this works but do not like because the two if's conditions are evaluated, want short circuit if condition1 is true
        .if(condition1) { $0.accentColor(color1)}
        .if(condition2) { $0.accentColor(color2)}
    }
}

struct ChildBBB: View {
    let condition1: Bool
    let condition2: Bool
    let color1 = Color.green
    let color2 = Color.yellow

    var body: some View {
        Button("Help Me!!!") {
        }
        // better, but harder to comprehend and `condition1` is repeated and evaluated twice
        .if(condition1 || condition2) { $0.accentColor(condition1 ? color1 : color2) }
    }
}


// how about add an `if` overload that take a pair of (condition, transform)?
extension View {
    @warn_unqualified_access
    @ViewBuilder
    func `if`<Transform1: View, Transform2: View>(_ condition1: Bool, _ transform1: (Self) -> Transform1, _ condition2: () -> Bool, _ transform2: (Self) -> Transform2) -> some View {
        if condition1 {
            transform1(self)
        } else if condition2() {
            transform2(self)
        } else {
            self
        }
    }
}


struct ChildCCC: View {
    let condition1: Bool
    let condition2: () -> Bool
    let color1 = Color.green
    let color2 = Color.yellow

    init(condition1: Bool, condition2: @escaping @autoclosure () -> Bool) {
        self.condition1 = condition1
        self.condition2 = condition2
    }

    var body: some View {
        Button("Help Me!!!") {
        }
        // is this better?
        // with this, condition1 and condition2 appear once, can short circuit if condition1 is true
        .if(condition1, { $0.accentColor(color1) }, condition2, { $0.accentColor(color2) })
    }
}


struct ContentView: View {
    var body: some View {
        VStack {
            ChildAAA(condition1: false, condition2: false)
            ChildAAA(condition1: true, condition2: false)
            ChildAAA(condition1: false, condition2: true)

            ChildBBB(condition1: false, condition2: false)
            ChildBBB(condition1: true, condition2: false)
            ChildBBB(condition1: false, condition2: true)

            ChildCCC(condition1: false, condition2: false)
            ChildCCC(condition1: true, condition2: false)
            ChildCCC(condition1: false, condition2: true)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
.accentColor(
  condition1 ? color1 :
  condition2 ? color2 :
  nil
)

This sounds very fizzbuzz, heh.

Unfortunately, nil doesn't mean inherit the default. It's blue.

You can add an environment key in your View and pass that down instead of nil.

struct MyStruct: View {
  @Environment(\.accentColor) var parentAccentColor

  var resolvedAccentColor: Color {
    condition1 ? color1 :
    condition2 ? color2 :
    parentAccentColor
  }

  // ...

}

I tried that but:

'accentColor' is inaccessible due to "internal" protection level

:(

You can add an environment key in your View

Okay, let me try this...

Oh, wait! Sorry, Color.accentColor is how you access this, no environment key needed.

1 Like

From accentColor(_:),

The color to use as an accent color. If nil , the accent color continues to be inherited

Sounds like a bug (unless they mean inherit from the system).

In the mean time, you can do:

extension View {
  @ViewBuilder
  func conditional(accentColor: Color?) -> some View {
    if let color = color {
      self.accentColor(accentColor)
    } else {
      self
    }
  }
}

...
.conditional(accentColor:
  condition1 ? color1 :
  condition2 ? color2 :
  nil
)
1 Like

As a sidenote, it's not advisable to make one of these view builders that swaps views out under the hood like this. This causes the view to get removed from the hierarchy and a new one put in its place whenever the condition changes, rather than just changing the accent color.

Agreed but, there's no inherit option for accent color. So that's about as good as it gets without explicitly passing accentColor around, which has its own annoyance.

I saw your tweet yesterday. So all these so call "conditional modifier" view extension are no good?!

Anyway, .accentColor(:) modifier passing Color.accentColor or nil do not inherit the accent color from parent, it set it to blue.

This one actually seems like a bug, then. Color.accentColor is meant to resolve to the current accent color inherited from the environment.

Apologies for leading y'all astray for a bit!

It's pretty bad for views that move around, or change shape, because you're just transitioning views in and out, instead of animating it. Relatively safe (though I can't recommend it still) for static views, though.

Yea, doesn't seem to match the description (I mentioned above). You might want to file a bug report for that.

I think it should be:

extension View {
  @ViewBuilder
  func conditional(accentColor: Color?) -> some View {
    if let color = accentColor {
    // or this:
    if accentColor != nil {     // better?
      self.accentColor(accentColor)  // give it the original optional, not color so it's not promoted to optional again
    } else {
      self
    }
  }
}

??

I file a feedback: FB8832606

1 Like

I filed a bug just last week on this. .accentColor and nil are not "blue", but the app's Accent Color as defined in your Build Settings>"Global Accent Color Name". You can set this to be the name of a colour in your asset catalogs, allowing you to use your original logic.

what do you mean "your original logic"?

I don't see any effect of setting the value of ** Build Settings>"Global Accent Color Name"**, leaving it blank or setting it to some name, app accent color still take on "AccentColor" in asset catalog or blue:

import SwiftUI

// "AccentColor" in asset catalog is set to "systemOrangeColor"
// "FooColor" in asset catalog is set to "systemGreenColor"
// Build Settings>"Global Accent Color Name" set to "FooColor"
struct ContentView: View {
    var body: some View {
        VStack {
            Button("Expect Orange Color") {}    // Orange, not Green!
            Button("Expect Orange Color") {}    // blue :(
                .accentColor(nil)
            Button("Expect Orange Color") {}    // blue :(
                .accentColor(.accentColor)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

I even change "AccentColor" to "AccentColorXXX" and app accent color still not green, but blue if there is not "AccentColor" in asset catalog.

I did have trouble getting the build setting to take effect. Deleting derived data did the trick for me though. I meant that passing Color.accentColor anywhere uses the accent color defined in build settings, and passing .accentColor(nil) is the equivalent to .accentColor(.accentColor)

Doesn't do it for me. Xcode Version 12.2 beta 3 (12B5035g).

That's odd. From Xcode pref/Locations, I click the arrow to open Finder/DerivedData folder, close Xcode, delete all in DerivedData. Re-open Xcode. It's as if "AccentColor" is hardcoded and "Global Accent Color Name" build setting value not used at all.

So just be sure: you can get:

aView.accentColor(nil)

to show in the right accent color from setting?

That is correct. It was a morning of great frustration getting the build setting to take effect. I wish I could provide steps to reproduce, but it was a lot of naive attempts and a lot of hope (deleting app on device, deleting derived data, restarting Xcode... there was a Big Sur beta update in there too...)

Xcode 12.1 (12A7403)

The new .tint(_:): replacement for .accentColor(_:) appears to work correctly with nil:

.tint(nil)    // this gets the "AccentColor" from the Asset catalog, .accentColor(nil) used to always be blue

.accentColor(_:) still wrong. Do not mix. Only use .tint(_:) for correct behavior.