SwiftUI ViewModifier for custom View

5.4k views Asked by At

Is there a way to create a modifier to update a @State private var in the view being modified?

I have a custom view that returns either a Text with a "dynamic" background color OR a Circle with a "dynamic" foreground color.

struct ChildView: View {
    var theText = ""
    
    @State private var color = Color(.purple)
    
    var body: some View {
        HStack {
            if theText.isEmpty {          // If there's no theText, a Circle is created
                Circle()
                    .foregroundColor(color)
                    .frame(width: 100, height: 100)
            } else {                      // If theText is provided, a Text is created
                Text(theText)
                    .padding()
                    .background(RoundedRectangle(cornerRadius: 25.0)
                                    .foregroundColor(color))
                    .foregroundColor(.white)
            }
        }
    }
}

I re-use this view in different parts around my app. As you can see, the only parameter I need to specify is theText. So, the possible ways to create this ChildView are as follows:

struct SomeParentView: View {
    var body: some View {
        VStack(spacing: 20) {
            ChildView()   // <- Will create a circle

            ChildView(theText: "Hello world!")   // <- Will create a text with background
        }
    }
}

enter image description here

Nothing fancy so far. Now, what I need is to create (maybe) a modifier or the like so that in the parent views I can change the value of that @State private var color from .red to other color if I need more customization on that ChildView. Example of what I'm trying to achieve:

struct SomeOtherParentView: View {
    var body: some View {
        HStack(spacing: 20) {
            ChildView()

            ChildView(theText: "Hello world!")
                .someModifierOrTheLike(color: Color.green)   // <- what I think I need
        }
    }
}

I know I could just remove the private keyword from that var and pass the color as parameter in the constructor (ex: ChildView(theText: "Hello World", color: .green)), but I don't think that's the way to solve this problem, because if I need more customization on the child view I'd end up with a very large constructor.

So, Any ideas on how to achieve what I'm looking for? Hope I explained myself :) Thanks!!!

4

There are 4 answers

5
Asperi On BEST ANSWER

It is your view and modifiers are just functions that generate another, modified, view, so... here is some possible simple way to achieve what you want.

Tested with Xcode 12 / iOS 14

demo

struct ChildView: View {
    var theText = ""
    
    @State private var color = Color(.purple)
    
    var body: some View {
        HStack {
            if theText.isEmpty {          // If there's no theText, a Circle is created
                Circle()
                    .foregroundColor(color)
                    .frame(width: 100, height: 100)
            } else {                      // If theText is provided, a Text is created
                Text(theText)
                    .padding()
                    .background(RoundedRectangle(cornerRadius: 25.0)
                                    .foregroundColor(color))
                    .foregroundColor(.white)
            }
        }
    }
    
    // simply modify self, as self is just a value
    public func someModifierOrTheLike(color: Color) -> some View {
        var view = self
        view._color = State(initialValue: color)
        return view.id(UUID())
    }
}
0
New Dev On

Using a custom ViewModifier is indeed a way to help expose a simpler interface to users, but the general idea of how to pass customization parameters to a View (other than using an init), is via environment variables with .environment.

struct MyColorKey: EnvironmentKey {
   static var defaultValue: Color = .black
}

extension EnvironmentValues {
   var myColor: Color {
      get { self[MyColorKey] }
      set { self[MyColorKey] = newValue }
   }
}

Then you could rely on this in your View:

struct ChildView: View {
   @Environment(\.myColor) var color: Color

   var body: some View {
      Circle()
         .foregroundColor(color)
         .frame(width: 100, height: 100)
   }
}

And the usage would be:

ChildView()
   .environment(\.myColor, .blue)

You can make it somewhat nicer by using a view modifier:

struct MyColorModifier: ViewModifier {
   var color: Color

   func body(content: Content) -> some View {
      content
         .environment(\.myColor, color)
   }
}

extension ChildView {
   func myColor(_ color: Color) { 
      self.modifier(MyColorModifier(color: color) 
   }
}

ChildView()
   .myColor(.blue)

Of course, if you have multiple customizations settings or if this is too low-level for the user, you could create a ViewModifier that exposes a subset of them, or create a type that encapsulates a style, like SwiftUI does with a .buttonStyle(_:)

0
MariusK On

Here's how you can chain methods based on Asperi`s answer:

struct ChildView: View {
    @State private var foregroundColor = Color.red
    @State private var backgroundColor = Color.blue

    var body: some View {
        Text("Hello World")
            .foregroundColor(foregroundColor)
            .background(backgroundColor)
    }

    func foreground(color: Color) -> ChildView {
        var view = self
        view._foregroundColor = State(initialValue: color)
        return view
    }

    func background(color: Color) -> ChildView {
        var view = self
        view._backgroundColor = State(initialValue: color)
        return view
    }
}

struct ParentView: View {
    var body: some View {
        ChildView()
            .foreground(color: .yellow)
            .background(color: .green)
            .id(UUID())
    }
}
0
Mahi Al Jawad On

I noticed you are using @State property wrapper, which you actually don't need.

We use any property wrapper e.g. @State if there's a way to change the value of the state from your view. But notice that you don't ever change the value color from inside your view.

Yes, you modify your view from some custom modifier function's made using builder pattern. But in that case you don't need the state variable, also because you eventually create another view and return (as per Asperi's answer).

So I'd suggest the following code in your use-case scenario:

struct ChildView: View {
    var theText = ""
    
    init(theText: String) {
        self.theText = theText
    }
    
    private var color = Color(.purple)
    
    var body: some View {
        HStack {
            if theText.isEmpty {          // If there's no theText, a Circle is created
                Circle()
                    .foregroundColor(color)
                    .frame(width: 100, height: 100)
            } else {                      // If theText is provided, a Text is created
                Text(theText)
                    .padding()
                    .background(RoundedRectangle(cornerRadius: 25.0)
                                    .foregroundColor(color))
                    .foregroundColor(.white)
            }
        }
    }
    
    // simply modify self, as self is just a value
    public func customModifier(color: Color) -> ChildView {
        var view = self
        view.color = color
        return view
    }
}

struct ContentView: View {
    var body: some View {
        ChildView(theText: "abd")
            .customModifier(color: .green)
    }
}

I hope it improves your code.

As per as Apple's suggestion we should use @State variable only when the property is mutable from inside the view itself (Ref: https://developer.apple.com/documentation/swiftui/managing-user-interface-state).