Skip to content

Instantly share code, notes, and snippets.

@IanKeen
Last active June 26, 2024 07:38
Show Gist options
  • Save IanKeen/31fa4b36be507e649a027fda5afaf17a to your computer and use it in GitHub Desktop.
Save IanKeen/31fa4b36be507e649a027fda5afaf17a to your computer and use it in GitHub Desktop.
SwiftUI: Conditional views from optional bindings with required constraints
struct RootView: View {
@State private var state: AppState = .init()
var body: some View {
// if more or less than `1` if the `If`s below succeed then an error view will be shown instead
Require(1) {
// `If` only shows it's content if the provided Binding<T?> can be unwrapped to Binding<T>
If(binding: $state.signedOut) { binding in
SignedOutView(state: binding)
}
If(binding: $state.signedInt) { binding in
SignedInView(state: binding)
}
}
}
}
public struct Require<Content: View>: View {
@StateObject private var registrar: ViewRegistrar
@ViewBuilder private var content: () -> Content
public init(_ value: Int, @ViewBuilder content: @escaping () -> Content) {
self._registrar = .init(wrappedValue: ViewRegistrar(limit: value))
self.content = content
}
public var body: some View {
ZStack {
registrar.errorView()
content()
.opacity(registrar.isValid() ? 1 : 0)
}
.environmentObject(registrar)
}
}
public struct If<Content: View, T>: View {
@EnvironmentObject private var registrar: ViewRegistrar
@State private var id: AnyHashable
@Binding private var binding: T?
@ViewBuilder private var content: (Binding<T>) -> Content
public init(id: AnyHashable = UUID(), binding: Binding<T?>, @ViewBuilder content: @escaping (Binding<T>) -> Content) {
self._id = .init(wrappedValue: id)
self._binding = binding
self.content = content
}
public var body: some View {
Binding(_binding).map { content($0).onAppear { registrar.active(id) } }
}
}
private class ViewRegistrar: ObservableObject {
@Published private var active: [AnyHashable] = []
private let limit: Int
init(limit: Int) {
self.limit = limit
}
func active(_ value: AnyHashable) {
active.append(value)
}
func isValid() -> Bool {
return active.count == limit
}
func errorView() -> some View {
VStack {
Text("Invalid State!")
.font(.title)
Text("\(limit) view(s) are required, but \(active.count) are active")
if !active.isEmpty {
Text("Active Views:")
.font(.title3)
.padding(.top, 16)
VStack(alignment: .leading) {
ForEach(active, id: \.self) { item in
Text(verbatim: "- \(item)")
}
}
}
}
.opacity(isValid() ? 0 : 1)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment