Skip to content

Commit

Permalink
95
Browse files Browse the repository at this point in the history
  • Loading branch information
stephencelis committed Mar 23, 2020
1 parent 798cd62 commit 6998f38
Show file tree
Hide file tree
Showing 73 changed files with 4,926 additions and 1 deletion.
2 changes: 1 addition & 1 deletion 0094-adaptive-state-management-pt1/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: [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.
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,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
}
}
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 {}
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>
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>


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)
}
}
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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import XCTest
@testable import ComposableArchitecture

class ComposableArchitectureTests: XCTestCase {
}
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>
Loading

0 comments on commit 6998f38

Please sign in to comment.