@_spi(Reflection) import CasePaths import SwiftUI /// A view that observes when enum state held in a store changes cases, and provides stores to /// ``CaseLet`` views. /// /// An application may model parts of its state with enums. For example, app state may differ if a /// user is logged-in or not: /// /// ```swift /// @Reducer /// struct AppFeature { /// enum State { /// 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: /// /// ```swift /// struct AppView: View { /// let store: StoreOf /// /// var body: some View { /// SwitchStore(self.store) { state in /// switch state { /// case .loggedIn: /// CaseLet( /// /AppFeature.State.loggedIn, action: AppFeature.Action.loggedIn /// ) { loggedInStore in /// LoggedInView(store: loggedInStore) /// } /// case .loggedOut: /// CaseLet( /// /AppFeature.State.loggedOut, action: AppFeature.Action.loggedOut /// ) { loggedOutStore in /// LoggedOutView(store: loggedOutStore) /// } /// } /// } /// } /// } /// ``` /// /// > Important: The `SwitchStore` view builder is only evaluated when the case of state passed to /// > it changes. As such, you should not rely on this value for anything other than checking the /// > current case, _e.g._ by switching on it and routing to an appropriate `CaseLet`. /// /// See ``Reducer/ifCaseLet(_:action:then:fileID:filePath:line:column:)-7sg8d`` and /// ``Scope/init(state:action:child:fileID:filePath:line:column:)-9g44g`` for embedding reducers /// that operate on each case of an enum in reducers that operate on the entire enum. @available( iOS, deprecated: 9999, message: "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" ) @available( macOS, deprecated: 9999, message: "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" ) @available( tvOS, deprecated: 9999, message: "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" ) @available( watchOS, deprecated: 9999, message: "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" ) public struct SwitchStore: View { public let store: Store public let content: (State) -> Content public init( _ store: Store, @ViewBuilder content: @escaping (_ initialState: State) -> Content ) { self.store = store self.content = content } public var body: some View { WithViewStore( self.store, observe: { $0 }, removeDuplicates: { enumTag($0) == enumTag($1) } ) { viewStore in self.content(viewStore.state) .environmentObject(StoreObservableObject(store: self.store)) } } } /// A view that handles a specific case of enum state in a ``SwitchStore``. @available( iOS, deprecated: 9999, message: "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" ) @available( macOS, deprecated: 9999, message: "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" ) @available( tvOS, deprecated: 9999, message: "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" ) @available( watchOS, deprecated: 9999, message: "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" ) public struct CaseLet: View { public let toCaseState: (EnumState) -> CaseState? public let fromCaseAction: (CaseAction) -> EnumAction public let content: (Store) -> Content private let fileID: StaticString private let filePath: StaticString private let line: UInt private let column: UInt @EnvironmentObject private var store: StoreObservableObject /// Initializes a ``CaseLet`` view that computes content depending on if a store of enum state /// matches a particular case. /// /// - Parameters: /// - toCaseState: A function that can extract a case of switch store state, which can be /// specified using case path literal syntax, _e.g._ `/State.case`. /// - fromCaseAction: 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. /// - fileID: The fileID. /// - filePath: The filePath. /// - line: The line. /// - column: The column. public init( _ toCaseState: @escaping (EnumState) -> CaseState?, action fromCaseAction: @escaping (CaseAction) -> EnumAction, @ViewBuilder then content: @escaping (_ store: Store) -> Content, fileID: StaticString = #fileID, filePath: StaticString = #filePath, line: UInt = #line, column: UInt = #column ) { self.toCaseState = toCaseState self.fromCaseAction = fromCaseAction self.content = content self.fileID = fileID self.filePath = filePath self.line = line self.column = column } public var body: some View { IfLetStore( self.store.wrappedValue._scope(state: self.toCaseState, action: self.fromCaseAction), then: self.content, else: { _CaseLetMismatchView( fileID: self.fileID, filePath: self.filePath, line: self.line, column: self.column ) } ) } } extension CaseLet where EnumAction == CaseAction { /// Initializes a ``CaseLet`` view that computes content depending on if a store of enum state /// matches a particular case. /// /// - Parameters: /// - toCaseState: A function that can extract a case of switch store state, which can be /// specified using case path literal syntax, _e.g._ `/State.case`. /// - 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 toCaseState: @escaping (EnumState) -> CaseState?, @ViewBuilder then content: @escaping (_ store: Store) -> Content ) { self.init( toCaseState, action: { $0 }, then: content ) } } public struct _CaseLetMismatchView: View { @EnvironmentObject private var store: StoreObservableObject let fileID: StaticString let filePath: StaticString let line: UInt let column: UInt public var body: some View { #if DEBUG let message = """ Warning: A "CaseLet" at "\(self.fileID):\(self.line)" was encountered when state was set \ to another case: \(debugCaseOutput(self.store.wrappedValue.withState { $0 })) This usually happens when there is a mismatch between the case being switched on and the \ "CaseLet" view being rendered. For example, if ".screenA" is being switched on, but the "CaseLet" view is pointed to \ ".screenB": case .screenA: CaseLet( /State.screenB, action: Action.screenB ) { /* ... */ } Look out for typos to ensure that these two cases align. """ 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 { reportIssue(message, fileID: fileID, filePath: filePath, line: line, column: column) } #else return EmptyView() #endif } } private final class StoreObservableObject: ObservableObject { let wrappedValue: Store init(store: Store) { self.wrappedValue = store } } private func enumTag(_ `case`: Case) -> UInt32? { EnumMetadata(Case.self)?.tag(of: `case`) }