Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Version 1: New API #40

Merged
merged 1 commit into from
Dec 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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