Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
  • Loading branch information
muukii committed Feb 13, 2025
1 parent b0a70bd commit 151bfd3
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 119 deletions.
50 changes: 31 additions & 19 deletions Sources/Verge/Store/StoreType+SwiftUI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,32 @@ extension BindingDerived {

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension StoreDriverType where Self : Sendable {

public func binding() -> SwiftUI.Binding<Scope> {
.init(get: { [self /* source store lives until binding released */] in
return self.state.primitive
}, set: { [weak self] value in
self?.commit {
$0 = value

/// Generates a SwiftUI.Binding that gets and updates the StoreType.State.
/// Usage:
///
/// TextField("hoge", text: store.binding(\.inputingText))
///
/// - Warning: Still in experimentals.
/// - Parameters:
/// - keypath: A property of the state to be bound.
/// - mutation: A closure to update the state.
/// If the closure is nil, state will be automatically updated.
/// - Returns: The result of binding
public func binding<T>(_ keyPath: WritableKeyPath<Scope, T> & Sendable) -> SwiftUI.Binding<T> {
.init(
get: { [self /* source store lives until binding released */] in
return self.state.primitive[keyPath: keyPath]
}, set: { [weak self] value in
self?.commit {
$0[keyPath: keyPath] = value
}
}
})
)
}
}

extension StoreDriverType {

/// Generates a SwiftUI.Binding that gets and updates the StoreType.State.
/// Usage:
Expand All @@ -65,23 +81,19 @@ extension StoreDriverType where Self : Sendable {
/// - mutation: A closure to update the state.
/// If the closure is nil, state will be automatically updated.
/// - Returns: The result of binding
public func binding<T>(_ keyPath: WritableKeyPath<Scope, T> & Sendable, with mutation: (@Sendable (T) -> Void)? = nil) -> SwiftUI.Binding<T> {

public nonisolated func binding<T>(_ keyPath: WritableKeyPath<Scope, T>) -> SwiftUI.Binding<T> {
.init(
get: { [self /* source store lives until binding released */] in
return self.state.primitive[keyPath: keyPath]

Check warning on line 87 in Sources/Verge/Store/StoreType+SwiftUI.swift

View workflow job for this annotation

GitHub Actions / test

capture of 'self' with non-sendable type 'Self' in a `@Sendable` closure

Check warning on line 87 in Sources/Verge/Store/StoreType+SwiftUI.swift

View workflow job for this annotation

GitHub Actions / test

capture of 'keyPath' with non-sendable type 'WritableKeyPath<Self.Scope, T>' in a `@Sendable` closure
}, set: { [weak self] value in
if let mutation = mutation {
mutation(value)
} else {
self?.commit {
$0[keyPath: keyPath] = value
}
}, set: { [weak self, keyPath] value in
self?.commit { [keyPath] in

Check warning on line 89 in Sources/Verge/Store/StoreType+SwiftUI.swift

View workflow job for this annotation

GitHub Actions / test

capture of 'self' with non-sendable type 'Self?' in a `@Sendable` closure
$0[keyPath: keyPath] = value
}
}
)

)
}

}


#endif
239 changes: 139 additions & 100 deletions Sources/Verge/SwiftUI/StoreReader.swift
Original file line number Diff line number Diff line change
@@ -1,29 +1,56 @@
import Combine
import Foundation
import SwiftUI
import StateStruct
import SwiftUI

/**
For SwiftUI - A View that reads a ``Store`` including ``Derived``.
It updates its content when reading properties have been updated.

Technically, it observes what properties used in making content closure as KeyPath.
``ReadTracker`` can get those using dynamicMemberLookup.
Store emits events of updated state, StoreReader filters them with current using KeyPaths.
Therefore functions of the state are not available in this situation.
A view that reads the state from Store and displays content according to the state.
The view subscribes to the state updates and updates the content when the state changes.

The state requires `@Tracking` macro to be used.

```swift
@Tracking
struct State {
var count: Int = 0
}
```

If you have nested types, you can use `@Tracking` macro to the nested types.
Then the StoreReader can track through the nested types.

```swift
@Tracking
struct Nested {
var count: Int = 0
}

@Tracking
struct State {
var nested: Nested = .init()
}

## How to make Binding

Use ``StoreBindable`` to make binding.

```swift
@StoreBindable var store = store
$store.count
```
*/
@available(iOS 14, watchOS 7.0, tvOS 14, *)
public struct StoreReader<State: TrackingObject, Activity: Sendable, Content: View>: View {

private let store: Store<State, Activity>

@SwiftUI.State private var version: UInt64 = 0

private let file: StaticString
private let line: UInt

private let content: @MainActor (State) -> Content

/// Initialize from `Store`
///
/// - Parameters:
Expand All @@ -35,18 +62,18 @@ public struct StoreReader<State: TrackingObject, Activity: Sendable, Content: Vi
_ store: Driver,
@ViewBuilder content: @escaping @MainActor (State) -> Content
) where State == Driver.TargetStore.State, Activity == Driver.TargetStore.Activity {

let store = store.store.asStore()

self.init(
file: file,
line: line,
store: store,
content: content
)

}

private init(
file: StaticString,
line: UInt,
Expand All @@ -58,135 +85,147 @@ public struct StoreReader<State: TrackingObject, Activity: Sendable, Content: Vi
self.store = store
self.content = content
}

public var body: some View {

// trigger to subscribe
let _ = $version.wrappedValue

let _content = store.tracking(content, onChange: {
ImmediateMainActorTargetQueue.main.execute {
version &+= 1
}
})


let _content = store.tracking(
content,
onChange: {
ImmediateMainActorTargetQueue.main.execute {
version &+= 1
}
})

_content
}

}

public enum StoreReaderComponents<StateType: Equatable> {

// Proxy
@dynamicMemberLookup
public struct StateProxy: ~Copyable {

typealias Detectors = [PartialKeyPath<StateType> : (Changes<StateType>) -> Bool]

private let wrapped: ReadonlyBox<StateType>

private(set) var detectors: Detectors = [:]
private weak var source: (any StoreDriverType<StateType>)?

init(
wrapped: ReadonlyBox<StateType>,
source: (any StoreDriverType<StateType>)?
) {
self.wrapped = wrapped
self.source = source
}
@propertyWrapper
@dynamicMemberLookup
public struct StoreBindable<StoreDriver: StoreDriverType & Sendable> {

public subscript<T>(dynamicMember keyPath: WritableKeyPath<StateType, T>) -> Binding<T> {
binding(keyPath)
}
private let storeDriver: StoreDriver

public func binding<T>(_ keyPath: WritableKeyPath<StateType, T>) -> SwiftUI.Binding<T> {
return .init { [value = self.wrapped.value[keyPath: keyPath]] in
return value
} set: { [weak source = self.source, keyPath] newValue, _ in
source?.commit { [keyPath] state in
state[keyPath: keyPath] = newValue
}
public init(
wrappedValue: StoreDriver
) {
self.storeDriver = wrappedValue
}

public var wrappedValue: StoreDriver {
storeDriver
}

public var projectedValue: Self {
self
}

public subscript<T>(dynamicMember keyPath: WritableKeyPath<StoreDriver.Scope, T>) -> Binding<T> {
binding(keyPath)
}

public func binding<T>(_ keyPath: WritableKeyPath<StoreDriver.Scope, T>) -> SwiftUI.Binding<T> {
let currentValue = storeDriver.state.primitive[keyPath: keyPath]
return .init {
return currentValue
} set: { [weak storeDriver] newValue, _ in
storeDriver?.commit { [keyPath] state in
state[keyPath: keyPath] = newValue
}
}
}

public func binding<T: Sendable>(_ keyPath: WritableKeyPath<StoreDriver.Scope, T> & Sendable)
-> SwiftUI.Binding<T>
{
return .init { [currentValue = storeDriver.state.primitive[keyPath: keyPath]] in
return currentValue
} set: { [weak storeDriver] newValue, _ in
storeDriver?.commit { [keyPath] state in
state[keyPath: keyPath] = newValue
}
}
}

}

#if DEBUG

@available(iOS 14, watchOS 7.0, tvOS 14, *)
enum Preview_StoreReader: PreviewProvider {
@available(iOS 14, watchOS 7.0, tvOS 14, *)
enum Preview_StoreReader: PreviewProvider {

static var previews: some View {
static var previews: some View {

Group {
Content()
}

Group {
Content()
}

}
struct Content: View {

struct Content: View {
@StoreObject var viewModel_1: ViewModel = .init()
@StoreObject var viewModel_2: ViewModel = .init()

@StoreObject var viewModel_1: ViewModel = .init()
@StoreObject var viewModel_2: ViewModel = .init()
@State var flag = false

@State var flag = false
var body: some View {

var body: some View {
VStack {

VStack {
let store = flag ? viewModel_1 : viewModel_2

let store = flag ? viewModel_1 : viewModel_2
StoreReader(store) { state in
Text(state.count.description)
}

StoreReader(store) { state in
Text(state.count.description)
}
Button("up") {
store.increment()
}

Button("up") {
store.increment()
}
Button("swap") {
flag.toggle()
}

Button("swap") {
flag.toggle()
}

}
}
}

final class ViewModel: StoreDriverType {
final class ViewModel: StoreDriverType {

@Tracking
struct State: Equatable {
var count: Int = 0
var count_dummy: Int = 0
}
@Tracking
struct State: Equatable {
var count: Int = 0
var count_dummy: Int = 0
}

let store: Store<State, Never>
let store: Store<State, Never>

init() {
self.store = .init(initialState: .init())
}
init() {
self.store = .init(initialState: .init())
}

func increment() {
commit {
$0.count += 1
func increment() {
commit {
$0.count += 1
}
}
}

func incrementDummy() {
commit {
$0.count_dummy += 1
func incrementDummy() {
commit {
$0.count_dummy += 1
}
}
}

deinit {
print("deinit")
deinit {
print("deinit")
}
}
}

}
}

#endif
Loading

0 comments on commit 151bfd3

Please sign in to comment.