Skip to content

Instantly share code, notes, and snippets.

@adam-zethraeus
Last active August 31, 2023 07:45
Show Gist options
  • Save adam-zethraeus/729bdb5b49bb2ff7a9da2323d8766161 to your computer and use it in GitHub Desktop.
Save adam-zethraeus/729bdb5b49bb2ff7a9da2323d8766161 to your computer and use it in GitHub Desktop.
Conditional tweaks to SwiftUI views without using AnyView
import SwiftUI
extension View {
/// Conditionally make a change to a view in a modifier chain
///
/// - Parameter if: the condition required to make the change
/// - Parameter modify: the change to make
public func conditionally(if condition: Bool, @ViewBuilder _ modify: @escaping (_ content: Self) -> some View) -> some View {
EitherView(condition: condition, input: self, modify: modify)
}
/// Make a change to a view in a modifier chain iff an optional value is available
///
/// - Parameter ifLet: the value required for the change to be made
/// - Parameter modify: the change to make
public func conditionally<V: View, P>(ifLet payload: P?, @ViewBuilder _ modify: @escaping (_ content: Self, _ value: P) -> V) -> some View {
EitherView(input: self, payload: payload, build: modify)
}
}
enum EitherView<V1: View, V2: View>: View {
init(condition: Bool, input: V1, @ViewBuilder modify: (V1)->V2) {
if condition {
self = .v1(input)
} else {
self = .v2(modify(input))
}
}
init<P>(input: V1, payload: P?, @ViewBuilder build: (V1, P)->V2) {
if let payload {
self = .v2(build(input, payload))
} else {
self = .v1(input)
}
}
case v1(V1)
case v2(V2)
var body: some View {
switch self {
case .v1(let v1): v1
case .v2(let v2): v2
}
}
}
@adam-zethraeus
Copy link
Author

The goal is to allow writing this code:

struct ConciseView: View {
  @State var isBad: Bool
  @State var isLoud: Bool

  var body: some View {
    MyLabel()
		.conditionally(if: isLoud) { $0.fontWeight(.bold) }
    .conditionally(if: isBad) { $0.background(.red) }
  }
}

Instead of something like this...

struct VerboseView: View {
  @State var isBad: Bool
  @State var isLoud: Bool

  var body: some View {
    if isBad && isLoud {
      MyLabel()
        .fontWeight(.bold)
        .background(.red)
    } else if !isBad && isLoud {
			MyLabel()
        .fontWeight(.bold)
		} else if isBad && !isLoud {
			MyLabel()
			  .background(.red)
		} else {
			MyLabel()
		}
  }
}

...and to do so while maintaining the compiler's knowledge of the
view's structural information (i.e. it'a type) — instead of erasing it with an AnyView
wrapper.

AnyView would otherwise open the door to lots of ways of
write this code. Some more obviously silly than others...

struct ConciseView: View {
  @State var isBad: Bool
  @State var isLoud: Bool

  var body: some View {
    var v = AnyView(MyLabel())
    if isLoud {
      v = AnyView(v.fontWeight(.bold))
    }
    if isBad {
      v = AnyView(v.background(.red))
    }
    return v // return disables ViewBuilder behavior
  }
}

EitherView avoids type-erasing the view by embedding its potential
cases from each side of the branch in its generic parameters.
It maintains that type/structure even across branch changes.

This means SwiftUI can work with (this part of) our view hierarchy without
having to resort to runtime dynamic lookups to determine that views have changed.

This in turn can sometimes yeild performance improvements.

per the docs:

An AnyView allows changing the type of view used in a given view hierarchy.
Whenever the type of view used with an AnyView changes, the old hierarchy is
destroyed and a new hierarchy is created for the new type.

How often does this actually matter?
Tough to say + hard to measure + it depends.

AnyView has a purpose. This description of AnyView's value
is solidly worth a read.
Nevertheless, given some sufficiently large amount of forced-dynamism casually sprinkled through
a view hierarchy we'd eventually encounter substantive performance issues.

Avoiding AnyView when possible — and especially in utility methods that could end up being used liberally throughout
your app — seems prudent. And it's possible here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment