2

Sometimes when custom a view in SwiftUI, it require some parameters to change the appearance of it which is not possible to be achieve by ViewModifier. Like the demo shown below, I want to set text color and image color separately.

struct CustomView: View {
    
    var imageColor: Color
    var textColor: Color
    
    init(imageColor: Color = .primary, textColor: Color = .primary) {
        self.imageColor = imageColor
        self.textColor = textColor
    }
    
    var body: some View {
        VStack {
            Image(systemName: "star")
                .foregroundColor(imageColor)
            Text("Hello World")
                .foregroundColor(textColor)
        }
    }
}

Though providing parameters via init() works. But it's ugly when there're tens of parameters. It could be a nightmare to invoke it.

SwiftUI uses modifiers to solve this problem. It couldn't be better if I can custom my own.

Solution1 (but not working)

struct CustomView: View {
    
    var imageColor: Color = .primary
    var textColor: Color = .primary
    
    var body: some View {
        VStack {
            Image(systemName: "star")
                .foregroundColor(imageColor)
            Text("Hello World")
                .foregroundColor(textColor)
        }
    }
    
    mutating func imageColor(_ imageColor: Color) -> CustomView {
        self.imageColor = imageColor
        return self
    }
    
    mutating func textColor(_ textColor: Color) -> CustomView {
        self.textColor = textColor
        return self
    }
}

But there's an error when compiling Cannot use mutating member on immutable value: function call returns immutable value.

CustomView()
    .imageColor(.red)    // error
    .textColor(.blue)    // error

Then I changed the var to @State var in CustomView struct. But the value will never change when calling it. The reason I found is discussed here: https://forums.swift.org/t/im-so-confused-why-state-var-inside-a-view-do-not-change-when-you-call-a-method-on-this-view-from-some-outer-view/31944

Solution2 (but inefficient)

struct CustomView: View {
    
    var imageColor: Color
    var textColor: Color
    
    init(imageColor: Color = .primary, textColor: Color = .primary) {
        self.imageColor = imageColor
        self.textColor = textColor
    }
    
    var body: some View {
        VStack {
            Image(systemName: "star")
                .foregroundColor(imageColor)
            Text("Hello World")
                .foregroundColor(textColor)
        }
    }

    func imageColor(_ imageColor: Color) -> CustomView {
        return CustomView(imageColor: imageColor, textColor: self.textColor)
    }

    func textColor(_ textColor: Color) -> CustomView {
        return CustomView(imageColor: self.imageColor, textColor: textColor)
    }
}

Obviously, it takes a little more system resource as shown in below monitor of Xcode. So how can I add modifiers like these efficiently? How Apple SwifUI's built-in modifiers made this? Memory-use monitored by Xcode

5
  • the best and simplest approach will be to use @State wrapper before var then you don't have to have mutable Commented Jan 24, 2023 at 10:34
  • @NoorAhmedNatali I can't make the @State work. The actual value will never change. The reason are described here: forums.swift.org/t/… Commented Apr 9, 2023 at 10:50
  • I struggle with exactly the same issue. Did you find anything in the mean time? Commented Jan 12, 2024 at 14:52
  • @Gregor One solution I'm using is to define a custom EnvironmentKey and extension it in EnvironmentValues, then set that environment value inside modifier-like functions like environment(\.customEnvironment, value). But it's not ideal, since the environment value will affect the child views. Commented Jan 13, 2024 at 8:09
  • Thanks @YimingDesigner. I already have your solution as a workaround. Even with a function that sets the environment. The only thing missing is that the function has to return "some View". Would it be possible to return the custom View itself it would have been much cleaner. Commented Jan 15, 2024 at 10:19

2 Answers 2

2

I have a similar situation here. I have a padding inside the CustomView that I would like to change via a specific modifier, and the following seemed to work in my case (changed the wording to work with your example):

extension CustomView {
    func imageColor(_ imageColor: Color) -> CustomView {
        var newView = self
        self.imageColor = imageColor
        return newView
    }
}

Then you can use:

CustomView()
    .imageColor(.red) 

Hope it helps.

Sign up to request clarification or add additional context in comments.

Comments

-1

To build your own view modifier, you first need to define a struct that conforms to ViewModifier, e.g.

struct ImageColor:  ViewModifier {    
    let color: Color
    
    func body(content: Content) -> some View {
        content
            .foregroundColor(color)
    }
}

then for ease of use at the call-site, add a view extension as follows:

extension View where Self == Image {
    func imageColor(_ color: Color) -> some View {
        modifier(ImageColor(color: color))
    }
}

then you can use it just like a "built-in" modifier…

Image(systemName: "star")
    .imageColor(imageColor)

1 Comment

As I mentioned in the question, sometimes there are some abstractions (e.g. in a module) that I cannot access inner code of CustomView. So I think ViewModifier won't work.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.