Search code examples
swiftui

How do I add a conditional shadow in this modifier?


I created a shadow modifier to a View.

struct ConditionalShadowModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .shadow(color: condition ? Color.black.opacity(0.2) : Color.clear, radius: 10)
    }
}

I need to add a condition to show and hide the shadow. Any idea?


Solution

  • Your code is fine, but know that you do not need a ViewModifier struct to apply a conditional shadow (or any other modifier).

    So in your example, you could perform the same logic directly in the view extension function:

    extension View {
        func conditionalShadow(_ condition: Bool) -> some View {
            self
                .shadow(color: condition ? Color.black.opacity(0.2) : Color.clear, radius: condition ? 10 : 0)
        }
    }
    

    Personally, I prefer this approach, since it's faster to check the conditional logic as the app's codebase and complexity grows.

    I'll use a ViewModifier like you did only if the use case requires it, like maybe when I pass bindings or have to perform more complex operations that are more suited for a struct. But for a simple shadow modifier, I find it to be overkill.

    Now, what can be even faster and more flexible, but still reusable, is to keep the conditional logic inline, without a custom view extension function.

    To do this, you will need to rely on some kind of default values that would normally come from the environment or some observable class. To keep it simple, here's a basic example, using a struct:

    struct ContentView: View {
        @State private var isShadowed = false
        
        let defaults = Defaults()
    
        var body: some View {
            VStack {
                Toggle("Toggle Shadow", isOn: $isShadowed)
                    .padding()
                
                Rectangle()
                    .fill(Color.blue)
                    .frame(width: 100, height: 100)
                    .shadow(color: isShadowed ? defaults.shadowColor : .clear, radius: isShadowed ? defaults.shadowRadius : 0)
            }
            .padding()
        }
    }
    
    struct Defaults {
        
        let shadowRadius: CGFloat = 10
        let shadowColor: Color = .black.opacity(0.2)
        
    }
    

    If you find that you need to apply this modifier often, extract it into an extension as mentioned before:

    Rectangle()
        .fill(Color.blue)
        .frame(width: 100, height: 100)
        .defaultShadow(isShadowed)
    
    extension View {
        
        func defaultShadow(_ apply: Bool) -> some View {
            let defaults: Defaults = Defaults()
            return self
                .shadow(color: apply ? defaults.shadowColor : .clear, radius: apply ? defaults.shadowRadius : 0)
        }
    }
    

    But if you do use it that often, chances are that it will need to rely on more global states and values (like the app's color scheme, rather than the isShadowed state). In that case, the condition should happen entirely in the extension function, which would require no parameters:

    Rectangle()
        .fill(Color.blue)
        .frame(width: 100, height: 100)
        .shadowedIfLightMode()
    
    extension View {
        
        func shadowedIfLightMode() -> some View {
            let defaults = Defaults()
            return self
                .shadow(color: defaults.darkMode ? .clear : defaults.shadowColor , radius: defaults.darkMode ? 0 : defaults.shadowRadius )
        }
    }
    

    For more example on using modifiers conditionally, here's some functional code to explore. Note that no ViewModifiers structs were used:

    import SwiftUI
    
    struct ConditionalShadow: View {
        
        @State private var defaults = Defaults()
        @State private var weatherCondition: WeatherCondition = .sunny
        
        var body: some View {
            
            let darkMode = defaults.darkMode
            
            ZStack {
                Group {
                    if darkMode {
                        Color.black
                    } else {
                        Color.yellow
                    }
                }
                .ignoresSafeArea()
                
                VStack(spacing: 40) {
                    Group {
                        if darkMode {
                            Image(systemName: "bed.double")
                                .foregroundStyle(.black.opacity(0.8))
                                .faintShadow()
                        } else {
                            Image(systemName: "figure.run")
                                .highlightShadow()
                        }
                    }
                    .font(.system(size: 120))
                    
                    
                    Text(darkMode ? "Good night" : "Good morning")
                        .textShadow(darkMode)
                        .bigCapsuleStyle(darkMode ? .blue : .black)
                        .weatherOverlay(weatherCondition, darkMode)
                        .saturation(darkMode ? defaults.darkModeSaturation : 1)
                    
                }
            }
            
            HStack {
                Toggle("Dark mode", isOn: $defaults.darkMode)
                    .padding()
                    .fixedSize()
                
                Picker("Weather", selection: $weatherCondition){
                    Text("Sunny").tag(.sunny as WeatherCondition)
                    Text("Rain").tag(.rain as WeatherCondition)
                    Text("Snow").tag(.snow as WeatherCondition)
                }
            }
            
        }
    }
    
    @Observable
    class Defaults {
        
        var darkMode = false
        let shadowRadius: CGFloat = 10
        let shadowColor: Color = .black.opacity(0.2)
        let darkModeSaturation: Double = 0.5
        
    }
    
    enum WeatherCondition {
        case sunny, rain, snow
    }
    
    extension View {
        
        func highlightShadow() -> some View {
            self
                .shadow(color: .white, radius: 10, x: 5, y: -10)
        }   
        
        func faintShadow() -> some View {
            self
                .shadow(color: .white, radius: 40)
        }
        
        func textShadow(_ darkMode: Bool) -> some View {
            
            self
                .shadow(color: darkMode ? .black : .clear, radius: darkMode ? 1 : 0)
        }
        
        func bigCapsuleStyle(_ background: Color) -> some View {
            self
                .font(.system(size: 30, weight: .bold))
                .foregroundStyle(.white)
                .padding(30)
                .background(background, in: Capsule())
        }
        
        func weatherOverlay(_ weatherCondition: WeatherCondition, _ darkMode: Bool) -> some View {
            
            var weatherIcon = ""
            
            switch weatherCondition {
                case .sunny:
                    weatherIcon = "sun.max.fill"
                case .rain:
                    weatherIcon = "cloud.rain.fill"
                case .snow:
                    weatherIcon = "snowflake"
            }
            return self
                .overlay(alignment: .topTrailing){
                    Image(systemName: weatherIcon)
                        .padding(10)
                        .background(darkMode ? .brown : .white, in: Circle())
                        .offset(x: 0, y: -20)
                }
        }
    }
    
    #Preview {
        ConditionalShadow()
    }
    
    

    enter image description here