-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Public API revamp, provide a way of multiple wrappers be stacked. ```swift @ObservableValue @RemoteValue(.backend(.getUserName)) @Persistent(.secure("com.user.name")) var str = "str-default" ``` Also changing the mutability: ```swift @SwiftUIBind( \Storage.$str, mutate: UpdateStr.self ) var str ``` So `@Mutable` wrapper is removed, instead, decisions are the only way to mutate the state. `UpdateStr` is a special type of decision, that takes a `newValue` as a parameter: ```swift struct UpdateStr: ValueDecision { var newValue: String func mutate(_ env: Decide.DecisionEnvironment) { env[\.Storage.$str] = newValue } } ``` this way the mutability is expressed explicitly and gives ability to combine other decisions and side effects on value updates. **Verbosity:** right now this version requires quite a sophisticated code to be written, e.g. `SwiftUIBind`, later the plan is to utilise swift macros to write this code for the user.
- Loading branch information
1 parent
5aa66e7
commit ffc2901
Showing
32 changed files
with
547 additions
and
1,079 deletions.
There are no files selected for viewing
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
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,40 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Decide package open source project | ||
// | ||
// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package | ||
// open source project authors | ||
// Licensed under MIT | ||
// | ||
// See LICENSE.txt for license information | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import SwiftUI | ||
import Decide | ||
import XCTest | ||
import DecideTesting | ||
|
||
@MainActor final class SwiftUI_Tests: XCTestCase { | ||
|
||
final class Storage: KeyedStorage<Int> { | ||
@ObservableState var str = "str-default" | ||
@Mutable @ObservableState var strMutable = "strMutable-default" | ||
} | ||
|
||
struct ViewUnderTest: View { | ||
@BindKeyed(\Storage.$strMutable) var strMutable | ||
@ObserveKeyed(\Storage.$str) var str | ||
@ObserveKeyed(\Storage.$strMutable) var strMutableObserved | ||
|
||
var body: some View { | ||
TextField("", text: strMutable[1]) | ||
Text(str[1]) | ||
Text(strMutableObserved[1]) | ||
} | ||
} | ||
|
||
} | ||
|
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
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,124 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Decide package open source project | ||
// | ||
// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package | ||
// open source project authors | ||
// Licensed under MIT | ||
// | ||
// See LICENSE.txt for license information | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
final class ChangesPublisher: ObservableObject {} | ||
|
||
#if canImport(SwiftUI) | ||
import SwiftUI | ||
|
||
@propertyWrapper | ||
@MainActor | ||
public struct SwiftUIBind< | ||
Root: StateRoot, | ||
Value, | ||
Mutation: ValueDecision | ||
>: DynamicProperty { | ||
@SwiftUI.Environment(\.sharedEnvironment) var environment | ||
@ObservedObject var publisher = ChangesPublisher() | ||
|
||
public var wrappedValue: Value { | ||
get { | ||
environment | ||
.get(Root.self)[keyPath: statePath] | ||
.getValueSubscribing( | ||
observer: Observer(publisher) { [weak publisher] in | ||
publisher?.objectWillChange.send() | ||
} | ||
) | ||
} | ||
set { | ||
environment | ||
.get(Root.self)[keyPath: statePath] | ||
.set(value: newValue) | ||
} | ||
} | ||
|
||
let statePath: KeyPath<Root, ObservableValue<Value>> | ||
let mutate: Mutation.Type | ||
|
||
public init( | ||
_ statePath: KeyPath<Root, ObservableValue<Value>>, | ||
mutate: Mutation.Type | ||
) { | ||
self.statePath = statePath | ||
self.mutate = mutate | ||
} | ||
} | ||
#endif | ||
|
||
public protocol ObservingEnvironmentObject: AnyObject { | ||
var environment: SharedEnvironment { get set} | ||
var onChange: () -> Void { get } | ||
} | ||
|
||
/** | ||
(!) Limited support for non SwiftUI objects, | ||
caveat is that each value this object observes will call the `onUpdate`. | ||
it will lead to multiple updates even when there should be one update. | ||
Might cause to many renderings in UIKit views. | ||
TODO: Improve support merging updates in one update, | ||
may be throttling to one per 0.5 sec )(60sec/120framesPerSec) | ||
*/ | ||
@propertyWrapper | ||
@MainActor | ||
public final class Bind<Root, Value> where Root: StateRoot { | ||
|
||
let statePath: KeyPath<Root, ObservableValue<Value>> | ||
|
||
var environment = SharedEnvironment.default | ||
|
||
public init( | ||
_ statePath: KeyPath<Root, ObservableValue<Value>>, | ||
file: StaticString = #fileID, | ||
line: UInt = #line | ||
) { | ||
self.statePath = statePath | ||
} | ||
|
||
public static subscript<EnclosingObject>( | ||
_enclosingInstance instance: EnclosingObject, | ||
wrapped wrappedKeyPath: KeyPath<EnclosingObject, Value>, | ||
storage storageKeyPath: KeyPath<EnclosingObject, Bind> | ||
) -> Value | ||
where EnclosingObject: ObservingEnvironmentObject | ||
{ | ||
get { | ||
let wrapperInstance = instance[keyPath: storageKeyPath] | ||
let root = wrapperInstance.environment.get(Root.self) | ||
let observableValue = root[keyPath: wrapperInstance.statePath] | ||
|
||
#warning(""" | ||
TODO: Squash updates of any values this instance is subscribed to, | ||
to one update to instance. | ||
""") | ||
let observer = Observer(wrapperInstance) { [weak instance] in | ||
instance?.onChange() | ||
} | ||
return observableValue.getValueSubscribing(observer: observer) | ||
} | ||
set { | ||
let wrapperInstance = instance[keyPath: storageKeyPath] | ||
let root = wrapperInstance.environment.get(Root.self) | ||
let observableValue = root[keyPath: wrapperInstance.statePath] | ||
observableValue.set(value: newValue) | ||
} | ||
} | ||
@available(*, unavailable, message: "@DefaultBind can only be enclosed by EnvironmentObservingObject.") | ||
public var wrappedValue: Value { | ||
get { fatalError() } | ||
set { fatalError() } | ||
} | ||
} |
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,64 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Decide package open source project | ||
// | ||
// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package | ||
// open source project authors | ||
// Licensed under MIT | ||
// | ||
// See LICENSE.txt for license information | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import Foundation | ||
|
||
public typealias EnvironmentMutation = (DecisionEnvironment) -> Void | ||
|
||
/// Encapsulates values updates applied to the ``ApplicationEnvironment`` immediately. | ||
/// Provided with an ``DecisionEnvironment`` to read and write state. | ||
/// Might return an array of ``Effect``, that will be performed asynchronously | ||
/// within the ``ApplicationEnvironment``. | ||
@MainActor public protocol Decision { | ||
func mutate(_ env: DecisionEnvironment) -> Void | ||
} | ||
|
||
|
||
/// Decision that has a `newValue` to use in `mutate`. | ||
@MainActor public protocol ValueDecision: Decision { | ||
associatedtype Value | ||
var newValue: Value { get } | ||
} | ||
|
||
/// A restricted interface of ``ApplicationEnvironment`` provided to ``Decision``. | ||
@MainActor public final class DecisionEnvironment { | ||
|
||
/** | ||
TODO: Implement isolation, creating a new instance of environment, | ||
that reads value form itself or uses a value from the original environment. | ||
|
||
Storing updated keys is a problem tho | ||
May be storing mutations isn't a bad idea | ||
|
||
But so tempting to remove the transaction part. | ||
*/ | ||
|
||
unowned var environment: SharedEnvironment | ||
|
||
var effects = [Effect]() | ||
|
||
init(_ environment: SharedEnvironment) { | ||
self.environment = environment | ||
} | ||
} | ||
|
||
extension Decision { | ||
var debugDescription: String { | ||
String(reflecting: self) | ||
} | ||
|
||
var name: String { | ||
String(describing: type(of: self)) | ||
} | ||
} |
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,36 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Decide package open source project | ||
// | ||
// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package | ||
// open source project authors | ||
// Licensed under MIT | ||
// | ||
// See LICENSE.txt for license information | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import Foundation | ||
|
||
/// Encapsulates asynchronous execution of side-effects e.g. network call. | ||
/// Provided with an ``EffectEnvironment`` to read state and make ``Decision``s. | ||
public protocol Effect: Actor { | ||
func perform(in env: EffectEnvironment) async | ||
} | ||
|
||
/// A restricted interface of ``ApplicationEnvironment`` provided to ``Effect``. | ||
public final class EffectEnvironment { | ||
} | ||
|
||
extension Effect { | ||
public var debugDescription: String { | ||
String(reflecting: self) | ||
} | ||
|
||
nonisolated var name: String { | ||
String(describing: type(of: self)) | ||
+ " (" + String(describing: self.self) + ")" | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.