Last active
August 31, 2023 07:45
-
-
Save adam-zethraeus/729bdb5b49bb2ff7a9da2323d8766161 to your computer and use it in GitHub Desktop.
Conditional tweaks to SwiftUI views without using AnyView
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The goal is to allow writing this code:
Instead of something like this...
...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 ofwrite this code. Some more obviously silly than others...
EitherView
avoids type-erasing the view by embedding its potentialcases 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:
How often does this actually matter?
Tough to say + hard to measure + it depends.
AnyView
has a purpose. This description of AnyView's valueis 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 throughoutyour app — seems prudent. And it's possible here.