I have this test code, I think I code everything right, but for some reasons it does not work, I mean it does not apply the modification. I cannot see the issue why?
my goal is stop repeating input value to modifier and using parent view values
import SwiftUI
struct ContentView: View {
var body: some View {
MyView(value: "Hello, world!", color: .red, border: true)
.bordered()
}
}
struct MyView: View {
let value: String
let color: Color
let border: Bool
var body: some View {
Text(value)
.foregroundStyle(color)
.environment(\.borderColor, color)
.environment(\.isBorderEnabled, border)
}
}
struct BorderedModifier: ViewModifier {
@Environment(\.borderColor) private var color
@Environment(\.isBorderEnabled) private var border
func body(content: Content) -> some View {
Group {
if (border) {
content
.padding(5.0)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(color, lineWidth: 2)
)
} else {
content
}
}
}
}
extension View {
func bordered() -> some View {
self.modifier(BorderedModifier())
}
}
private struct BorderColorKey: EnvironmentKey {
static let defaultValue: Color = .black
}
private struct BorderEnabledKey: EnvironmentKey {
static let defaultValue: Bool = false
}
extension EnvironmentValues {
var borderColor: Color {
get { self[BorderColorKey.self] }
set { self[BorderColorKey.self] = newValue }
}
var isBorderEnabled: Bool {
get { self[BorderEnabledKey.self] }
set { self[BorderEnabledKey.self] = newValue }
}
}
This is an issue of modifier ordering. The view modifier will only pick up environment values that were set after the view modifier was applied. But the way you have it, the environment values are being set before the view modifier is applied.
You can fix by moving the .bordered()
modifier into MyView
:
// MyView
Text(value)
.foregroundStyle(color)
.bordered() // 👈 modifier inserted here
.environment(\.borderColor, color)
.environment(\.isBorderEnabled, border)
// ContentView
MyView(value: "Hello, world!", color: .red, border: true)
// .bordered() // 👈 modifier removed from here
An alternative way to get it working is to change the logic a little.
In fact, you already had the parent view determining whether the border is enabled or not, because you were passing a boolean flag to the child view as parameter.
So here is the updated example to show how it could work this way.
.isBorderEnabled
via the View
extension bordered
.false
as parameter to .bordered
, or just don't call it at all.BorderedModifier
is called by the child view via the new View
extension borderColor
..borderColor
is no longer needed.struct ContentView: View {
var body: some View {
VStack {
MyView(value: "The quick brown fox", color: .red)
MyView(value: "jumps over the lazy dog", color: .blue)
}
.bordered()
// .bordered(false)
}
}
struct MyView: View {
let value: String
let color: Color
var body: some View {
Text(value)
.foregroundStyle(color)
.borderColor(color: color)
}
}
struct BorderedModifier: ViewModifier {
let color: Color
@Environment(\.isBorderEnabled) private var border
func body(content: Content) -> some View {
Group {
if border {
content
.padding(5.0)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(color, lineWidth: 2)
)
} else {
content
}
}
}
}
extension View {
func borderColor(color: Color = .black) -> some View {
self.modifier(BorderedModifier(color: color))
}
func bordered(_ enabled: Bool = true) -> some View {
self.environment(\.isBorderEnabled, enabled)
}
}
private struct BorderEnabledKey: EnvironmentKey {
static let defaultValue: Bool = false
}
extension EnvironmentValues {
var isBorderEnabled: Bool {
get { self[BorderEnabledKey.self] }
set { self[BorderEnabledKey.self] = newValue }
}
}