diff --git a/Example/Example/UseCases/StoreUseCasesView.swift b/Example/Example/UseCases/StoreUseCasesView.swift index 90e4404..b3ed1e5 100644 --- a/Example/Example/UseCases/StoreUseCasesView.swift +++ b/Example/Example/UseCases/StoreUseCasesView.swift @@ -14,7 +14,7 @@ struct StoreUseCasesView: View { typealias CounterStoreType = () -> (count: ReactiveValue, increment: () -> Void, decrement: () -> Void) -let useCounterStore: CounterStoreType = useStore("counter") { +let useCounterStore: CounterStoreType = defStore("counter") { let count = defValue(0) func increment() { @@ -25,11 +25,7 @@ let useCounterStore: CounterStoreType = useStore("counter") { count.value -= 1 } - return ( - count: count, - increment: increment, - decrement: decrement - ) + return (count, increment, decrement) } extension StoreUseCasesView { diff --git a/Example/Example/UseCases/ValueUseCasesView.swift b/Example/Example/UseCases/ValueUseCasesView.swift index daf961e..56c005d 100644 --- a/Example/Example/UseCases/ValueUseCasesView.swift +++ b/Example/Example/UseCases/ValueUseCasesView.swift @@ -8,12 +8,12 @@ import Water struct ValueUseCasesView: View { var body: some View { -// CounterView() + CounterView() // CounterNameView() // BooleanValueView() // ValueNotChangeView() // SimultaneousChangeValuesView() - ShowPasswordView() +// ShowPasswordView() } } diff --git a/Sources/Water/Composables/useReducer.swift b/Sources/Water/Composables/useReducer.swift index bc6ebab..ab8d933 100644 --- a/Sources/Water/Composables/useReducer.swift +++ b/Sources/Water/Composables/useReducer.swift @@ -5,7 +5,9 @@ import Foundation +// TODO: - reducer -> store ? public func useReducer(_ initialState: State, _ reducer: @escaping (inout State, Action) -> Void) -> (() -> State, (Action) -> Void) { + // FIXME: - change to defReactive ? let reactiveState = defValue(initialState) func dispatch(action: Action) -> Void { diff --git a/Sources/Water/Composables/useStore.swift b/Sources/Water/Composables/useStore.swift index 9c64726..5887d93 100644 --- a/Sources/Water/Composables/useStore.swift +++ b/Sources/Water/Composables/useStore.swift @@ -2,25 +2,37 @@ // useStore.swift // Water // +// 1. createStore +// 2. defStore +// 3. useStore +// 4. createSetupStore +// 5. unit test import Foundation -class Store { - private let setupFn: () ->T - - init(setupClosure: @escaping () -> T) { - self.setupFn = setupClosure - } +public typealias UseStoreFn = () -> Store + +// TODO: - view extension add global store manager to dispatcher +public func createStore() { - func setup() -> T { - return setupFn() - } } -public func useStore(_ storeId: String, setupClosure: @escaping () -> T) -> (() -> T) { - let store = Store(setupClosure: setupClosure) - let fn: () -> T = { - return store.setup() +func setupStore(with closure: @escaping () -> Store) -> Store { + // TODO: - call it with effect scope + // FIXME: - keep store reactive ? + let store = closure() + return store +} + +public func defStore(_ storeId: String, setupStoreClosure: @escaping () -> Store) -> UseStoreFn { + func useStore() -> Store { + // TODO: - follow the step + // 1. get dispatcher + // 2. check store exist + // 3. generate store + let store: Store = setupStore(with: setupStoreClosure) + // 4. save store with id + return store } - return fn + return useStore } diff --git a/Sources/Water/Reactivity/Computed.swift b/Sources/Water/Reactivity/Computed.swift index 4a1f5d4..2f3bff3 100644 --- a/Sources/Water/Reactivity/Computed.swift +++ b/Sources/Water/Reactivity/Computed.swift @@ -54,6 +54,5 @@ extension ComputedValue: Reactor { // MARK: - def public func defComputed(_ getter: @escaping ComputedGetter, setter: ComputedSetter? = nil) -> ComputedValue { - let computedValue = ComputedValue(getter: getter, setter: setter) - return computedValue + ComputedValue(getter: getter, setter: setter) } diff --git a/Sources/Water/Reactivity/Effect.swift b/Sources/Water/Reactivity/Effect.swift index 7e00716..eeae450 100644 --- a/Sources/Water/Reactivity/Effect.swift +++ b/Sources/Water/Reactivity/Effect.swift @@ -132,7 +132,11 @@ public func stop(_ runner: ReactiveEffectRunner) { runner.stop() } -// MARK: - track and trigger reactor +// MARK: - reactor + +public func isDefined(_ value: Any) -> Bool { + return value is Reactor +} protocol Reactor: AnyObject { func trackEffects() @@ -185,6 +189,8 @@ func triggerEffects(_ effects: [AnyEffect]) { } } +// MARK: - track and trigger effect + struct ReactorEffectMap { let reactor: AnyReactor var effects: [AnyEffect] = [] diff --git a/Sources/Water/Reactivity/Handler.swift b/Sources/Water/Reactivity/Handler.swift index 8b265c5..21146c9 100644 --- a/Sources/Water/Reactivity/Handler.swift +++ b/Sources/Water/Reactivity/Handler.swift @@ -86,9 +86,14 @@ extension ReactiveHandler { return reactiveObject._target[keyPath: keyPath] } - func handleSetProperty(of reactiveObject: ReactiveObject, at keyPath: WritableKeyPath, with newValue: V) { + func handleSetProperty(of reactiveObject: ReactiveObject, at keyPath: KeyPath, with newValue: V) { assert(!isReadonly, "set property at keyPath: \(keyPath) failed, becase \(reactiveObject._target) is readonly") + guard let keyPath = keyPath as? WritableKeyPath else { + assertionFailure("set value: \(newValue) at keyPath: \(keyPath) failed, maybe define your property use 'var' not 'let'") + return + } + let oldValue = reactiveObject._target[keyPath: keyPath] if sameValue(lhs: oldValue, rhs: newValue) { return diff --git a/Sources/Water/Reactivity/Object.swift b/Sources/Water/Reactivity/Object.swift index 8644e24..7b6222d 100644 --- a/Sources/Water/Reactivity/Object.swift +++ b/Sources/Water/Reactivity/Object.swift @@ -25,19 +25,21 @@ public class ReactiveObject: Reactor { } } + // FIXME: - reactive nested public subscript(dynamicMember keyPath: KeyPath) -> V { get { -// print("get keyPath \(keyPath)") - _reactiveHandler.handleGetProperty(of: self, at: keyPath) - } - set { -// print("set keyPath \(keyPath) - new value = \(newValue)") - if let keyPath = keyPath as? WritableKeyPath { - _reactiveHandler.handleSetProperty(of: self, at: keyPath, with: newValue) + print("get keyPath \(keyPath)") + let value = _reactiveHandler.handleGetProperty(of: self, at: keyPath) + if isDefined(value), let castValue = value as? ReactiveValue { + return castValue.unwrap() } else { - fatalError("the key path is not writable") + return value } } + set { + print("set keyPath \(keyPath) - new value = \(newValue)") + _reactiveHandler.handleSetProperty(of: self, at: keyPath, with: newValue) + } } public func unwrap() -> T { diff --git a/Sources/Water/Reactivity/Value.swift b/Sources/Water/Reactivity/Value.swift index 3b639f4..0aa03a6 100644 --- a/Sources/Water/Reactivity/Value.swift +++ b/Sources/Water/Reactivity/Value.swift @@ -13,6 +13,9 @@ public class ReactiveValue: Reactor { init(value: T, handler: ReactiveHandler) { _value = value _reactiveHandler = handler + if isClass(value) { + assertionFailure("reference class type not supported") // FIXME: need more check + } } public var value: T { diff --git a/Tests/WaterTests/EffectSpec.swift b/Tests/WaterTests/EffectSpec.swift index 7b7464e..3119ba8 100644 --- a/Tests/WaterTests/EffectSpec.swift +++ b/Tests/WaterTests/EffectSpec.swift @@ -46,6 +46,25 @@ class EffectSpec: QuickSpec { expect(age1).to(equal(12)) } + it("effect with change nested object property") { + let foo = Foo(bar: "bar", user: User(age: 10)) + let observed = defReactive(foo) + + var age = 0 + var callNum = 0 + defEffect { + callNum += 1 + age = observed.user.age + } + + expect(callNum).to(equal(1)) + expect(age).to(equal(10)) + + observed.user.age = 11 + expect(callNum).to(equal(2)) + expect(age).to(equal(11)) + } + it("effect return runner") { var foo = 10 diff --git a/Tests/WaterTests/ReactiveObjectSpec.swift b/Tests/WaterTests/ReactiveObjectSpec.swift index cd23b46..e6b266e 100644 --- a/Tests/WaterTests/ReactiveObjectSpec.swift +++ b/Tests/WaterTests/ReactiveObjectSpec.swift @@ -9,7 +9,7 @@ import Nimble class ReactiveObjectSpec: QuickSpec { override class func spec() { - it("happy path struct") { + it("object should be reactive") { let origin = User(age: 10) let user = defReactive(origin) @@ -50,5 +50,29 @@ class ReactiveObjectSpec: QuickSpec { user.age = 22 expect(user.age).to(equal(22)) } + + it("change reactive object let property will assert") { + struct Foo { + let bar: Int + } + + let foo = defReactive(Foo(bar: 10)) + expect { foo.bar = 11 }.to(throwAssertion()) + } + + it("reactive with nested object should reactive") { + let foo = NestedReactiveFoo(bar: "bar", user: defReactive(User(age: 10)), array: defReactive([1, 2, 3])) + + let observed = defReactive(foo) + + expect(observed.isReactive).to(equal(true)) + expect(observed.user.isReactive).to(equal(true)) + expect(observed.array.isReactive).to(equal(true)) + } + + it("check object is defined reactor") { + let user = defReactive(User(age: 10)) + expect(isDefined(user)).to(equal(true)) + } } } diff --git a/Tests/WaterTests/ReactiveReadonlySpec.swift b/Tests/WaterTests/ReactiveReadonlySpec.swift index 37e34bf..d98ad22 100644 --- a/Tests/WaterTests/ReactiveReadonlySpec.swift +++ b/Tests/WaterTests/ReactiveReadonlySpec.swift @@ -36,6 +36,11 @@ class ReactiveReadonlySpec: QuickSpec { expect(user.isReadonly).to(equal(false)) expect(user.isReactive).to(equal(true)) } + + it("check readonly is defined reactor") { + let user = defReadonly(User(age: 10)) + expect(isDefined(user)).to(equal(true)) + } } describe("readonly / array") { diff --git a/Tests/WaterTests/ReactiveValueSpec.swift b/Tests/WaterTests/ReactiveValueSpec.swift index f141207..fb4970e 100644 --- a/Tests/WaterTests/ReactiveValueSpec.swift +++ b/Tests/WaterTests/ReactiveValueSpec.swift @@ -7,6 +7,12 @@ import Quick import Nimble @testable import Water +extension User: Equatable { + static func == (lhs: User, rhs: User) -> Bool { + lhs.age == rhs.age + } +} + class ReactiveValueSpec: QuickSpec { override class func spec() { it("should hold a value") { @@ -33,5 +39,44 @@ class ReactiveValueSpec: QuickSpec { expect(callNum).to(equal(2)) expect(b).to(equal(2)) } + + it("nested struct object should reactive") { + let user = defValue(User(age: 10)) + var dummy = 0 + var callNum = 0 + + defEffect { + callNum += 1 + dummy = user.value.age + } + + expect(callNum).to(equal(1)) + expect(dummy).to(equal(10)) + + user.value.age = 11 + expect(callNum).to(equal(2)) + expect(dummy).to(equal(11)) + + user.value.age = 11 + expect(callNum).to(equal(2)) + expect(dummy).to(equal(11)) + } + + it("nested class object should not support") { + expect { defValue(ClassUser(uname: "haha", age: 10)) }.to(throwAssertion()) + } + + it("object contains value property") { +// struct Foo { +// let name = defValue("hello") +// } +// +// let foo = defReactive(Foo()) +// +// expect(foo.name).to(equal("hello")) + +// foo.name = "123" +// expect(foo.name).to(equal("123")) + } } } diff --git a/Tests/WaterTests/TestHelpers.swift b/Tests/WaterTests/TestHelpers.swift index 8cb2b2c..7cc095c 100644 --- a/Tests/WaterTests/TestHelpers.swift +++ b/Tests/WaterTests/TestHelpers.swift @@ -3,6 +3,8 @@ // Water // +@testable import Water + struct User { var age: Int } @@ -26,3 +28,14 @@ class ClassUser { self.age = age } } + +struct Foo { + let bar: String + var user: User +} + +struct NestedReactiveFoo { + let bar: String + let user: ReactiveObject + let array: ReactiveArray +} diff --git a/docs/compare-with-x.md b/docs/compare-with-x.md new file mode 100644 index 0000000..2389dad --- /dev/null +++ b/docs/compare-with-x.md @@ -0,0 +1,16 @@ +## compare with TCA + +- TCA not good + - force to use Redux style + - store state is more than view needs + - state action must equtable + - call stack is confusing + - avoid unnecessary body recall + - combine temp state and data state + +- Water should learn + - runtime warning + - @usableFromInline @inlinable + - comments + +- Water is good \ No newline at end of file diff --git a/docs/reactivity/reactive.md b/docs/reactivity/reactive.md new file mode 100644 index 0000000..19edd94 --- /dev/null +++ b/docs/reactivity/reactive.md @@ -0,0 +1,8 @@ +# Reactive + +## simple usage + + +## nested + +- yourself must declare it as reactive \ No newline at end of file diff --git a/docs/reactivity/readonly.md b/docs/reactivity/readonly.md new file mode 100644 index 0000000..e69de29