Skip to content

Commit

Permalink
Version 1: New API (#40)
Browse files Browse the repository at this point in the history
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
MaximBazarov authored Dec 31, 2023
1 parent 5aa66e7 commit ffc2901
Show file tree
Hide file tree
Showing 32 changed files with 547 additions and 1,079 deletions.
File renamed without changes.
File renamed without changes.
40 changes: 40 additions & 0 deletions Decide-Tests copy/SwiftUI_Tests.swift
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])
}
}

}

37 changes: 28 additions & 9 deletions Decide-Tests/SwiftUI_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,39 @@ import DecideTesting

@MainActor final class SwiftUI_Tests: XCTestCase {

final class Storage: KeyedStorage<Int> {
@ObservableState var str = "str-default"
@Mutable @ObservableState var strMutable = "strMutable-default"
final class Storage: StateRoot {
unowned var environment: Decide.SharedEnvironment
init(environment: Decide.SharedEnvironment) {
self.environment = environment
}

@ObservableValue
@Persistent
var str = "str-default"

func doTest() {
}
}

struct UpdateStr: ValueDecision {
var newValue: String

func mutate(_ env: Decide.DecisionEnvironment) {
// env[\.Storage.$str] = newValue
}
}

struct ViewUnderTest: View {
@BindKeyed(\Storage.$strMutable) var strMutable
@ObserveKeyed(\Storage.$str) var str
@ObserveKeyed(\Storage.$strMutable) var strMutableObserved
@SwiftUIBind(
\Storage.$str,
mutate: UpdateStr.self
) var str

var body: some View {
TextField("", text: strMutable[1])
Text(str[1])
Text(strMutableObserved[1])
EmptyView()
// TextField("", text: $str)
// Text(str[1])
// Text(strMutableObserved[1])
}
}

Expand Down
124 changes: 124 additions & 0 deletions Decide/Binding/Bind.swift
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() }
}
}
64 changes: 64 additions & 0 deletions Decide/Decision/Decision.swift
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))
}
}
36 changes: 36 additions & 0 deletions Decide/Effect/Effect.swift
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) + ")"
}
}
45 changes: 0 additions & 45 deletions Decide/Environment/DefaultEnvironment.swift

This file was deleted.

Loading

0 comments on commit ffc2901

Please sign in to comment.