-
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
798cd62
commit 6998f38
Showing
73 changed files
with
4,926 additions
and
1 deletion.
There are no files selected for viewing
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 |
---|---|---|
@@ -1,5 +1,5 @@ | ||
## [Point-Free](https://www.pointfree.co) | ||
|
||
> #### This directory contains code from Point-Free Episode: [Testable State Management: The Point](https://www.pointfree.co/episodes/ep86-swiftui-snapshot-testing) | ||
> #### This directory contains code from Point-Free Episode: [Adaptive State Management: Performance](https://www.pointfree.co/episodes/ep94-adaptive-state-management-performance) | ||
> | ||
> It's time to put the finishing touches to our architecture so that we can use it in production. This week we begin exploring how to make the composable architecture adapt to many use cases, and we will use a potential performance problem as inspiration for this exploration. |
19 changes: 19 additions & 0 deletions
19
0095-adaptive-state-management-pt2/PrimeTime/ComposableArchitecture/ComposableArchitecture.h
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,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> | ||
|
||
|
139 changes: 139 additions & 0 deletions
139
...aptive-state-management-pt2/PrimeTime/ComposableArchitecture/ComposableArchitecture.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,139 @@ | ||
import CasePaths | ||
import Combine | ||
import SwiftUI | ||
|
||
public typealias Reducer<Value, Action, Environment> = (inout Value, Action, Environment) -> [Effect<Action>] | ||
|
||
public func combine<Value, Action, Environment>( | ||
_ reducers: Reducer<Value, Action, Environment>... | ||
) -> Reducer<Value, Action, Environment> { | ||
return { value, action, environment in | ||
let effects = reducers.flatMap { $0(&value, action, environment) } | ||
return effects | ||
} | ||
} | ||
|
||
public func pullback<LocalValue, GlobalValue, LocalAction, GlobalAction, LocalEnvironment, GlobalEnvironment>( | ||
_ reducer: @escaping Reducer<LocalValue, LocalAction, LocalEnvironment>, | ||
value: WritableKeyPath<GlobalValue, LocalValue>, | ||
action: CasePath<GlobalAction, LocalAction>, | ||
environment: @escaping (GlobalEnvironment) -> LocalEnvironment | ||
) -> Reducer<GlobalValue, GlobalAction, GlobalEnvironment> { | ||
return { globalValue, globalAction, globalEnvironment in | ||
guard let localAction = action.extract(from: globalAction) else { return [] } | ||
let localEffects = reducer(&globalValue[keyPath: value], localAction, environment(globalEnvironment)) | ||
|
||
return localEffects.map { localEffect in | ||
localEffect.map(action.embed) | ||
.eraseToEffect() | ||
} | ||
} | ||
} | ||
|
||
public func logging<Value, Action, Environment>( | ||
_ reducer: @escaping Reducer<Value, Action, Environment> | ||
) -> Reducer<Value, Action, Environment> { | ||
return { value, action, environment in | ||
let effects = reducer(&value, action, environment) | ||
let newValue = value | ||
return [.fireAndForget { | ||
print("Action: \(action)") | ||
print("Value:") | ||
dump(newValue) | ||
print("---") | ||
}] + effects | ||
} | ||
} | ||
|
||
public final class ViewStore<Value>: ObservableObject { | ||
@Published public fileprivate(set) var value: Value | ||
fileprivate var cancellable: Cancellable? | ||
|
||
public init(initialValue value: Value) { | ||
self.value = value | ||
} | ||
} | ||
|
||
extension Store where Value: Equatable { | ||
public var view: ViewStore<Value> { | ||
self.view(removeDuplicates: ==) | ||
} | ||
} | ||
|
||
extension Store { | ||
public func view( | ||
removeDuplicates predicate: @escaping (Value, Value) -> Bool | ||
) -> ViewStore<Value> { | ||
let viewStore = ViewStore(initialValue: self.value) | ||
|
||
viewStore.cancellable = self.$value | ||
.removeDuplicates(by: predicate) | ||
.sink(receiveValue: { [weak viewStore] value in | ||
viewStore?.value = value | ||
self | ||
}) | ||
|
||
return viewStore | ||
} | ||
} | ||
|
||
public final class Store<Value, Action> /*: ObservableObject */ { | ||
private let reducer: Reducer<Value, Action, Any> | ||
private let environment: Any | ||
@Published private var value: Value | ||
private var viewCancellable: Cancellable? | ||
private var effectCancellables: Set<AnyCancellable> = [] | ||
|
||
public init<Environment>( | ||
initialValue: Value, | ||
reducer: @escaping Reducer<Value, Action, Environment>, | ||
environment: Environment | ||
) { | ||
self.reducer = { value, action, environment in | ||
reducer(&value, action, environment as! Environment) | ||
} | ||
self.value = initialValue | ||
self.environment = environment | ||
} | ||
|
||
public func send(_ action: Action) { | ||
let effects = self.reducer(&self.value, action, self.environment) | ||
effects.forEach { effect in | ||
var effectCancellable: AnyCancellable? | ||
var didComplete = false | ||
effectCancellable = effect.sink( | ||
receiveCompletion: { [weak self, weak effectCancellable] _ in | ||
didComplete = true | ||
guard let effectCancellable = effectCancellable else { return } | ||
self?.effectCancellables.remove(effectCancellable) | ||
}, | ||
receiveValue: { [weak self] in self?.send($0) } | ||
) | ||
if !didComplete, let effectCancellable = effectCancellable { | ||
self.effectCancellables.insert(effectCancellable) | ||
} | ||
} | ||
} | ||
|
||
public func scope<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 [] | ||
}, | ||
environment: self.environment | ||
) | ||
localStore.viewCancellable = self.$value | ||
.map(toLocalValue) | ||
// .removeDuplicates() | ||
.sink { [weak localStore] newValue in | ||
localStore?.value = newValue | ||
} | ||
return localStore | ||
} | ||
} |
42 changes: 42 additions & 0 deletions
42
0095-adaptive-state-management-pt2/PrimeTime/ComposableArchitecture/Effect.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,42 @@ | ||
import Combine | ||
|
||
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 Effect { | ||
public static func fireAndForget(work: @escaping () -> Void) -> Effect { | ||
return Deferred { () -> Empty<Output, Never> in | ||
work() | ||
return Empty(completeImmediately: true) | ||
}.eraseToEffect() | ||
} | ||
|
||
public static func sync(work: @escaping () -> Output) -> Effect { | ||
return Deferred { | ||
Just(work()) | ||
}.eraseToEffect() | ||
} | ||
} | ||
|
||
extension Publisher where Failure == Never { | ||
public func eraseToEffect() -> Effect<Output> { | ||
return Effect(publisher: self.eraseToAnyPublisher()) | ||
} | ||
} | ||
|
||
extension Publisher where Output == Never, Failure == Never { | ||
public func fireAndForget<A>() -> Effect<A> { | ||
return self.map(absurd).eraseToEffect() | ||
} | ||
} | ||
|
||
private func absurd<A>(_ never: Never) -> A {} |
22 changes: 22 additions & 0 deletions
22
0095-adaptive-state-management-pt2/PrimeTime/ComposableArchitecture/Info.plist
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,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> |
19 changes: 19 additions & 0 deletions
19
...ement-pt2/PrimeTime/ComposableArchitectureTestSupport/ComposableArchitectureTestSupport.h
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,19 @@ | ||
// | ||
// ComposableArchitectureTestSupport.h | ||
// ComposableArchitectureTestSupport | ||
// | ||
// Created by Stephen Celis on 2/6/20. | ||
// Copyright © 2020 Point-Free. All rights reserved. | ||
// | ||
|
||
#import <Foundation/Foundation.h> | ||
|
||
//! Project version number for ComposableArchitectureTestSupport. | ||
FOUNDATION_EXPORT double ComposableArchitectureTestSupportVersionNumber; | ||
|
||
//! Project version string for ComposableArchitectureTestSupport. | ||
FOUNDATION_EXPORT const unsigned char ComposableArchitectureTestSupportVersionString[]; | ||
|
||
// In this header, you should import all the public headers of your framework using statements like #import <ComposableArchitectureTestSupport/PublicHeader.h> | ||
|
||
|
79 changes: 79 additions & 0 deletions
79
...t-pt2/PrimeTime/ComposableArchitectureTestSupport/ComposableArchitectureTestSupport.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,79 @@ | ||
import ComposableArchitecture | ||
import XCTest | ||
|
||
public enum StepType { | ||
case send | ||
case receive | ||
} | ||
|
||
public struct Step<Value, Action> { | ||
let type: StepType | ||
let action: Action | ||
let update: (inout Value) -> Void | ||
let file: StaticString | ||
let line: UInt | ||
|
||
public init( | ||
_ type: StepType, | ||
_ action: Action, | ||
file: StaticString = #file, | ||
line: UInt = #line, | ||
_ update: @escaping (inout Value) -> Void = { _ in } | ||
) { | ||
self.type = type | ||
self.action = action | ||
self.update = update | ||
self.file = file | ||
self.line = line | ||
} | ||
} | ||
|
||
public func assert<Value: Equatable, Action: Equatable, Environment>( | ||
initialValue: Value, | ||
reducer: Reducer<Value, Action, Environment>, | ||
environment: Environment, | ||
steps: Step<Value, Action>..., | ||
file: StaticString = #file, | ||
line: UInt = #line | ||
) { | ||
var state = initialValue | ||
var effects: [Effect<Action>] = [] | ||
|
||
steps.forEach { step in | ||
var expected = state | ||
|
||
switch step.type { | ||
case .send: | ||
if !effects.isEmpty { | ||
XCTFail("Action sent before handling \(effects.count) pending effect(s)", file: step.file, line: step.line) | ||
} | ||
effects.append(contentsOf: reducer(&state, step.action, environment)) | ||
|
||
case .receive: | ||
guard !effects.isEmpty else { | ||
XCTFail("No pending effects to receive from", file: step.file, line: step.line) | ||
break | ||
} | ||
let effect = effects.removeFirst() | ||
var action: Action! | ||
let receivedCompletion = XCTestExpectation(description: "receivedCompletion") | ||
let e = effect.sink( | ||
receiveCompletion: { _ in | ||
receivedCompletion.fulfill() | ||
}, | ||
receiveValue: { action = $0 } | ||
) | ||
if XCTWaiter.wait(for: [receivedCompletion], timeout: 0.01) != .completed { | ||
XCTFail("Timed out waiting for the effect to complete", file: step.file, line: step.line) | ||
} | ||
XCTAssertEqual(action, step.action, file: step.file, line: step.line) | ||
effects.append(contentsOf: reducer(&state, action, environment)) | ||
} | ||
|
||
step.update(&expected) | ||
XCTAssertEqual(state, expected, file: step.file, line: step.line) | ||
} | ||
if !effects.isEmpty { | ||
XCTFail("Assertion failed to handle \(effects.count) pending effect(s)", file: file, line: line) | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
0095-adaptive-state-management-pt2/PrimeTime/ComposableArchitectureTestSupport/Info.plist
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,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> |
5 changes: 5 additions & 0 deletions
5
...te-management-pt2/PrimeTime/ComposableArchitectureTests/ComposableArchitectureTests.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,5 @@ | ||
import XCTest | ||
@testable import ComposableArchitecture | ||
|
||
class ComposableArchitectureTests: XCTestCase { | ||
} |
22 changes: 22 additions & 0 deletions
22
0095-adaptive-state-management-pt2/PrimeTime/ComposableArchitectureTests/Info.plist
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,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>1</string> | ||
</dict> | ||
</plist> |
Oops, something went wrong.