Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Conditional property in SwiftUI

How can I add an additional property based on a condition?

With my code below I get the error:

Cannot assign value of type 'some View' (result of 'Self.overlay(_:alignment:)') to type 'some View' (result of 'Self.onTapGesture(count:perform:)')

import SwiftUI

struct ConditionalProperty: View {
    @State var overlay: Bool
    var body: some View {
        var view = Image(systemName: "photo")
            .resizable()
            .onTapGesture(count: 2, perform: self.tap)
        if self.overlay {
            view = view.overlay(Circle().foregroundColor(Color.red))
        }
        return view
    }
    
    func tap() {
        // ...
    }
}
like image 551
Ben Keil Avatar asked Aug 12 '19 19:08

Ben Keil


3 Answers

Thanks to this post I found an option with a very simple interface:

Text("Hello")
    .if(shouldBeRed) { $0.foregroundColor(.red) }

Enabled by this extension:

extension View {
    @ViewBuilder
    func `if`<Transform: View>(_ condition: Bool, transform: (Self) -> Transform) -> some View {
        if condition { transform(self) }
        else { self }
    }
}

Here is a great blog post with additional tips for conditional modifiers: https://fivestars.blog/swiftui/conditional-modifiers.html

Warning: Conditional view modifiers come with some caveats, and you may want to reconsider using the code above after reading this: https://www.objc.io/blog/2021/08/24/conditional-view-modifiers/

Personally I would choose to use this approach in answer to OP in order to avoid the potential issues mentioned in that article:

var body: some View {
    Image(systemName: "photo")
        .resizable()
        .onTapGesture(count: 2, perform: self.tap)
        .overlay(self.overlay ? Circle().foregroundColor(Color.red) : nil)
}
like image 74
ccwasden Avatar answered Oct 19 '22 20:10

ccwasden


In SwiftUI terminology, you're not adding a property. You're adding a modifier.

The problem here is that, in SwiftUI, every modifier returns a type that depends on what it's modifying.

var view = Image(systemName: "photo")
    .resizable()
    .onTapGesture(count: 2, perform: self.tap)

// view has some deduced type like GestureModifier<SizeModifier<Image>>

if self.overlay {
    let newView = view.overlay(Circle().foregroundColor(Color.red))
    // newView has a different type than view, something like
    // OverlayModifier<GestureModifier<SizeModifier<Image>>>
}

Since newView has a different type than view, you can't just assign view = newView.

One way to solve this is to always use the overlay modifier, but to make the overlay transparent when you don't want it visible:

var body: some View {
    return Image(systemName: "photo")
        .resizable()
        .onTapGesture(count: 2, perform: self.tap)
        .overlay(Circle().foregroundColor(overlay ? .red : .clear))
}

Another way to handle it is to use the type eraser AnyView:

var body: some View {
    let view = Image(systemName: "photo")
        .resizable()
        .onTapGesture(count: 2, perform: self.tap)
    if overlay {
        return AnyView(view.overlay(Circle().foregroundColor(.red)))
    } else {
        return AnyView(view)
    }
}

The recommendations I have seen from Apple engineers are to avoid using AnyView because is less efficient than using fully typed views. For example, this tweet and this tweet.

like image 14
rob mayoff Avatar answered Oct 19 '22 19:10

rob mayoff


Rob Mayoff already explained pretty well why this behavior appears and proposes two solutions (Transparant overlays or using AnyView). A third more elegant way is to use _ConditionalContent.

A simple way to create _ConditionalContent is by writing if else statements inside a group or stack. Here is an example using a group:

import SwiftUI

struct ConditionalProperty: View {
    @State var overlay: Bool
    var body: some View {
        Group {
            if overlay {
                base.overlay(Circle().foregroundColor(Color.red))
            } else {
                base
            }
        }
    }
    
    var base: some View {
        Image("photo")
            .resizable()
            .onTapGesture(count: 2, perform: self.tap)
    }

    func tap() {
        // ...
    }
}
like image 14
Damiaan Dufaux Avatar answered Oct 19 '22 20:10

Damiaan Dufaux