diff --git a/0069-composable-state-management-state-pullbacks/ComposableArchitecture.playground/Contents.swift b/0069-composable-state-management-state-pullbacks/ComposableArchitecture.playground/Contents.swift new file mode 100644 index 00000000..3b6044e0 --- /dev/null +++ b/0069-composable-state-management-state-pullbacks/ComposableArchitecture.playground/Contents.swift @@ -0,0 +1,288 @@ +import Combine +import SwiftUI + +struct AppState { + var count = 0 + var favoritePrimes: [Int] = [] + var loggedInUser: User? = nil + var activityFeed: [Activity] = [] + + struct Activity { + let timestamp: Date + let type: ActivityType + + enum ActivityType { + case addedFavoritePrime(Int) + case removedFavoritePrime(Int) + } + } + + struct User { + let id: Int + let name: String + let bio: String + } +} + +enum CounterAction { + case decrTapped + case incrTapped +} +enum PrimeModalAction { + case saveFavoritePrimeTapped + case removeFavoritePrimeTapped +} +enum FavoritePrimesAction { + case deleteFavoritePrimes(IndexSet) +} +enum AppAction { + case counter(CounterAction) + case primeModal(PrimeModalAction) + case favoritePrimes(FavoritePrimesAction) +} + +// (A) -> A +// (inout A) -> Void + +// (A, B) -> (A, C) +// (inout A, B) -> C + +// (Value, Action) -> Value +// (inout Value, Action) -> Void + +//[1, 2, 3].reduce(into: <#T##Result#>, <#T##updateAccumulatingResult: (inout Result, Int) throws -> ()##(inout Result, Int) throws -> ()#>) + +func counterReducer(state: inout Int, action: AppAction) { + switch action { + case .counter(.decrTapped): + state -= 1 + + case .counter(.incrTapped): + state += 1 + + default: + break + } +} + +func primeModalReducer(state: inout AppState, action: AppAction) { + switch action { + case .primeModal(.removeFavoritePrimeTapped): + state.favoritePrimes.removeAll(where: { $0 == state.count }) + state.activityFeed.append(.init(timestamp: Date(), type: .addedFavoritePrime(state.count))) + + case .primeModal(.saveFavoritePrimeTapped): + state.favoritePrimes.append(state.count) + state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.count))) + + default: + break + } +} + +struct FavoritePrimesState { + var favoritePrimes: [Int] + var activityFeed: [AppState.Activity] +} + +func favoritePrimesReducer(state: inout FavoritePrimesState, action: AppAction) { + switch action { + case let .favoritePrimes(.deleteFavoritePrimes(indexSet)): + for index in indexSet { + state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.favoritePrimes[index]))) + state.favoritePrimes.remove(at: index) + } + + default: + break + } +} + +//func appReducer(state: inout AppState, action: AppAction) { +// switch action { +// } +//} + +func combine( + _ reducers: (inout Value, Action) -> Void... +) -> (inout Value, Action) -> Void { + return { value, action in + for reducer in reducers { + reducer(&value, action) + } + } +} +final class Store: ObservableObject { + let reducer: (inout Value, Action) -> Void + @Published private(set) var value: Value + + init(initialValue: Value, reducer: @escaping (inout Value, Action) -> Void) { + self.reducer = reducer + self.value = initialValue + } + + func send(_ action: Action) { + self.reducer(&self.value, action) + } +} +func pullback( + _ reducer: @escaping (inout LocalValue, Action) -> Void, + value: WritableKeyPath +) -> (inout GlobalValue, Action) -> Void { + return { globalValue, action in + reducer(&globalValue[keyPath: value], action) + } +} + +extension AppState { + var favoritePrimesState: FavoritePrimesState { + get { + FavoritePrimesState( + favoritePrimes: self.favoritePrimes, + activityFeed: self.activityFeed + ) + } + set { + self.favoritePrimes = newValue.favoritePrimes + self.activityFeed = newValue.activityFeed + } + } +} + +let _appReducer = combine( + pullback(counterReducer, value: \.count), + primeModalReducer, + pullback(favoritePrimesReducer, value: \.favoritePrimesState) +) +let appReducer = pullback(_appReducer, value: \.self) + //combine(combine(counterReducer, primeModalReducer), favoritePrimesReducer) + +// [1, 2, 3].reduce(<#T##initialResult: Result##Result#>, <#T##nextPartialResult: (Result, Int) throws -> Result##(Result, Int) throws -> Result#>) + +var state = AppState() +//appReducer(state: &state, action: .incrTapped) +//appReducer(state: &state, action: .decrTapped) +//print( +// counterReducer( +// state: counterReducer(state: state, action: .incrTapped), +// action: .decrTapped +// ) +//) +//counterReducer(state: state, action: .decrTapped) + +// Store + +struct PrimeAlert: Identifiable { + let prime: Int + var id: Int { self.prime } +} + +struct CounterView: View { + @ObservedObject var store: Store + @State var isPrimeModalShown = false + @State var alertNthPrime: PrimeAlert? + @State var isNthPrimeButtonDisabled = false + + var body: some View { + VStack { + HStack { + Button("-") { self.store.send(.counter(.decrTapped)) } + Text("\(self.store.value.count)") + Button("+") { self.store.send(.counter(.incrTapped)) } + } + Button("Is this prime?") { self.isPrimeModalShown = true } + Button( + "What is the \(ordinal(self.store.value.count)) prime?", + action: self.nthPrimeButtonAction + ) + .disabled(self.isNthPrimeButtonDisabled) + } + .font(.title) + .navigationBarTitle("Counter demo") + .sheet(isPresented: self.$isPrimeModalShown) { + IsPrimeModalView(store: self.store) + } + .alert(item: self.$alertNthPrime) { alert in + Alert( + title: Text("The \(ordinal(self.store.value.count)) prime is \(alert.prime)"), + dismissButton: .default(Text("Ok")) + ) + } + } + + func nthPrimeButtonAction() { + self.isNthPrimeButtonDisabled = true + nthPrime(self.store.value.count) { prime in + self.alertNthPrime = prime.map(PrimeAlert.init(prime:)) + self.isNthPrimeButtonDisabled = false + } + } +} + +struct IsPrimeModalView: View { + @ObservedObject var store: Store + + var body: some View { + VStack { + if isPrime(self.store.value.count) { + Text("\(self.store.value.count) is prime 🎉") + if self.store.value.favoritePrimes.contains(self.store.value.count) { + Button("Remove from favorite primes") { + self.store.send(.primeModal(.removeFavoritePrimeTapped)) + } + } else { + Button("Save to favorite primes") { + self.store.send(.primeModal(.saveFavoritePrimeTapped)) + } + } + } else { + Text("\(self.store.value.count) is not prime :(") + } + } + } +} + +struct FavoritePrimesView: View { + @ObservedObject var store: Store + + var body: some View { + List { + ForEach(self.store.value.favoritePrimes, id: \.self) { prime in + Text("\(prime)") + } + .onDelete { indexSet in + self.store.send(.favoritePrimes(.deleteFavoritePrimes(indexSet))) + } + } + .navigationBarTitle("Favorite Primes") + } +} + +struct ContentView: View { + @ObservedObject var store: Store + + var body: some View { + NavigationView { + List { + NavigationLink( + "Counter demo", + destination: CounterView(store: self.store) + ) + NavigationLink( + "Favorite primes", + destination: FavoritePrimesView(store: self.store) + ) + } + .navigationBarTitle("State management") + } + } +} + +// import Overture + +import PlaygroundSupport +PlaygroundPage.current.liveView = UIHostingController( + rootView: ContentView( + store: Store(initialValue: AppState(), reducer: appReducer) + ) +) diff --git a/0069-composable-state-management-state-pullbacks/ComposableArchitecture.playground/Sources/Util.swift b/0069-composable-state-management-state-pullbacks/ComposableArchitecture.playground/Sources/Util.swift new file mode 100644 index 00000000..3b8c20ce --- /dev/null +++ b/0069-composable-state-management-state-pullbacks/ComposableArchitecture.playground/Sources/Util.swift @@ -0,0 +1,85 @@ +import Foundation + +private let wolframAlphaApiKey = "6H69Q3-828TKQJ4EP" + +private struct WolframAlphaResult: Decodable { + let queryresult: QueryResult + + struct QueryResult: Decodable { + let pods: [Pod] + + struct Pod: Decodable { + let primary: Bool? + let subpods: [SubPod] + + struct SubPod: Decodable { + let plaintext: String + } + } + } +} + +private func wolframAlpha(query: String, callback: @escaping (WolframAlphaResult?) -> Void) -> Void { + var components = URLComponents(string: "https://api.wolframalpha.com/v2/query")! + components.queryItems = [ + URLQueryItem(name: "input", value: query), + URLQueryItem(name: "format", value: "plaintext"), + URLQueryItem(name: "output", value: "JSON"), + URLQueryItem(name: "appid", value: wolframAlphaApiKey), + ] + + URLSession.shared.dataTask(with: components.url(relativeTo: nil)!) { data, response, error in + callback( + data + .flatMap { try? JSONDecoder().decode(WolframAlphaResult.self, from: $0) } + ) + } + .resume() +} + +public func nthPrime(_ n: Int, callback: @escaping (Int?) -> Void) -> Void { + wolframAlpha(query: "prime \(n)") { result in + callback( + result + .flatMap { + $0.queryresult + .pods + .first(where: { $0.primary == .some(true) })? + .subpods + .first? + .plaintext + } + .flatMap(Int.init) + ) + } +} + +public func ordinal(_ n: Int) -> String { + let formatter = NumberFormatter() + formatter.numberStyle = .ordinal + return formatter.string(for: n) ?? "" +} + +public func isPrime (_ p: Int) -> Bool { + if p <= 1 { return false } + if p <= 3 { return true } + for i in 2...Int(sqrtf(Float(p))) { + if p % i == 0 { return false } + } + return true +} + +public func compose( + _ f: @escaping (B) -> C, + _ g: @escaping (A) -> B + ) + -> (A) -> C { + + return { (a: A) -> C in + f(g(a)) + } +} + +public func with(_ a: A, _ f: (A) throws -> B) rethrows -> B { + return try f(a) +} diff --git a/0069-composable-state-management-state-pullbacks/ComposableArchitecture.playground/contents.xcplayground b/0069-composable-state-management-state-pullbacks/ComposableArchitecture.playground/contents.xcplayground new file mode 100644 index 00000000..5da2641c --- /dev/null +++ b/0069-composable-state-management-state-pullbacks/ComposableArchitecture.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/0069-composable-state-management-state-pullbacks/README.md b/0069-composable-state-management-state-pullbacks/README.md new file mode 100644 index 00000000..25cf6ab4 --- /dev/null +++ b/0069-composable-state-management-state-pullbacks/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [Composable State Management: State Pullbacks](https://www.pointfree.co/episodes/ep69-composable-state-management-state-pullbacks) +> +> So far we have pulled a lot of our application’s logic into a reducer, but that reducer is starting to get big. Turns out that reducers emit many types of powerful compositions, and this week we explore two of them: combines and pullbacks. diff --git a/README.md b/README.md index 95060e32..78b0e862 100644 --- a/README.md +++ b/README.md @@ -71,3 +71,4 @@ This repository is the home of code written on episodes of 1. [SwiftUI and State Management: Part 2](0066-swiftui-and-state-management-pt2) 1. [SwiftUI and State Management: Part 3](0067-swiftui-and-state-management-pt3) 1. [Composable State Management: Reducers](0068-composable-state-management-reducers) +1. [Composable State Management: State Pullbacks](0069-composable-state-management-state-pullbacks)