import SwiftUI /// A view that can switch over a store of enum state and handle each case. /// /// An application may model parts of its state with enums. For example, app state may differ if a /// user is logged-in or not: /// /// enum AppState { /// case loggedIn(LoggedInState) /// case loggedOut(LoggedOutState) /// } /// /// In the view layer, a store on this state can switch over each case using a `SwitchStore` and /// a `CaseLet` view per case: /// /// struct AppView: View { /// let store: Store /// /// var body: some View { /// SwitchStore(self.store) { /// CaseLet(state: /AppState.loggedIn, action: AppAction.loggedIn) { loggedInStore in /// LoggedInView(store: loggedInStore) /// } /// CaseLet(state: /AppState.loggedOut, action: AppAction.loggedOut) { loggedOutStore in /// LoggedOutView(store: loggedOutStore) /// } /// } /// } /// } /// /// If a `SwitchStore` does not exhaustively handle every case with a corresponding `CaseLet` view, /// a debug breakpoint will be raised when an unhandled case is encountered. To fall back on a /// default view instead, introduce a `Default` view at the end of the `SwitchStore`: /// /// SwitchStore(self.store) { /// CaseLet(state: /MyState.first, action: MyAction.first, then: FirstView.init(store:)) /// CaseLet(state: /MyState.second, action: MyAction.second, then: SecondView.init(store:)) /// /// Default { /// Text("State is neither first nor second.") /// } /// } /// /// - See also: `Reducer.pullback`, a method that aids in transforming reducers that operate on each /// case of an enum into reducers that operate on the entire enum. /// public struct SwitchStore: View where Content: View { public let store: Store public let content: () -> Content init( store: Store, @ViewBuilder content: @escaping () -> Content ) { self.store = store self.content = content } public var body: some View { self.content() .environmentObject(StoreObservableObject(store: self.store)) } } /// A view that handles a specific case of enum state in a `SwitchStore`. public struct CaseLet: View where Content: View { @EnvironmentObject private var store: StoreObservableObject public let toLocalState: (GlobalState) -> LocalState? public let fromLocalAction: (LocalAction) -> GlobalAction public let content: (Store) -> Content /// Initializes a `CaseLet` view that computes content depending on if a store of enum state /// matches a particular case. /// /// - Parameters: /// - toLocalState: A case path that can extract a case of switch store state. /// - fromLocalAction: A function that can embed a case action in a switch store action. /// - content: A function that is given a store of the given case's state and returns a view /// that is visible only when the switch store's state matches. public init( state toLocalState: @escaping (GlobalState) -> LocalState?, action fromLocalAction: @escaping (LocalAction) -> GlobalAction, @ViewBuilder then content: @escaping (Store) -> Content ) { self.toLocalState = toLocalState self.fromLocalAction = fromLocalAction self.content = content } public var body: some View { IfLetStore( self.store.wrappedValue.scope( state: self.toLocalState, action: self.fromLocalAction ), then: self.content ) } } /// A view that covers any cases that aren't addressed in a `SwitchStore`. /// /// If you wish to use `SwitchStore` in a non-exhaustive manner (i.e. you do not want to provide /// a `CaseLet` for each case of the enum), then you must insert a `Default` view at the end of /// the `SwitchStore`'s body. public struct Default: View where Content: View { private let content: () -> Content /// Initializes a `Default` view that computes content depending on if a store of enum state does /// not match a particular case. /// /// - Parameter content: A function that returns a view that is visible only when the switch /// store's state does not match a preceding `CaseLet` view. public init(@ViewBuilder content: @escaping () -> Content) { self.content = content } public var body: some View { self.content() } } extension SwitchStore { public init( _ store: Store, @ViewBuilder content: @escaping () -> TupleView< ( CaseLet, Default ) > ) where Content == WithViewStore< State, Action, _ConditionalContent< CaseLet, Default > > { self.init(store: store) { let content = content().value return WithViewStore(store, removeDuplicates: { enumTag($0) == enumTag($1) }) { viewStore in if content.0.toLocalState(viewStore.state) != nil { content.0 } else { content.1 } } } } public init( _ store: Store, file: StaticString = #file, line: UInt = #line, @ViewBuilder content: @escaping () -> CaseLet ) where Content == WithViewStore< State, Action, _ConditionalContent< CaseLet, Default<_ExhaustivityCheckView> > > { self.init(store) { content() Default { _ExhaustivityCheckView(file: file, line: line) } } } public init( _ store: Store, @ViewBuilder content: @escaping () -> TupleView< ( CaseLet, CaseLet, Default ) > ) where Content == WithViewStore< State, Action, _ConditionalContent< _ConditionalContent< CaseLet, CaseLet >, Default > > { self.init(store: store) { let content = content().value return WithViewStore(store, removeDuplicates: { enumTag($0) == enumTag($1) }) { viewStore in if content.0.toLocalState(viewStore.state) != nil { content.0 } else if content.1.toLocalState(viewStore.state) != nil { content.1 } else { content.2 } } } } public init( _ store: Store, file: StaticString = #file, line: UInt = #line, @ViewBuilder content: @escaping () -> TupleView< ( CaseLet, CaseLet ) > ) where Content == WithViewStore< State, Action, _ConditionalContent< _ConditionalContent< CaseLet, CaseLet >, Default<_ExhaustivityCheckView> > > { let content = content() self.init(store) { content.value.0 content.value.1 Default { _ExhaustivityCheckView(file: file, line: line) } } } public init< State1, Action1, Content1, State2, Action2, Content2, State3, Action3, Content3, DefaultContent >( _ store: Store, @ViewBuilder content: @escaping () -> TupleView< ( CaseLet, CaseLet, CaseLet, Default ) > ) where Content == WithViewStore< State, Action, _ConditionalContent< _ConditionalContent< CaseLet, CaseLet >, _ConditionalContent< CaseLet, Default > > > { self.init(store: store) { let content = content().value return WithViewStore(store, removeDuplicates: { enumTag($0) == enumTag($1) }) { viewStore in if content.0.toLocalState(viewStore.state) != nil { content.0 } else if content.1.toLocalState(viewStore.state) != nil { content.1 } else if content.2.toLocalState(viewStore.state) != nil { content.2 } else { content.3 } } } } public init( _ store: Store, file: StaticString = #file, line: UInt = #line, @ViewBuilder content: @escaping () -> TupleView< ( CaseLet, CaseLet, CaseLet ) > ) where Content == WithViewStore< State, Action, _ConditionalContent< _ConditionalContent< CaseLet, CaseLet >, _ConditionalContent< CaseLet, Default<_ExhaustivityCheckView> > > > { let content = content() self.init(store) { content.value.0 content.value.1 content.value.2 Default { _ExhaustivityCheckView(file: file, line: line) } } } public init< State1, Action1, Content1, State2, Action2, Content2, State3, Action3, Content3, State4, Action4, Content4, DefaultContent >( _ store: Store, @ViewBuilder content: @escaping () -> TupleView< ( CaseLet, CaseLet, CaseLet, CaseLet, Default ) > ) where Content == WithViewStore< State, Action, _ConditionalContent< _ConditionalContent< _ConditionalContent< CaseLet, CaseLet >, _ConditionalContent< CaseLet, CaseLet > >, Default > > { self.init(store: store) { let content = content().value return WithViewStore(store, removeDuplicates: { enumTag($0) == enumTag($1) }) { viewStore in if content.0.toLocalState(viewStore.state) != nil { content.0 } else if content.1.toLocalState(viewStore.state) != nil { content.1 } else if content.2.toLocalState(viewStore.state) != nil { content.2 } else if content.3.toLocalState(viewStore.state) != nil { content.3 } else { content.4 } } } } public init< State1, Action1, Content1, State2, Action2, Content2, State3, Action3, Content3, State4, Action4, Content4 >( _ store: Store, file: StaticString = #file, line: UInt = #line, @ViewBuilder content: @escaping () -> TupleView< ( CaseLet, CaseLet, CaseLet, CaseLet ) > ) where Content == WithViewStore< State, Action, _ConditionalContent< _ConditionalContent< _ConditionalContent< CaseLet, CaseLet >, _ConditionalContent< CaseLet, CaseLet > >, Default<_ExhaustivityCheckView> > > { let content = content() self.init(store) { content.value.0 content.value.1 content.value.2 content.value.3 Default { _ExhaustivityCheckView(file: file, line: line) } } } public init< State1, Action1, Content1, State2, Action2, Content2, State3, Action3, Content3, State4, Action4, Content4, State5, Action5, Content5, DefaultContent >( _ store: Store, @ViewBuilder content: @escaping () -> TupleView< ( CaseLet, CaseLet, CaseLet, CaseLet, CaseLet, Default ) > ) where Content == WithViewStore< State, Action, _ConditionalContent< _ConditionalContent< _ConditionalContent< CaseLet, CaseLet >, _ConditionalContent< CaseLet, CaseLet > >, _ConditionalContent< CaseLet, Default > > > { self.init(store: store) { let content = content().value return WithViewStore(store, removeDuplicates: { enumTag($0) == enumTag($1) }) { viewStore in if content.0.toLocalState(viewStore.state) != nil { content.0 } else if content.1.toLocalState(viewStore.state) != nil { content.1 } else if content.2.toLocalState(viewStore.state) != nil { content.2 } else if content.3.toLocalState(viewStore.state) != nil { content.3 } else if content.4.toLocalState(viewStore.state) != nil { content.4 } else { content.5 } } } } public init< State1, Action1, Content1, State2, Action2, Content2, State3, Action3, Content3, State4, Action4, Content4, State5, Action5, Content5 >( _ store: Store, file: StaticString = #file, line: UInt = #line, @ViewBuilder content: @escaping () -> TupleView< ( CaseLet, CaseLet, CaseLet, CaseLet, CaseLet ) > ) where Content == WithViewStore< State, Action, _ConditionalContent< _ConditionalContent< _ConditionalContent< CaseLet, CaseLet >, _ConditionalContent< CaseLet, CaseLet > >, _ConditionalContent< CaseLet, Default<_ExhaustivityCheckView> > > > { let content = content() self.init(store) { content.value.0 content.value.1 content.value.2 content.value.3 content.value.4 Default { _ExhaustivityCheckView(file: file, line: line) } } } } public struct _ExhaustivityCheckView: View { @EnvironmentObject private var store: StoreObservableObject let file: StaticString let line: UInt public var body: some View { #if DEBUG let message = """ Warning: SwitchStore.body@\(self.file):\(self.line) "\(debugCaseOutput(self.store.wrappedValue.state.value))" was encountered by a \ "SwitchStore" that does not handle this case. Make sure that you exhaustively provide a "CaseLet" view for each case in "\(State.self)", \ or provide a "Default" view at the end of the "SwitchStore". """ return VStack(spacing: 17) { #if os(macOS) Text("⚠️") #else Image(systemName: "exclamationmark.triangle.fill") .font(.largeTitle) #endif Text(message) } .frame(maxWidth: .infinity, maxHeight: .infinity) .foregroundColor(.white) .padding() .background(Color.red.edgesIgnoringSafeArea(.all)) .onAppear { breakpoint( """ --- \(message) --- """ ) } #else EmptyView() #endif } } private class StoreObservableObject: ObservableObject { let wrappedValue: Store init(store: Store) { self.wrappedValue = store } } private func enumTag(_ `case`: Case) -> UInt32? { let metadataPtr = unsafeBitCast(type(of: `case`), to: UnsafeRawPointer.self) let kind = metadataPtr.load(as: Int.self) let isEnumOrOptional = kind == 0x201 || kind == 0x202 guard isEnumOrOptional else { return nil } let vwtPtr = (metadataPtr - MemoryLayout.size).load(as: UnsafeRawPointer.self) let vwt = vwtPtr.load(as: EnumValueWitnessTable.self) return withUnsafePointer(to: `case`) { vwt.getEnumTag($0, metadataPtr) } } private struct EnumValueWitnessTable { let f1, f2, f3, f4, f5, f6, f7, f8: UnsafeRawPointer let f9, f10: Int let f11, f12: UInt32 let getEnumTag: @convention(c) (UnsafeRawPointer, UnsafeRawPointer) -> UInt32 let f13, f14: UnsafeRawPointer }