Skip to content

Commit

Permalink
69
Browse files Browse the repository at this point in the history
  • Loading branch information
stephencelis committed Aug 12, 2019
1 parent 96d373c commit d79b980
Show file tree
Hide file tree
Showing 5 changed files with 383 additions and 0 deletions.
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)
)
)
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)
}
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>
5 changes: 5 additions & 0 deletions 0069-composable-state-management-state-pullbacks/README.md
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.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit d79b980

Please sign in to comment.