Skip to content

Commit

Permalink
79
Browse files Browse the repository at this point in the history
  • Loading branch information
stephencelis committed Nov 4, 2019
1 parent 3f99e9c commit c37e20f
Show file tree
Hide file tree
Showing 47 changed files with 3,269 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,25 @@ struct Parallel<A> {
//UIView.animate(withDuration: TimeInterval, animations: () -> Void) -> Void
//URLSession.shared.dataTask(with: URL, completionHandler: (Data?, URLResponse?, Error?) -> Void) -> Void

public typealias Effect<Action> = (@escaping (Action) -> Void) -> Void
//public typealias Effect<Action> = (@escaping (Action) -> Void) -> Void

public struct Effect<Output>: Publisher {
public typealias Failure = Never

let publisher: AnyPublisher<Output, Failure>

public func receive<S>(
subscriber: S
) where S: Subscriber, Failure == S.Failure, Output == S.Input {
self.publisher.receive(subscriber: subscriber)
}
}

extension Publisher where Failure == Never {
public func eraseToEffect() -> Effect<Output> {
return Effect(publisher: self.eraseToAnyPublisher())
}
}

public typealias Reducer<Value, Action> = (inout Value, Action) -> [Effect<Action>]

Expand All @@ -18,7 +36,8 @@ public typealias Reducer<Value, Action> = (inout Value, Action) -> [Effect<Actio
public final class Store<Value, Action>: ObservableObject {
private let reducer: Reducer<Value, Action>
@Published public private(set) var value: Value
private var cancellable: Cancellable?
private var viewCancellable: Cancellable?
private var effectCancellables: [AnyCancellable] = []

public init(initialValue: Value, reducer: @escaping Reducer<Value, Action>) {
self.reducer = reducer
Expand All @@ -28,7 +47,15 @@ public final class Store<Value, Action>: ObservableObject {
public func send(_ action: Action) {
let effects = self.reducer(&self.value, action)
effects.forEach { effect in
effect(self.send)
var effectCancellable: AnyCancellable!
effectCancellable = effect
.sink(
receiveCompletion: { [weak self] _ in
self?.effectCancellables.removeAll(where: { $0 == effectCancellable })
},
receiveValue: self.send
)
self.effectCancellables.append(effectCancellable)
}
// DispatchQueue.global().async {
// effects.forEach { effect in
Expand All @@ -53,7 +80,7 @@ public final class Store<Value, Action>: ObservableObject {
return []
}
)
localStore.cancellable = self.$value.sink { [weak localStore] newValue in
localStore.viewCancellable = self.$value.sink { [weak localStore] newValue in
localStore?.value = toLocalValue(newValue)
}
return localStore
Expand All @@ -78,14 +105,13 @@ public func pullback<LocalValue, GlobalValue, LocalAction, GlobalAction>(
guard let localAction = globalAction[keyPath: action] else { return [] }
let localEffects = reducer(&globalValue[keyPath: value], localAction)
return localEffects.map { localEffect in
{ callback in
// guard let localAction = localEffect() else { return nil }
localEffect { localAction in
localEffect
.map { localAction -> GlobalAction in
var globalAction = globalAction
globalAction[keyPath: action] = localAction
callback(globalAction)
}
return globalAction
}
.eraseToEffect()
}
}
}
Expand All @@ -96,11 +122,32 @@ public func logging<Value, Action>(
return { value, action in
let effects = reducer(&value, action)
let newValue = value
return [{ _ in
print("Action: \(action)")
print("Value:")
dump(newValue)
print("---")
}] + effects
return [
Deferred {
Future { _ in
print("Action: \(action)")
print("Value:")
dump(newValue)
print("---")
}
}.eraseToEffect()
] + effects
}
}

extension Effect {
public static func fireAndForget(work: @escaping () -> Void) -> Effect {
return Deferred { () -> Empty<Output, Never> in
work()
return Empty<Output, Never>(completeImmediately: true)
}
.eraseToEffect()
}

public static func sync(work: @escaping () -> Output) -> Effect {
return Deferred {
Just(work())
}
.eraseToEffect()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ public func counterReducer(state: inout CounterState, action: CounterAction) ->
case .nthPrimeButtonTapped:
state.isNthPrimeButtonDisabled = true
let count = state.count
return [{ callback in
nthPrime(count) { prime in
DispatchQueue.main.async {
callback(.nthPrimeResponse(prime))
}
}
// return [{ callback in
// nthPrime(count) { prime in
// DispatchQueue.main.async {
// callback(.nthPrimeResponse(prime))
// }
// }
// var p: Int?
// let sema = DispatchSemaphore(value: 0)
// nthPrime(count) { prime in
Expand All @@ -43,7 +43,8 @@ public func counterReducer(state: inout CounterState, action: CounterAction) ->
// }
// sema.wait()
// return .nthPrimeResponse(p)
}]
// }]
return []

case let .nthPrimeResponse(prime):
state.alertNthPrime = prime.map(PrimeAlert.init(prime:))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public func favoritePrimesReducer(state: inout [Int], action: FavoritePrimesActi
}

private func saveEffect(favoritePrimes: [Int]) -> Effect<FavoritePrimesAction> {
return {
return .fireAndForget {
let data = try! JSONEncoder().encode(favoritePrimes)
let documentsPath = NSSearchPathForDirectoriesInDomains(
.documentDirectory, .userDomainMask, true
Expand All @@ -39,11 +39,10 @@ private func saveEffect(favoritePrimes: [Int]) -> Effect<FavoritePrimesAction> {
let favoritePrimesUrl = documentsUrl
.appendingPathComponent("favorite-primes.json")
try! data.write(to: favoritePrimesUrl)
return nil
}
}

private let loadEffect: Effect<FavoritePrimesAction> = {
private let loadEffect = Effect<FavoritePrimesAction?>.sync {
let documentsPath = NSSearchPathForDirectoriesInDomains(
.documentDirectory, .userDomainMask, true
)[0]
Expand All @@ -56,6 +55,8 @@ private let loadEffect: Effect<FavoritePrimesAction> = {
else { return nil }
return .loadedFavoritePrimes(favoritePrimes)
}
.compactMap { $0 }
.eraseToEffect()

public struct FavoritePrimesView: View {
@ObservedObject var store: Store<[Int], FavoritePrimesAction>
Expand Down
2 changes: 1 addition & 1 deletion 0078-effectful-state-management-async-effects/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
## [Point-Free](https://www.pointfree.co)

> #### This directory contains code from Point-Free Episode: [Effectful State Management: Unidirectional Effects](https://www.pointfree.co/episodes/ep77-effectful-state-management-unidirectional-effects)
> #### This directory contains code from Point-Free Episode: [Effectful State Management: Asynchronous Effects](https://www.pointfree.co/episodes/ep78-effectful-state-management-asynchronous-effects)
>
> It's time to finish our architecture's story for side effects. We've described synchronous effects and unidirectional effects, but we still haven't captured the complexity of async effects. Let's fix that with a final, functional refactor.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// ComposableArchitecture.h
// ComposableArchitecture
//
// Created by Stephen Celis on 9/8/19.
// Copyright © 2019 Point-Free. All rights reserved.
//

#import <Foundation/Foundation.h>

//! Project version number for ComposableArchitecture.
FOUNDATION_EXPORT double ComposableArchitectureVersionNumber;

//! Project version string for ComposableArchitecture.
FOUNDATION_EXPORT const unsigned char ComposableArchitectureVersionString[];

// In this header, you should import all the public headers of your framework using statements like #import <ComposableArchitecture/PublicHeader.h>


Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import Combine
import SwiftUI


struct Parallel<A> {
let run: (@escaping (A) -> Void) -> Void
}

//DispatchQueue.main.async(execute: <#T##() -> Void#>) -> Void
//UIView.animate(withDuration: <#T##TimeInterval#>, animations: <#T##() -> Void#>) -> Void
//URLSession.shared.dataTask(with: <#T##URL#>, completionHandler: <#T##(Data?, URLResponse?, Error?) -> Void#>) -> Void

//public typealias Effect<Action> = (@escaping (Action) -> Void) -> Void


public struct Effect<A> {
public let run: (@escaping (A) -> Void) -> Void

public init(run: @escaping (@escaping (A) -> Void) -> Void) {
self.run = run
}

public func map<B>(_ f: @escaping (A) -> B) -> Effect<B> {
return Effect<B> { callback in self.run { a in callback(f(a)) } }
}
}

public typealias Reducer<Value, Action> = (inout Value, Action) -> [Effect<Action>]

//Button.init("Save", action: <#T##() -> Void#>)

public final class Store<Value, Action>: ObservableObject {
private let reducer: Reducer<Value, Action>
@Published public private(set) var value: Value
private var cancellable: Cancellable?

public init(initialValue: Value, reducer: @escaping Reducer<Value, Action>) {
self.reducer = reducer
self.value = initialValue
}

public func send(_ action: Action) {
let effects = self.reducer(&self.value, action)
effects.forEach { effect in
effect.run(self.send)
}
// DispatchQueue.global().async {
// effects.forEach { effect in
// if let action = effect() {
// DispatchQueue.main.async {
// self.send(action)
// }
// }
// }
// }
}

public func view<LocalValue, LocalAction>(
value toLocalValue: @escaping (Value) -> LocalValue,
action toGlobalAction: @escaping (LocalAction) -> Action
) -> Store<LocalValue, LocalAction> {
let localStore = Store<LocalValue, LocalAction>(
initialValue: toLocalValue(self.value),
reducer: { localValue, localAction in
self.send(toGlobalAction(localAction))
localValue = toLocalValue(self.value)
return []
}
)
localStore.cancellable = self.$value.sink { [weak localStore] newValue in
localStore?.value = toLocalValue(newValue)
}
return localStore
}
}

public func combine<Value, Action>(
_ reducers: Reducer<Value, Action>...
) -> Reducer<Value, Action> {
return { value, action in
let effects = reducers.flatMap { $0(&value, action) }
return effects
// return { () -> Action? in
// var finalAction: Action?
// for effect in effects {
// let action = effect()
// if let action = action {
// finalAction = action
// }
// }
// return finalAction
// }
}
}

public func pullback<LocalValue, GlobalValue, LocalAction, GlobalAction>(
_ reducer: @escaping Reducer<LocalValue, LocalAction>,
value: WritableKeyPath<GlobalValue, LocalValue>,
action: WritableKeyPath<GlobalAction, LocalAction?>
) -> Reducer<GlobalValue, GlobalAction> {
return { globalValue, globalAction in
guard let localAction = globalAction[keyPath: action] else { return [] }
let localEffects = reducer(&globalValue[keyPath: value], localAction)

return localEffects.map { localEffect in
Effect { callback in
// guard let localAction = localEffect() else { return nil }
localEffect.run { localAction in
var globalAction = globalAction
globalAction[keyPath: action] = localAction
callback(globalAction)
}
}
}

// return effect
}
}

public func logging<Value, Action>(
_ reducer: @escaping Reducer<Value, Action>
) -> Reducer<Value, Action> {
return { value, action in
let effects = reducer(&value, action)
let newValue = value
return [Effect { _ in
print("Action: \(action)")
print("Value:")
dump(newValue)
print("---")
}] + effects
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
extension Effect where A == (Data?, URLResponse?, Error?) {
public func decode<M: Decodable>(as type: M.Type) -> Effect<M?> {
return self.map { data, _, _ in
data
.flatMap { try? JSONDecoder().decode(M.self, from: $0) }
}
}
}

extension Effect {
public func receive(on queue: DispatchQueue) -> Effect {
return Effect { callback in
self.run { a in
queue.async {
callback(a)
}
}
}
}
}

public func dataTask(with url: URL) -> Effect<(Data?, URLResponse?, Error?)> {
return Effect { callback in
URLSession.shared.dataTask(with: url) { data, response, error in
callback((data, response, error))
}
.resume()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>
Loading

0 comments on commit c37e20f

Please sign in to comment.