diff --git a/Sources/Orbit/Components/Collapse.swift b/Sources/Orbit/Components/Collapse.swift index 9f327fb30d6..55e599dd85f 100644 --- a/Sources/Orbit/Components/Collapse.swift +++ b/Sources/Orbit/Components/Collapse.swift @@ -15,12 +15,12 @@ public struct Collapse: View { private let headerVerticalPadding: CGFloat private let showSeparator: Bool - private let isExpanded: Binding? + private let isExpanded: OptionalBindingSource @ViewBuilder private let content: Content @ViewBuilder private let header: Header public var body: some View { - BindingSource(isExpanded, fallbackInitialValue: false) { $isExpanded in + OptionalBinding(isExpanded) { $isExpanded in VStack(alignment: .leading, spacing: 0) { SwiftUI.Button { withAnimation(.easeInOut(duration: 0.2)) { @@ -71,7 +71,7 @@ public extension Collapse { self.headerVerticalPadding = 0 self.content = content() self.showSeparator = showSeparator - self.isExpanded = isExpanded + self.isExpanded = .binding(isExpanded) } /// Creates Orbit ``Collapse`` component. @@ -80,7 +80,7 @@ public extension Collapse { self.headerVerticalPadding = 0 self.content = content() self.showSeparator = showSeparator - self.isExpanded = nil + self.isExpanded = .state(false) } } @@ -92,7 +92,7 @@ public extension Collapse where Header == Text { self.headerVerticalPadding = .small self.content = content() self.showSeparator = showSeparator - self.isExpanded = isExpanded + self.isExpanded = .binding(isExpanded) } /// Creates Orbit ``Collapse`` component. @@ -101,7 +101,7 @@ public extension Collapse where Header == Text { self.headerVerticalPadding = .small self.content = content() self.showSeparator = showSeparator - self.isExpanded = nil + self.isExpanded = .state(false) } } @@ -144,6 +144,9 @@ struct CollapsePreviews: PreviewProvider { headerPlaceholder } } + Collapse("Toggle with internal state") { + contentPlaceholder + } } .padding(.medium) } diff --git a/Sources/Orbit/Orbit.docc/Uncategorized.md b/Sources/Orbit/Orbit.docc/Uncategorized.md index fbbb676d9a5..3d8bb1487af 100644 --- a/Sources/Orbit/Orbit.docc/Uncategorized.md +++ b/Sources/Orbit/Orbit.docc/Uncategorized.md @@ -68,3 +68,5 @@ Supporting types and components under development. - ``IDPreferenceKey`` - ``IDPreference`` - ``HorizontalScrollViewProxy`` +- ``OptionalBinding`` +- ``OptionalBindingSource`` diff --git a/Sources/Orbit/Support/BindingSource.swift b/Sources/Orbit/Support/BindingSource.swift index 448b68df955..83beee687fd 100644 --- a/Sources/Orbit/Support/BindingSource.swift +++ b/Sources/Orbit/Support/BindingSource.swift @@ -1,30 +1,43 @@ import SwiftUI -/// A view that provides a binding to its content. +/// A binding source for the ``OptionalBinding``. +public enum OptionalBindingSource { + case binding(Binding) + case state(Value) +} + +/// A view that provides either a binding to its content or an internal state, based on provided ``OptionalBindingSource`` value. /// -/// This binding can either be supplied, in which case it is used directly, -/// or one is derived from internal state (starting with `defaultValue`). +/// The binding can either be supplied, in which case it is used directly, +/// or one is derived from internal state. /// -/// This is is useful for components that can manage their own state, -/// but we also want to make it possible for that state to be driven -/// from the outside if a binding is passed. -struct BindingSource: View { - - let outer: Binding? - @State var inner: Value +/// This is is useful for components that need to manage their own state, +/// but also allow that state to be overridden +/// using the binding provided from the outside. +public struct OptionalBinding: View { + + let binding: Binding? + @State var state: Value let content: (Binding) -> Content - var body: some View { - content(outer ?? $inner) + public var body: some View { + content(binding ?? $state) } - init( - _ binding: Binding?, - fallbackInitialValue: Value, + /// Create a view with either a binding to its content or an internal state. + public init( + _ source: OptionalBindingSource, @ViewBuilder content: @escaping (Binding) -> Content ) { - self.outer = binding - self._inner = State(wrappedValue: fallbackInitialValue) + switch source { + case .binding(let binding): + self.binding = binding + self._state = .init(wrappedValue: binding.wrappedValue) + case .state(let value): + self.binding = nil + self._state = State(wrappedValue: value) + } + self.content = content } }