swiftswiftui

Why ViewModifier does not apply changes with EnvironmentKey?


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 }
    }
}

Solution

  • 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.

    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 }
        }
    }
    

    Screenshot