Search code examples
bindingswiftuiconditional-operatorviewbuilder

How do I initialize a Bool in this SwiftUI @ViewBuilder based on an optional Binding<Image>?


My goal here is to use a ternary operator to add a shadow to my Image, but not add one if the View is using the default image.

This is my code:

import SwiftUI

struct ImageDisplay: View {
    @State private var defaultImage: Image = Image(systemName: "person.crop.circle.fill")
    private var defaultText: String = "Add picture"
    private var hasShadow: Bool = true
    private var text: String?
    private var image: Binding<Image>?
    
    public init(_ text: String, image: Binding<Image>) {
        self.init(
            text: .some(text),
            image: image
        )
    }
    
    public init(image: Binding<Image>) {
        self.init(
            text: nil,
            image: image
        )
    }
    
    public init() {
        self.init(
            text: nil,
            image: nil
        )
    }
    
    private init(text: String?, image: Binding<Image>?) {
        self.text = text
        self.image = image
    }
    
    private struct InternalImageDisplay: View {
        var text: String
        var hasShadow: Bool  /// NOT CERTAIN WHERE THIS BELONGS, OR IF IT GOES HERE ...
        @Binding var image: Image
        
        @ViewBuilder
        var body: some View {
                        
            VStack {
                image
                    .imageDisplayStyle()
                    .shadow(radius: hasShadow ? 4.0 : 0.0) /// HERE IS WHERE I'LL USE IT...
                
                Button(action: {
                    
                    //TODO: - Code to present image picker.
                    
                }, label: {
                    Text(text)
                })
            }
        }
    }
    
    var body: some View {
        InternalImageDisplay(
            text: text ?? defaultText,
            hasShadow: false,   /// THIS IS NOT WORKING IN ANY IMPLEMENTATION ...
            image: image ?? $defaultImage
        )
    }
}

I also have this extension to Image:

import SwiftUI

extension Image {
    func imageDisplayStyle() -> some View {
        return self
            .resizable()
            .scaledToFill()
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
            .aspectRatio(contentMode: .fit)
            .clipShape(Circle())
            .foregroundColor(.gray)
   }
}

I am attempting to get code that looks like this to work nicely and it's great , except for the shadow bool:

struct ContentView: View {
    
    @State private var image = Image("img1")
    
    @State private var defaultImage: Bool = true
    
    var body: some View {
        VStack (spacing: 48) {
            // no binding
            ImageDisplay()
            // binding
            ImageDisplay("Select image", image: $image)
            ImageDisplay(image: $image)
        }
        .frame(width: 190)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}



Solution

  • In your initializers, you can set hasShadow based on whether or not there's an image parameter being passed in.

    Then, you can pass that through to your InternalImageDisplay.

    I made the shadow a little more obvious just for testing purposes:

    
    struct ImageDisplay: View {
        @State private var defaultImage: Image = Image(systemName: "person.crop.circle.fill")
        private var defaultText: String = "Add picture"
        private var hasShadow: Bool = true
        private var text: String?
        private var image: Binding<Image>?
        
        public init(_ text: String, image: Binding<Image>) {
            self.init(
                text: text,
                image: image,
                hasShadow: true //<-- Here
            )
        }
        
        public init(image: Binding<Image>) {
            self.init(
                text: nil,
                image: image,
                hasShadow: true //<-- Here
            )
        }
        
        public init() {
            self.init(
                text: nil,
                image: nil,
                hasShadow: false //<-- Here
            )
        }
        
        private init(text: String?, image: Binding<Image>?, hasShadow : Bool) {  //<-- Here
            self.text = text
            self.image = image
            self.hasShadow = hasShadow  //<-- Here
        }
        
        private struct InternalImageDisplay: View {
            var text: String
            var hasShadow: Bool   //<-- This gets passed in as a prop
            @Binding var image: Image
            
            @ViewBuilder
            var body: some View {
                            
                VStack {
                    image
                        .imageDisplayStyle()
                        .shadow(color: Color.green, radius: hasShadow ? 10.0 : 0)  //<-- Here (now it's green for testing)
                    
                    Button(action: {
                        
                        //TODO: - Code to present image picker.
                        
                    }, label: {
                        Text(text)
                    })
                }
            }
        }
        
        var body: some View {
            InternalImageDisplay(
                text: text ?? defaultText,
                hasShadow: hasShadow,  //<-- Here
                image: image ?? $defaultImage
            )
        }
    }