'How to assign default modifiers to a SwiftUI view?
I'm trying to make reusable custom views that themselves can be customized later. Ideally I'd be able to define default modifiers like Color, Font, etc that the Views would have without any customization, but allow these to be easily overwritten by additional modifiers used later on.
For example if I had the custom view:
struct MyCustomTextField: View {
@Binding var text: String
var body: some View {
HStack {
Image(systemName: "envelope").foregroundColor(.gray)
TextField("", text: $text)
.foregroundColor(.gray)
.font(.system(.title))
}
}
}
The view would have default gray foreground color and the TextField would have title font. But now if I wanted to reuse this view and customize it for my specific use-case, I might want to override these modifiers like so:
struct ContentView: View {
@State var text = "hello"
var body: some View {
MyCustomTextField(text: $text)
.foregroundColor(.blue)
.font(.system(.body))
}
}
But these outside modifiers are not effective as the inner modifiers take precedent.
What's the best way to make custom views so that I can define default modifiers for their contents but still be able to override those defaults later?
Thanks
Solution 1:[1]
We should always remember that SwiftUI Views are value types, therefore you cannot change them after the initialization like you would do on UIKit. One possible way would be to inject it on the initialization. But if you wanna make it adjustable after the initialization (e.g to change the color upon pressing some other button), use the binding mechanism:
Add a @Binding
to your MyCustomTextField, and use its value at the foregroundColor modifier.
struct MyCustomTextField: View {
@Binding var text: String
@Binding var foregroundColor: Color
var body: some View {
HStack {
Image(systemName: "envelope").foregroundColor(.gray)
TextField("", text: $text)
.foregroundColor(foregroundColor)
.font(.system(.title))
}
}
}
and then in the client view you can do as following:
struct MyView: View {
@State var myText: String = ""
@State var hasSpecialColor: Bool = false
var foregroundColor: Binding<Color> {
return Binding<Color>(get: {
return hasSpecialColor ? Color.red : Color.black
}, set: { newValue in
hasSpecialColor = newValue == Color.red
})
}
var body: some View {
VStack {
Text("Some Header Text")
MyCustomTextField(text: $myText, foregroundColor: foregroundColor)
Button("click to toggle color") {
hasSpecialColor.toggle()
}
}
}
}
now, everytime you will click the button, it will toggle hasSpecialColor (which is @State
), and therefore the view will render and the value for the @Binding
foregroundColor will change accordingly.
Solution 2:[2]
The font
modifier stores the Font
in the environment for access by the modified views. You can read it and provide a default like this:
struct MyCustomTextField: View {
@Binding var text: String
@Environment(\.font)
var envFont: Font?
var body: some View {
HStack {
Image(systemName: "envelope").foregroundColor(.gray)
TextField("", text: $text)
.foregroundColor(.gray)
.font(envFont ?? .system(.title))
}
}
}
Unfortunately, the foregroundColor
modifier doesn't store its setting in a (public) environment property, so you have to go a different route. One way is to provide a modifier directly on your custom view, like this:
struct MyCustomTextField: View {
@Binding var text: String
var _myColor: Color = .gray
@Environment(\.font)
var envFont: Font?
func myColor(_ color: Color) -> Self {
var copy = self
copy._myColor = color
return copy
}
var body: some View {
HStack {
Image(systemName: "envelope").foregroundColor(.gray)
TextField("", text: $text)
.foregroundColor(_myColor)
.font(envFont ?? .system(.title))
}
}
}
You can then use the myColor
modifier directly on a MyCustomTextField
like this:
PlaygroundPage.current.setLiveView(
MyCustomTextField(text: .constant("hello"))
.myColor(.red)
.padding()
.font(.body)
)
But you cannot use it on any enclosing view or after any non-MyCustomTextField
-specific modifier. For example, this will not work:
PlaygroundPage.current.setLiveView(
MyCustomTextField(text: .constant("hello"))
.padding()
.myColor(.red) // Error: Value of type 'some View' has no member 'myColor'
.font(.body)
)
If you want that to work, then you need to store the custom color in the environment, like this:
struct MyColor: EnvironmentKey {
static var defaultValue: Color? { nil }
}
extension EnvironmentValues {
var myColor: Color? {
get { self[MyColor.self] }
set { self[MyColor.self] = newValue }
}
}
extension View {
func myColor(_ color: Color?) -> some View {
return self.environment(\.myColor, color)
}
}
struct MyCustomTextField: View {
@Binding var text: String
@Environment(\.myColor)
var envColor: Color?
@Environment(\.font)
var envFont: Font?
var body: some View {
HStack {
Image(systemName: "envelope").foregroundColor(.gray)
TextField("", text: $text)
.foregroundColor(envColor ?? .gray)
.font(envFont ?? .system(.title))
}
}
}
And then you can use the myColor
modifier on any view, and it will apply to all enclosed subviews:
PlaygroundPage.current.setLiveView(
MyCustomTextField(text: .constant("hello"))
.padding()
.myColor(.red)
.font(.body)
)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Maor Atlas |
Solution 2 |