|
| 1 | +import Combine |
| 2 | +import CombineRex |
| 3 | +import PrimeModal |
| 4 | +import SwiftRex |
| 5 | +import SwiftUI |
| 6 | +import Utils |
| 7 | + |
| 8 | +public enum CounterAction: Equatable { |
| 9 | + case decrTapped |
| 10 | + case incrTapped |
| 11 | + case nthPrimeButtonTapped |
| 12 | + case nthPrimeResponse(Int?) |
| 13 | + case alertDismissButtonTapped |
| 14 | + case isPrimeButtonTapped |
| 15 | + case primeModalDismissed |
| 16 | +} |
| 17 | + |
| 18 | +public typealias CounterState = ( |
| 19 | + alertNthPrime: PrimeAlert?, |
| 20 | + count: Int, |
| 21 | + isNthPrimeButtonDisabled: Bool, |
| 22 | + isPrimeModalShown: Bool |
| 23 | +) |
| 24 | + |
| 25 | +public final class CounterMiddleware: Middleware { |
| 26 | + public typealias InputActionType = CounterAction |
| 27 | + public typealias OutputActionType = CounterAction |
| 28 | + public typealias StateType = Int |
| 29 | + |
| 30 | + private var output: AnyActionHandler<OutputActionType>? |
| 31 | + private var getState: GetState<StateType>! |
| 32 | + private var cancellables = Set<AnyCancellable>() |
| 33 | + |
| 34 | + public init() { |
| 35 | + } |
| 36 | + |
| 37 | + public func receiveContext(getState: @escaping GetState<StateType>, output: AnyActionHandler<OutputActionType>) { |
| 38 | + self.getState = getState |
| 39 | + self.output = output |
| 40 | + } |
| 41 | + |
| 42 | + public func handle(action: InputActionType, from dispatcher: ActionSource, afterReducer: inout AfterReducer) { |
| 43 | + switch action { |
| 44 | + case .nthPrimeButtonTapped: |
| 45 | + afterReducer = .do { |
| 46 | + Current.nthPrime(self.getState()) |
| 47 | + .map(CounterAction.nthPrimeResponse) |
| 48 | + .receive(on: DispatchQueue.main) |
| 49 | + .sink { [weak self] action in |
| 50 | + self?.output?.dispatch(action) |
| 51 | + }.store(in: &self.cancellables) |
| 52 | + } |
| 53 | + |
| 54 | + case .decrTapped, |
| 55 | + .incrTapped, |
| 56 | + .nthPrimeResponse, |
| 57 | + .alertDismissButtonTapped, |
| 58 | + .isPrimeButtonTapped, |
| 59 | + .primeModalDismissed: |
| 60 | + break |
| 61 | + } |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +let counterReducer = Reducer<CounterAction, CounterState> { action, state in |
| 66 | + var state = state |
| 67 | + switch action { |
| 68 | + case .decrTapped: |
| 69 | + state.count -= 1 |
| 70 | + return state |
| 71 | + case .incrTapped: |
| 72 | + state.count += 1 |
| 73 | + return state |
| 74 | + case .nthPrimeButtonTapped: |
| 75 | + state.isNthPrimeButtonDisabled = true |
| 76 | + return state |
| 77 | + case let .nthPrimeResponse(prime): |
| 78 | + state.alertNthPrime = prime.map(PrimeAlert.init(prime:)) |
| 79 | + state.isNthPrimeButtonDisabled = false |
| 80 | + return state |
| 81 | + case .alertDismissButtonTapped: |
| 82 | + state.alertNthPrime = nil |
| 83 | + return state |
| 84 | + case .isPrimeButtonTapped: |
| 85 | + state.isPrimeModalShown = true |
| 86 | + return state |
| 87 | + case .primeModalDismissed: |
| 88 | + state.isPrimeModalShown = false |
| 89 | + return state |
| 90 | + } |
| 91 | +} |
| 92 | + |
| 93 | +struct CounterEnvironment { |
| 94 | + var nthPrime: (Int) -> AnyPublisher<Int?, Never> |
| 95 | +} |
| 96 | + |
| 97 | +extension CounterEnvironment { |
| 98 | + static let live = CounterEnvironment(nthPrime: Counter.nthPrime) |
| 99 | +} |
| 100 | + |
| 101 | +var Current = CounterEnvironment.live |
| 102 | + |
| 103 | +extension CounterEnvironment { |
| 104 | + static let mock = CounterEnvironment(nthPrime: { _ in .sync { 17 }}) |
| 105 | +} |
| 106 | + |
| 107 | +import CasePaths |
| 108 | + |
| 109 | +public let counterViewReducer: Reducer<CounterViewAction, CounterViewState> = |
| 110 | + counterReducer.lift( |
| 111 | + actionGetter: /CounterViewAction.counter, |
| 112 | + stateGetter: { $0.counter }, |
| 113 | + stateSetter: setter(\CounterViewState.counter) |
| 114 | + ) <> primeModalReducer.lift( |
| 115 | + actionGetter: /CounterViewAction.primeModal, |
| 116 | + stateGetter: { $0.primeModal }, |
| 117 | + stateSetter: setter(\CounterViewState.primeModal) |
| 118 | + ) |
| 119 | + |
| 120 | +public struct PrimeAlert: Equatable, Identifiable { |
| 121 | + let prime: Int |
| 122 | + public var id: Int { self.prime } |
| 123 | +} |
| 124 | + |
| 125 | +public struct CounterViewState: Equatable { |
| 126 | + public var alertNthPrime: PrimeAlert? |
| 127 | + public var count: Int |
| 128 | + public var favoritePrimes: [Int] |
| 129 | + public var isNthPrimeButtonDisabled: Bool |
| 130 | + public var isPrimeModalShown: Bool |
| 131 | + |
| 132 | + public init( |
| 133 | + alertNthPrime: PrimeAlert? = nil, |
| 134 | + count: Int = 0, |
| 135 | + favoritePrimes: [Int] = [], |
| 136 | + isNthPrimeButtonDisabled: Bool = false, |
| 137 | + isPrimeModalShown: Bool = false |
| 138 | + ) { |
| 139 | + self.alertNthPrime = alertNthPrime |
| 140 | + self.count = count |
| 141 | + self.favoritePrimes = favoritePrimes |
| 142 | + self.isNthPrimeButtonDisabled = isNthPrimeButtonDisabled |
| 143 | + self.isPrimeModalShown = isPrimeModalShown |
| 144 | + } |
| 145 | + |
| 146 | + var counter: CounterState { |
| 147 | + get { (self.alertNthPrime, self.count, self.isNthPrimeButtonDisabled, self.isPrimeModalShown) } |
| 148 | + set { (self.alertNthPrime, self.count, self.isNthPrimeButtonDisabled, self.isPrimeModalShown) = newValue } |
| 149 | + } |
| 150 | + |
| 151 | + var primeModal: PrimeModalState { |
| 152 | + get { (self.count, self.favoritePrimes) } |
| 153 | + set { (self.count, self.favoritePrimes) = newValue } |
| 154 | + } |
| 155 | +} |
| 156 | + |
| 157 | +public enum CounterViewAction: Equatable { |
| 158 | + case counter(CounterAction) |
| 159 | + case primeModal(PrimeModalAction) |
| 160 | +} |
| 161 | + |
| 162 | +public struct CounterView: View { |
| 163 | + @ObservedObject var store: ObservableViewModel<CounterViewAction, CounterViewState> |
| 164 | + |
| 165 | + public init(store: ObservableViewModel<CounterViewAction, CounterViewState>) { |
| 166 | + self.store = store |
| 167 | + } |
| 168 | + |
| 169 | + public var body: some View { |
| 170 | + VStack { |
| 171 | + HStack { |
| 172 | + Button("-") { self.store.dispatch(.counter(.decrTapped)) } |
| 173 | + Text("\(self.store.state.count)") |
| 174 | + Button("+") { self.store.dispatch(.counter(.incrTapped)) } |
| 175 | + } |
| 176 | + Button("Is this prime?") { self.store.dispatch(.counter(.isPrimeButtonTapped)) } |
| 177 | + Button("What is the \(ordinal(self.store.state.count)) prime?") { |
| 178 | + self.store.dispatch(.counter(.nthPrimeButtonTapped)) |
| 179 | + } |
| 180 | + .disabled(self.store.state.isNthPrimeButtonDisabled) |
| 181 | + } |
| 182 | + .font(.title) |
| 183 | + .navigationBarTitle("Counter demo") |
| 184 | + .sheet( |
| 185 | + isPresented: .constant(self.store.state.isPrimeModalShown), |
| 186 | + onDismiss: { self.store.dispatch(.counter(.primeModalDismissed)) } |
| 187 | + ) { |
| 188 | + IsPrimeModalView( |
| 189 | + store: self.store.projection( |
| 190 | + action: { .primeModal($0) }, |
| 191 | + state: { ($0.count, $0.favoritePrimes) } |
| 192 | + ).asObservableViewModel( |
| 193 | + initialState: (0, []), |
| 194 | + emitsValue: .when { lhs, rhs in |
| 195 | + lhs.count != rhs.count || lhs.favoritePrimes != rhs.favoritePrimes |
| 196 | + }) |
| 197 | + ) |
| 198 | + } |
| 199 | + .alert( |
| 200 | + item: .constant(self.store.state.alertNthPrime) |
| 201 | + ) { alert in |
| 202 | + Alert( |
| 203 | + title: Text("The \(ordinal(self.store.state.count)) prime is \(alert.prime)"), |
| 204 | + dismissButton: .default(Text("Ok")) { |
| 205 | + self.store.dispatch(.counter(.alertDismissButtonTapped)) |
| 206 | + } |
| 207 | + ) |
| 208 | + } |
| 209 | + } |
| 210 | +} |
| 211 | + |
| 212 | +func ordinal(_ n: Int) -> String { |
| 213 | + let formatter = NumberFormatter() |
| 214 | + formatter.numberStyle = .ordinal |
| 215 | + return formatter.string(for: n) ?? "" |
| 216 | +} |
0 commit comments