import Combine import CustomDump import SwiftUI /// A structure that transforms a store into an observable view store in order to compute views from /// store state. public struct WithViewStore { private let content: (ViewStore) -> Content #if DEBUG private let file: StaticString private let line: UInt private var prefix: String? private var previousState: (State) -> State? #endif @ObservedObject private var viewStore: ViewStore fileprivate init( store: Store, removeDuplicates isDuplicate: @escaping (State, State) -> Bool, file: StaticString = #fileID, line: UInt = #line, content: @escaping (ViewStore) -> Content ) { self.content = content #if DEBUG self.file = file self.line = line var previousState: State? = nil self.previousState = { currentState in defer { previousState = currentState } return previousState } #endif self.viewStore = ViewStore(store, removeDuplicates: isDuplicate) } /// Prints debug information to the console whenever the view is computed. /// /// - Parameter prefix: A string with which to prefix all debug messages. /// - Returns: A structure that prints debug messages for all computations. public func debug(_ prefix: String = "") -> Self { var view = self #if DEBUG view.prefix = prefix #endif return view } fileprivate var _body: Content { #if DEBUG if let prefix = self.prefix { var stateDump = "" customDump(self.viewStore.state, to: &stateDump, indent: 2) let difference = self.previousState(self.viewStore.state) .map { diff($0, self.viewStore.state).map { "(Changed state)\n\($0)" } ?? "(No difference in state detected)" } ?? "(Initial state)\n\(stateDump)" func typeName(_ type: Any.Type) -> String { var name = String(reflecting: type) if let index = name.firstIndex(of: ".") { name.removeSubrange(...index) } return name } print( """ \(prefix.isEmpty ? "" : "\(prefix): ")\ WithViewStore<\(typeName(State.self)), \(typeName(Action.self)), _>\ @\(self.file):\(self.line) \(difference) """ ) } #endif return self.content(ViewStore(self.viewStore)) } } // MARK: - View extension WithViewStore: View where Content: View { /// Initializes a structure that transforms a store into an observable view store in order to /// compute views from store state. /// /// - Parameters: /// - store: A store. /// - isDuplicate: A function to determine when two `State` values are equal. When values are /// equal, repeat view computations are removed, /// - content: A function that can generate content from a view store. public init( _ store: Store, removeDuplicates isDuplicate: @escaping (State, State) -> Bool, file: StaticString = #fileID, line: UInt = #line, @ViewBuilder content: @escaping (ViewStore) -> Content ) { self.init( store: store, removeDuplicates: isDuplicate, file: file, line: line, content: content ) } public var body: Content { self._body } } extension WithViewStore where State: Equatable, Content: View { /// Initializes a structure that transforms a store into an observable view store in order to /// compute views from equatable store state. /// /// - Parameters: /// - store: A store of equatable state. /// - content: A function that can generate content from a view store. public init( _ store: Store, file: StaticString = #fileID, line: UInt = #line, @ViewBuilder content: @escaping (ViewStore) -> Content ) { self.init(store, removeDuplicates: ==, file: file, line: line, content: content) } } extension WithViewStore where State == Void, Content: View { /// Initializes a structure that transforms a store into an observable view store in order to /// compute views from void store state. /// /// - Parameters: /// - store: A store of equatable state. /// - content: A function that can generate content from a view store. public init( _ store: Store, file: StaticString = #fileID, line: UInt = #line, @ViewBuilder content: @escaping (ViewStore) -> Content ) { self.init(store, removeDuplicates: ==, file: file, line: line, content: content) } } extension WithViewStore: DynamicViewContent where State: Collection, Content: DynamicViewContent { public typealias Data = State public var data: State { self.viewStore.state } } // MARK: - AccessibilityRotorContent @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) extension WithViewStore: AccessibilityRotorContent where Content: AccessibilityRotorContent { /// Initializes a structure that transforms a store into an observable view store in order to /// compute accessibility rotor content from store state. /// /// - Parameters: /// - store: A store. /// - isDuplicate: A function to determine when two `State` values are equal. When values are /// equal, repeat view computations are removed, /// - content: A function that can generate content from a view store. public init( _ store: Store, removeDuplicates isDuplicate: @escaping (State, State) -> Bool, file: StaticString = #fileID, line: UInt = #line, @AccessibilityRotorContentBuilder content: @escaping (ViewStore) -> Content ) { self.init( store: store, removeDuplicates: isDuplicate, file: file, line: line, content: content ) } public var body: some AccessibilityRotorContent { self._body } } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) extension WithViewStore where State: Equatable, Content: AccessibilityRotorContent { /// Initializes a structure that transforms a store into an observable view store in order to /// compute accessibility rotor content from equatable store state. /// /// - Parameters: /// - store: A store of equatable state. /// - content: A function that can generate content from a view store. public init( _ store: Store, file: StaticString = #fileID, line: UInt = #line, @AccessibilityRotorContentBuilder content: @escaping (ViewStore) -> Content ) { self.init(store, removeDuplicates: ==, file: file, line: line, content: content) } } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) extension WithViewStore where State == Void, Content: AccessibilityRotorContent { /// Initializes a structure that transforms a store into an observable view store in order to /// compute accessibility rotor content from void store state. /// /// - Parameters: /// - store: A store of equatable state. /// - content: A function that can generate content from a view store. public init( _ store: Store, file: StaticString = #fileID, line: UInt = #line, @AccessibilityRotorContentBuilder content: @escaping (ViewStore) -> Content ) { self.init(store, removeDuplicates: ==, file: file, line: line, content: content) } } // MARK: - Commands @available(iOS 14.0, macOS 11.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) extension WithViewStore: Commands where Content: Commands { /// Initializes a structure that transforms a store into an observable view store in order to /// compute commands from store state. /// /// - Parameters: /// - store: A store. /// - isDuplicate: A function to determine when two `State` values are equal. When values are /// equal, repeat view computations are removed, /// - content: A function that can generate content from a view store. public init( _ store: Store, removeDuplicates isDuplicate: @escaping (State, State) -> Bool, file: StaticString = #fileID, line: UInt = #line, @CommandsBuilder content: @escaping (ViewStore) -> Content ) { self.init( store: store, removeDuplicates: isDuplicate, file: file, line: line, content: content ) } public var body: some Commands { self._body } } @available(iOS 14.0, macOS 11.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) extension WithViewStore where State: Equatable, Content: Commands { /// Initializes a structure that transforms a store into an observable view store in order to /// compute commands from equatable store state. /// /// - Parameters: /// - store: A store of equatable state. /// - content: A function that can generate content from a view store. public init( _ store: Store, file: StaticString = #fileID, line: UInt = #line, @CommandsBuilder content: @escaping (ViewStore) -> Content ) { self.init(store, removeDuplicates: ==, file: file, line: line, content: content) } } @available(iOS 14.0, macOS 11.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) extension WithViewStore where State == Void, Content: Commands { /// Initializes a structure that transforms a store into an observable view store in order to /// compute commands from void store state. /// /// - Parameters: /// - store: A store of equatable state. /// - content: A function that can generate content from a view store. public init( _ store: Store, file: StaticString = #fileID, line: UInt = #line, @CommandsBuilder content: @escaping (ViewStore) -> Content ) { self.init(store, removeDuplicates: ==, file: file, line: line, content: content) } } // MARK: - Scene @available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) extension WithViewStore: Scene where Content: Scene { /// Initializes a structure that transforms a store into an observable view store in order to /// compute scenes from store state. /// /// - Parameters: /// - store: A store. /// - isDuplicate: A function to determine when two `State` values are equal. When values are /// equal, repeat view computations are removed, /// - content: A function that can generate content from a view store. public init( _ store: Store, removeDuplicates isDuplicate: @escaping (State, State) -> Bool, file: StaticString = #fileID, line: UInt = #line, @SceneBuilder content: @escaping (ViewStore) -> Content ) { self.init( store: store, removeDuplicates: isDuplicate, file: file, line: line, content: content ) } public var body: Content { self._body } } @available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) extension WithViewStore where State: Equatable, Content: Scene { /// Initializes a structure that transforms a store into an observable view store in order to /// compute scenes from equatable store state. /// /// - Parameters: /// - store: A store of equatable state. /// - content: A function that can generate content from a view store. public init( _ store: Store, file: StaticString = #fileID, line: UInt = #line, @SceneBuilder content: @escaping (ViewStore) -> Content ) { self.init(store, removeDuplicates: ==, file: file, line: line, content: content) } } @available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) extension WithViewStore where State == Void, Content: Scene { /// Initializes a structure that transforms a store into an observable view store in order to /// compute scenes from void store state. /// /// - Parameters: /// - store: A store of equatable state. /// - content: A function that can generate content from a view store. public init( _ store: Store, file: StaticString = #fileID, line: UInt = #line, @SceneBuilder content: @escaping (ViewStore) -> Content ) { self.init(store, removeDuplicates: ==, file: file, line: line, content: content) } } // MARK: - ToolbarContent @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension WithViewStore: ToolbarContent where Content: ToolbarContent { /// Initializes a structure that transforms a store into an observable view store in order to /// compute toolbar content from store state. /// /// - Parameters: /// - store: A store. /// - isDuplicate: A function to determine when two `State` values are equal. When values are /// equal, repeat view computations are removed, /// - content: A function that can generate content from a view store. public init( _ store: Store, removeDuplicates isDuplicate: @escaping (State, State) -> Bool, file: StaticString = #fileID, line: UInt = #line, @ToolbarContentBuilder content: @escaping (ViewStore) -> Content ) { self.init( store: store, removeDuplicates: isDuplicate, file: file, line: line, content: content ) } public var body: some ToolbarContent { self._body } } @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension WithViewStore where State: Equatable, Content: ToolbarContent { /// Initializes a structure that transforms a store into an observable view store in order to /// compute toolbar content from equatable store state. /// /// - Parameters: /// - store: A store of equatable state. /// - content: A function that can generate content from a view store. public init( _ store: Store, file: StaticString = #fileID, line: UInt = #line, @ToolbarContentBuilder content: @escaping (ViewStore) -> Content ) { self.init(store, removeDuplicates: ==, file: file, line: line, content: content) } } @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension WithViewStore where State == Void, Content: ToolbarContent { /// Initializes a structure that transforms a store into an observable view store in order to /// compute toolbar content from void store state. /// /// - Parameters: /// - store: A store of equatable state. /// - content: A function that can generate content from a view store. public init( _ store: Store, file: StaticString = #fileID, line: UInt = #line, @ToolbarContentBuilder content: @escaping (ViewStore) -> Content ) { self.init(store, removeDuplicates: ==, file: file, line: line, content: content) } }