Conditional property in SwiftUI
Asked Answered
T

7

25

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() {
        // ...
    }
}
Tweeny answered 12/8, 2019 at 19:43 Comment(0)
T
44

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)
}
Teneshatenesmus answered 17/7, 2020 at 22:7 Comment(3)
This is the best solution.Caldwell
Did some thing change? It says Cannot find type 'View' in scopeBeecham
@Beecham you're missing import SwiftUIMacromolecule
M
14

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.

Marilumarilyn answered 12/8, 2019 at 19:59 Comment(1)
The actual performance testing of AnyView does not reveal any negative implications when using it: medium.com/swlh/…Kinnie
R
14

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() {
        // ...
    }
}
Robertaroberto answered 12/8, 2019 at 20:3 Comment(1)
Note that by default, SwiftUI treats the static type of a view as its identifier for animations (explained by Joe Groff here). In your solution, the two subviews (with/without overlay) are therefore treated as unrelated views. If one applies an animation, it may apply differently in your solution than in mine. That's not necessarily wrong, just something to be aware of. It can be worked around using the .id modifier.Marilumarilyn
P
6

Use conditional modifiers including if - else expressions only if you have absolutely no choice.

The huge disadvantage of conditional modifiers is they create new views which can cause unexpected behavior for example they will break animations.

Almost all modifiers accept a nil value for no change so if possible use always something like

@State var overlay: Bool

var body: some View {
    Image(systemName: "photo")
        .resizable()
        .overlay(overlay ? Circle().foregroundColor(Color.red) : nil)
}
Plantar answered 20/4, 2022 at 17:17 Comment(0)
A
4

Here's an answer similar to what Kiran Jasvanee posted, but simplified:

struct ContentView: View {

    var body: some View {
        Text("Hello, World!")
            .modifier(ConditionalModifier(isBold: true))
    }
}

struct ConditionalModifier: ViewModifier {

    var isBold: Bool

    func body(content: Content) -> some View {
        Group {
            if self.isBold {
                content.font(.custom("HelveticaNeue-Bold", size: 14))
            }
            else{
                content.font(.custom("HelveticaNeue", size: 14))
            }
        }
    }
}

The difference is that there's no need to add this extension:

extension View {
    func conditionalView(_ value: Bool) -> some View {
        self.modifier(ConditionalModifier(isBold: value))
  }
}

In the ConditionalModifier struct, you could also eliminate the Group view and instead use the @ViewBuilder decorator, like so:

struct ConditionalModifier: ViewModifier {

    var isBold: Bool

    @ViewBuilder
    func body(content: Content) -> some View {
        // Group {
            if self.isBold {
                content.font(.custom("HelveticaNeue-Bold", size: 14))
            }
            else{
                content.font(.custom("HelveticaNeue", size: 14))
            }
        // }
    }
}

You must either use a Group view (or some other parent view) or the @ViewBuilder decorator if you want to conditionally display views.

Here's another example in which the the text of a button can be toggled between bold and not bold:

struct ContentView: View {

    @State private var makeBold = false

    var body: some View {

        Button(action: {
            self.makeBold.toggle()
        }, label: {
            Text("Tap me to Toggle Bold")
                .modifier(ConditionalModifier(isBold: makeBold))
        })

    }
}

struct ConditionalModifier: ViewModifier {

    var isBold: Bool

    func body(content: Content) -> some View {
        Group {
            if self.isBold {
                content.font(.custom("HelveticaNeue-Bold", size: 14))
            }
            else{
                content.font(.custom("HelveticaNeue", size: 14))
            }
        }
    }
}
Ardeha answered 16/4, 2020 at 15:14 Comment(1)
Conditional modifier on a view breaks the ability of SwiftUI to identify the view when it changes state during a redraw. This results in unexpected behavior including strange animation transitions, so please avoid doing it. Use ternaries instead.Brieta
P
0

I think the Preferred way to implement Conditional Properties is below. It works well if you have Animations applied.
I tried a few other ways, but it affects the animation I've applied.

You can add your condition in place of false I've kept .conditionalView(false).

Below, You can switch to true to check the 'Hello, World!' will be shown BOLD.

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
        .conditionalView(false)
        // or use:
        // .modifier(ConditionalModifier(isBold: false))

    }
}

extension View {
    func conditionalView(_ value: Bool) -> some View {
        self.modifier(ConditionalModifier(isBold: value))
  }
}

struct ConditionalModifier: ViewModifier {
    var isBold: Bool
    func body(content: Content) -> some View {
    Group {
        if self.isBold {
            content.font(.custom("HelveticaNeue-Bold", size: 14))
        }else{
            content.font(.custom("HelveticaNeue", size: 14))
        }
    }
  }
}
Pore answered 24/3, 2020 at 13:39 Comment(2)
There's no need for the conditionalView extension for View. Initialize the ConditionalModifier struct directly using .modifier(ConditionalModifier(isBold : viewIsBold))Ardeha
@PeterSchorn I think I got your point, would suggest you add your answer too.Pore
K
0

As easy as this

import SwiftUI

struct SomeView: View {
  let value: Double
  let maxValue: Double
  
  private let lineWidth: CGFloat = 1
  
  var body: some View {
    Circle()
      .strokeBorder(Color.yellow, lineWidth: lineWidth)
      .overlay(optionalOverlay)
  }
  
  private var progressEnd: CGFloat {
    CGFloat(value) / CGFloat(maxValue)
  }
  
  private var showProgress: Bool {
    value != maxValue
  }
  
  @ViewBuilder private var optionalOverlay: some View {
    if showProgress {
      Circle()
        .inset(by: lineWidth / 2)
        .trim(from: 0, to: progressEnd)
        .stroke(Color.blue, lineWidth: lineWidth)
        .rotationEffect(.init(degrees: 90))
    }
  }
}

struct SomeView_Previews: PreviewProvider {
  static var previews: some View {
    Group {
      SomeView(value: 40, maxValue: 100)
      
      SomeView(value: 100, maxValue: 100)
    }
      .previewLayout(.fixed(width: 100, height: 100))
  }
}
Kinnie answered 9/11, 2020 at 16:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.