swiftuicustom-view

How do I change the background color of a Custom View like SwiftUI Views


I wanted my Custom views to behave like SwiftUI Views and for simplicity I'm considering the background and foreground colors. Now, instead of individually defining the foreground/background color of each subview of a view, I want to change it externally as in SwiftUI views. For simplicity, there is one subView: Text; on my tries.

import SwiftUI

struct TestButton: View {
    var text: String
    @State private var foregroundColor: Color = .primary
    @State private var backgroundColor: Color = .white
    @State private var id = UUID()

    var body: some View {
        Text(text)
            .foregroundColor(foregroundColor )
            .padding()
            .background(backgroundColor)
            .cornerRadius(10)
            .id(id)
            .accentColor(foregroundColor)
    }

    public func foregroundColor(_ color: Color)  {
        foregroundColor = color

        id = UUID()
    }

    public func backgroundFill(_ color: Color) {
        backgroundColor = color
        id = UUID()
    }
}

struct TestButton_Previews: PreviewProvider {
    static var previews: some View {
        TestButton(text: "Test")
            .foregroundColor(.red)
            .backgroundFill(.green)
    }
}

This doesn't work on Xcode 14.3 canvas, although I was expecting to work. So, I've turned into emulator to see there is a difference and the result was same as in canvas.

How can we design a Custom View so that we can use color changes as in SwiftUI's View?


Solution

  • Your question is very interesting because of what is fairly straight-forward and what is impossible, and the very surprising dividing line between them. I'll build this up from absolutely trivial to impossible.

    First, start with mapping .foregroundColor() to the color of the text. That is trivial and in fact requires no code at all. It "just works."

    struct TestButton: View {
        var text: String
    
        // Fixed this. It should not be variable or State.
        // It needs to be constant. But it doesn't actually matter for this example
        private let id = UUID()
    
        var body: some View {
            Text(text)
                .padding()
                .cornerRadius(10)
                .id(id)
        }
    }
    
    struct TestButton_Previews: PreviewProvider {
        static var previews: some View {
            TestButton(text: "Test")
                .foregroundColor(.red)
        }
    }
    

    There's no need for any variables. The value of .foregroundColor() is passed directly to Text with no intervention at all. Magic!

    How do you recreate that magic for backgroundFill? With Environment. This is how these settings are passed down the hierarchy. First, create an environment key, and extend EnvironmentValues to access it:

    private struct BackgroundFillKey: EnvironmentKey {
        static let defaultValue: Color = .white
    }
    
    extension EnvironmentValues {
        var backgroundFill: Color {
            get { self[BackgroundFillKey.self] }
            set { self[BackgroundFillKey.self] = newValue }
        }
    }
    

    Then, use @Environment to access it:

    struct TestButton: View {
        var text: String
    
        @Environment(\.backgroundFill)
        var backgroundFill: Color
    
        @State private var id = UUID()
    
        var body: some View {
            Text(text)
                .padding()
                .background(backgroundFill)
                .cornerRadius(10)
                .id(id)
        }
    }
    

    For convenience, add an extension to View to set it:

    extension View {
        func backgroundFill(_ color: Color) -> some View {
            environment(\.backgroundFill, color)
        }
    }
    

    And then use it:

    struct TestButton_Previews: PreviewProvider {
        static var previews: some View {
            TestButton(text: "Test")
                .foregroundColor(.red)
                .backgroundFill(.green)
        }
    }
    

    And this is how SwiftUI manages its own Views.

    But I've left out one bit of your question, which is this modifier:

            .accentColor(foregroundColor)
    

    In your example, this doesn't actually matter. Text doesn't use accentColor this way. But I think you're asking a very general question, which is "how do I set accentColor to be foregroundColor?" And a little more broadly, "how do I access foregroundColor?"

    The answer is you don't. That environment value is marked internal. I don't know why. Here is some useful commentary: How To Read Foreground and Tint Colors in SwiftUI Views, and a possible workaround/hack. But ultimately, it's not publicly available.

    You can, of course, create your own .foregroundTextButtonColor following the above explanation of .backgroundFill, but it won't integrate with SwiftUI's .foregroundColor.