Skip to content
This repository was archived by the owner on Aug 22, 2025. It is now read-only.

Commit fdae42e

Browse files
authored
Fix the API error "can't mutate self is immutable" for BindKeyed in SwiftUI view (#31)
Since `KeyedValueBinding` was a variable on the wrapper and wrapper was a variable on view that is structure — semantically we were mutating self. I moved it to the wrapper itself, and projectedValue now returns the wrapper, and added a subscript that is non mutating semantically.
1 parent 6a21824 commit fdae42e

File tree

8 files changed

+127
-108
lines changed

8 files changed

+127
-108
lines changed

Decide-Tests/Container/KeyedState_Tests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ import DecideTesting
9393
XCTAssertEqual(sut.updatesCount, 3)
9494
XCTAssertEqual(sut.str[id], newValue)
9595
XCTAssertEqual(sut.strMutableObserve[id], newValue)
96-
XCTAssertEqual(sut.strMutable[id].wrappedValue, newValue)
96+
XCTAssertEqual(sut.strMutable[id], newValue)
9797
}
9898

9999
func test_Observation_BindSet() async {
@@ -107,12 +107,12 @@ import DecideTesting
107107

108108
let newValue = "\(#function)-modified"
109109

110-
sut2.strMutable[id].wrappedValue = newValue
110+
sut2.strMutable[id] = newValue
111111
env.setValue(newValue, \State.$str, at: id)
112112

113113

114114
XCTAssertEqual(sut.updatesCount, 2)
115-
XCTAssertEqual(sut.strMutable[id].wrappedValue, newValue)
115+
XCTAssertEqual(sut.strMutable[id], newValue)
116116
XCTAssertEqual(sut.str[id], newValue)
117117
XCTAssertEqual(sut.strMutableObserve[id], newValue)
118118
}

Decide-Tests/SwiftUI_Tests.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Decide package open source project
4+
//
5+
// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package
6+
// open source project authors
7+
// Licensed under MIT
8+
//
9+
// See LICENSE.txt for license information
10+
//
11+
// SPDX-License-Identifier: MIT
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import SwiftUI
16+
import Decide
17+
import XCTest
18+
import DecideTesting
19+
20+
@MainActor final class SwiftUI_Tests: XCTestCase {
21+
22+
final class State: KeyedState<Int> {
23+
@Property var str = "str-default"
24+
@Mutable @Property var strMutable = "strMutable-default"
25+
}
26+
27+
struct ViewUnderTest: View {
28+
@BindKeyed(\State.$strMutable) var strMutable
29+
@ObserveKeyed(\State.$str) var str
30+
@ObserveKeyed(\State.$strMutable) var strMutableObserved
31+
32+
var body: some View {
33+
TextField("", text: strMutable[1])
34+
Text(str[1])
35+
Text(strMutableObserved[1])
36+
}
37+
}
38+
39+
}
40+

Decide/Accessor/Default/DefaultBindKeyed.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,30 @@ import Foundation
5959
set { fatalError() }
6060
}
6161
}
62+
63+
@MainActor public struct KeyedValueBinding<I: Hashable, S: KeyedState<I>, Value> {
64+
unowned var environment: ApplicationEnvironment
65+
66+
let observer: Observer
67+
let propertyKeyPath: KeyPath<S, Property<Value>>
68+
69+
init(
70+
bind propertyKeyPath: KeyPath<S, Property<Value>>,
71+
observer: Observer,
72+
environment: ApplicationEnvironment
73+
) {
74+
self.propertyKeyPath = propertyKeyPath
75+
self.observer = observer
76+
self.environment = environment
77+
}
78+
79+
public subscript(_ identifier: I) -> Value {
80+
get {
81+
environment.subscribe(observer, on: propertyKeyPath, at: identifier)
82+
return environment.getValue(propertyKeyPath, at: identifier)
83+
}
84+
set {
85+
environment.setValue(newValue, propertyKeyPath, at: identifier)
86+
}
87+
}
88+
}

Decide/Accessor/Default/DefaultObserveKeyed.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,27 @@ import Foundation
6969
set { fatalError() }
7070
}
7171
}
72+
73+
@MainActor public struct KeyedValueObserve<I: Hashable, S: KeyedState<I>, Value> {
74+
unowned var environment: ApplicationEnvironment
75+
76+
let observer: Observer
77+
let propertyKeyPath: KeyPath<S, Property<Value>>
78+
79+
init(
80+
bind propertyKeyPath: KeyPath<S, Property<Value>>,
81+
observer: Observer,
82+
environment: ApplicationEnvironment
83+
) {
84+
self.propertyKeyPath = propertyKeyPath
85+
self.observer = observer
86+
self.environment = environment
87+
}
88+
89+
public subscript(_ identifier: I) -> Value {
90+
get {
91+
environment.subscribe(observer, on: propertyKeyPath, at: identifier)
92+
return environment.getValue(propertyKeyPath, at: identifier)
93+
}
94+
}
95+
}

Decide/Accessor/KeyValueBinding.swift

Lines changed: 0 additions & 44 deletions
This file was deleted.

Decide/Accessor/KeyValueObserve.swift

Lines changed: 0 additions & 41 deletions
This file was deleted.

Decide/Accessor/SwiftUI/BindKeyed.swift

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,29 @@ import SwiftUI
2727
self.propertyKeyPath = propertyKeyPath.appending(path: \.wrappedValue)
2828
}
2929

30-
public lazy var wrappedValue: KeyedValueObserve<I, S, Value> = {
31-
return KeyedValueObserve(
32-
bind: propertyKeyPath,
33-
observer: Observer(observer),
34-
environment: environment
30+
public subscript(_ identifier: I) -> Binding<Value> {
31+
Binding<Value>(
32+
get: {
33+
environment.subscribe(
34+
Observer(observer),
35+
on: propertyKeyPath,
36+
at: identifier)
37+
return environment.getValue(propertyKeyPath, at: identifier)
38+
},
39+
set: {
40+
return environment.setValue($0, propertyKeyPath, at: identifier)
41+
}
3542
)
36-
}()
43+
}
3744

38-
public lazy var projectedValue: KeyedValueBinding<I, S, Value> = {
39-
return KeyedValueBinding(
40-
bind: propertyKeyPath,
41-
observer: Observer(observer),
42-
environment: environment
43-
)
44-
}()
45+
public subscript(_ identifier: I) -> Value {
46+
get {
47+
environment.subscribe(Observer(observer), on: propertyKeyPath, at: identifier)
48+
return environment.getValue(propertyKeyPath, at: identifier)
49+
}
50+
}
51+
52+
public var wrappedValue: Self {
53+
self
54+
}
4555
}

Decide/Accessor/SwiftUI/ObserveKeyed.swift

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,14 @@ import SwiftUI
3737
self.propertyKeyPath = propertyKeyPath.appending(path: \.wrappedValue)
3838
}
3939

40-
public lazy var wrappedValue: KeyedValueObserve<I, S, Value> = {
41-
return KeyedValueObserve(
42-
bind: propertyKeyPath,
43-
observer: Observer(observer),
44-
environment: environment
45-
)
46-
}()
40+
public subscript(_ identifier: I) -> Value {
41+
get {
42+
environment.subscribe(Observer(observer), on: propertyKeyPath, at: identifier)
43+
return environment.getValue(propertyKeyPath, at: identifier)
44+
}
45+
}
46+
47+
public var wrappedValue: Self {
48+
self
49+
}
4750
}

0 commit comments

Comments
 (0)