-
Notifications
You must be signed in to change notification settings - Fork 299
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
96d373c
commit d79b980
Showing
5 changed files
with
383 additions
and
0 deletions.
There are no files selected for viewing
288 changes: 288 additions & 0 deletions
288
...posable-state-management-state-pullbacks/ComposableArchitecture.playground/Contents.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Value, Action>( | ||
_ reducers: (inout Value, Action) -> Void... | ||
) -> (inout Value, Action) -> Void { | ||
return { value, action in | ||
for reducer in reducers { | ||
reducer(&value, action) | ||
} | ||
} | ||
} | ||
final class Store<Value, Action>: 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<LocalValue, GlobalValue, Action>( | ||
_ reducer: @escaping (inout LocalValue, Action) -> Void, | ||
value: WritableKeyPath<GlobalValue, LocalValue> | ||
) -> (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<AppState> | ||
|
||
struct PrimeAlert: Identifiable { | ||
let prime: Int | ||
var id: Int { self.prime } | ||
} | ||
|
||
struct CounterView: View { | ||
@ObservedObject var store: Store<AppState, AppAction> | ||
@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<AppState, AppAction> | ||
|
||
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<AppState, AppAction> | ||
|
||
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<AppState, AppAction> | ||
|
||
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) | ||
) | ||
) |
85 changes: 85 additions & 0 deletions
85
...ble-state-management-state-pullbacks/ComposableArchitecture.playground/Sources/Util.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<A, B, C>( | ||
_ f: @escaping (B) -> C, | ||
_ g: @escaping (A) -> B | ||
) | ||
-> (A) -> C { | ||
|
||
return { (a: A) -> C in | ||
f(g(a)) | ||
} | ||
} | ||
|
||
public func with<A, B>(_ a: A, _ f: (A) throws -> B) rethrows -> B { | ||
return try f(a) | ||
} |
4 changes: 4 additions & 0 deletions
4
...-state-management-state-pullbacks/ComposableArchitecture.playground/contents.xcplayground
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> | ||
<playground version='5.0' target-platform='ios'> | ||
<timeline fileName='timeline.xctimeline'/> | ||
</playground> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters