Skip to content

Commit 2da57e3

Browse files
authored
Add State implementation (#75)
* Add State _makeProperty implementation * Add StoredLocationBase implementation * Update StoredLocationBase implementation * Complete State implementation * FIx WASI build issue
1 parent e449dd2 commit 2da57e3

File tree

13 files changed

+303
-26
lines changed

13 files changed

+303
-26
lines changed

.github/workflows/wasm.yml

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ jobs:
1515
swift_version: ["5.10-SNAPSHOT-2024-03-30-a"] # "5.10-RELEASE" is not release for WASM, tracked via https://github.com/swiftwasm/swift/issues/5570
1616
os: [ubuntu-22.04]
1717
runs-on: ${{ matrix.os }}
18+
env:
19+
OPENSWIFTUI_WERROR: 1
20+
OPENSWIFTUI_SWIFT_TESTING: 0
21+
OPENGRAPH_ATTRIBUTEGRAPH: 0
22+
OPENSWIFTUI_COMPATIBILITY_TEST: 0
23+
OPENSWIFTUI_SWIFT_LOG: 1
1824
steps:
1925
- uses: actions/checkout@v4
2026
- uses: swiftwasm/setup-swiftwasm@v1

Package.resolved

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"location" : "https://github.com/OpenSwiftUIProject/OpenGraph",
88
"state" : {
99
"branch" : "main",
10-
"revision" : "315468c6339cdbcbaf7d8797b866745d5bf11d8e"
10+
"revision" : "036b8d28a8a1220a768a81bbc20624759094137c"
1111
}
1212
},
1313
{

Sources/OpenSwiftUI/Core/Data/Location/AnyLocation.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class AnyLocation<Value>: AnyLocationBase {
1616
}
1717
func get() -> Value { fatalError() }
1818
func set(_ value: Value, transaction: Transaction) { fatalError() }
19-
func projecting<P>(_ p: P) -> AnyLocation<P.Projected> where P: Projection, P.Base == Value {
19+
func projecting<P: Projection>(_ p: P) -> AnyLocation<P.Projected> where Value == P.Base {
2020
fatalError()
2121
}
2222
func update() -> (Value, Bool) { fatalError() }

Sources/OpenSwiftUI/Core/Data/Location/LocationBox.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ class LocationBox<L: Location>: AnyLocation<L.Value> {
2727
location.set(value, transaction: transaction)
2828
}
2929

30-
override func projecting<P>(_ p: P) -> AnyLocation<P.Projected> where L.Value == P.Base, P : Projection {
31-
cache.reference(for: p, on: location)
30+
override func projecting<P: Projection>(_ projection: P) -> AnyLocation<P.Projected> where L.Value == P.Base {
31+
cache.reference(for: projection, on: location)
3232
}
3333

3434
override func update() -> (L.Value, Bool) {

Sources/OpenSwiftUI/Core/Data/Location/TODO/StoredLocationBase.swift

-1
This file was deleted.

Sources/OpenSwiftUI/Core/Data/Property/PropertyList.swift

+17-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// Status: Blocked by merge
77
// ID: 2B32D570B0B3D2A55DA9D4BFC1584D20
88

9+
internal import COpenSwiftUI
910
internal import OpenGraphShims
1011

1112
// MARK: - PropertyList
@@ -18,6 +19,11 @@ struct PropertyList: CustomStringConvertible {
1819

1920
@inlinable
2021
init() { elements = nil }
22+
23+
@inline(__always)
24+
init(elements: Element?) {
25+
self.elements = elements
26+
}
2127

2228
@usableFromInline
2329
var description: String {
@@ -86,13 +92,23 @@ struct PropertyList: CustomStringConvertible {
8692
fatalError("TODO")
8793
}
8894

89-
mutating private func override(with plist: PropertyList) {
95+
mutating func override(with plist: PropertyList) {
9096
if let element = elements {
9197
elements = element.byPrepending(plist.elements)
9298
} else {
9399
elements = plist.elements
94100
}
95101
}
102+
103+
@inline(__always)
104+
static var current: PropertyList {
105+
if let data = _threadTransactionData() {
106+
// FIXME: swift_dynamicCastClassUnconditional
107+
PropertyList(elements: data.assumingMemoryBound(to: Element.self).pointee)
108+
} else {
109+
PropertyList()
110+
}
111+
}
96112
}
97113

98114
// MARK: - PropertyList Help functions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// ThreadUtils.swift
3+
//
4+
//
5+
// Created by Kyle on 2024/4/21.
6+
//
7+
8+
import Foundation
9+
10+
@inline(__always)
11+
func performOnMainThread(_ block: @escaping () -> Void) {
12+
#if os(WASI)
13+
// See #76: Thread and RunLoopMode.common is not available on WASI currently
14+
block()
15+
#else
16+
if Thread.isMainThread {
17+
block()
18+
} else {
19+
RunLoop.main.perform(inModes: [.common], block: block)
20+
}
21+
#endif
22+
}

Sources/OpenSwiftUI/Data/Model/Binding/Binding.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,12 @@ extension Binding: DynamicProperty {
343343
}
344344
}
345345

346-
public static func _makeProperty<V>(in buffer: inout _DynamicPropertyBuffer, container: _GraphValue<V>, fieldOffset: Int, inputs: inout _GraphInputs) {
346+
public static func _makeProperty<V>(
347+
in buffer: inout _DynamicPropertyBuffer,
348+
container _: _GraphValue<V>,
349+
fieldOffset: Int,
350+
inputs _: inout _GraphInputs
351+
) {
347352
buffer.append(Box(), fieldOffset: fieldOffset)
348353
}
349354
}

Sources/OpenSwiftUI/Data/Model/DynamicProperty/DynamicPropertyBox.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
protocol DynamicPropertyBox<Property>: DynamicProperty {
99
associatedtype Property: DynamicProperty
1010
func destroy()
11-
func reset()
11+
mutating func reset()
1212
mutating func update(property: inout Property, phase: _GraphInputs.Phase) -> Bool
1313
func getState<Value>(type: Value.Type) -> Binding<Value>?
1414
}

Sources/OpenSwiftUI/Data/Model/State/State.swift

+49-6
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
// OpenSwiftUI
44
//
55
// Audited for RELEASE_2021
6-
// Status: Blocked by DynamicProperty
6+
// Status: Complete
77
// ID: 08168374F4710A99DCB15B5E8768D632
88

9+
internal import OpenGraphShims
10+
911
/// A property wrapper type that can read and write a value managed by OpenSwiftUI.
1012
///
1113
/// Use state as the single source of truth for a given value type that you
@@ -206,11 +208,6 @@ public struct State<Value> {
206208
}
207209
}
208210

209-
extension State: DynamicProperty {
210-
// TODO:
211-
public static func _makeProperty(in _: inout _DynamicPropertyBuffer, container _: _GraphValue<some Any>, fieldOffset _: Swift.Int, inputs _: inout _GraphInputs) {}
212-
}
213-
214211
extension State where Value: ExpressibleByNilLiteral {
215212
/// Creates a state property without an initial value.
216213
///
@@ -237,3 +234,49 @@ extension State {
237234
}
238235
}
239236
}
237+
238+
extension State: DynamicProperty {
239+
public static func _makeProperty<V>(
240+
in buffer: inout _DynamicPropertyBuffer,
241+
container _: _GraphValue<V>,
242+
fieldOffset: Int,
243+
inputs _: inout _GraphInputs
244+
) {
245+
let attribute = Attribute(value: ())
246+
let box = StatePropertyBox<Value>(signal: WeakAttribute(attribute))
247+
buffer.append(box, fieldOffset: fieldOffset)
248+
}
249+
}
250+
251+
private struct StatePropertyBox<Value>: DynamicPropertyBox {
252+
let signal: WeakAttribute<Void>
253+
var location: StoredLocation<Value>?
254+
255+
typealias Property = State<Value>
256+
func destroy() {}
257+
mutating func reset() { location = nil }
258+
mutating func update(property: inout State<Value>, phase: _GraphInputs.Phase) -> Bool {
259+
let locationChanged = location == nil
260+
if location == nil {
261+
location = property._location as? StoredLocation ?? StoredLocation(
262+
initialValue: property._value,
263+
host: .currentHost,
264+
signal: signal
265+
)
266+
}
267+
let signalChanged = signal.changedValue()?.changed ?? false
268+
property._value = location!.updateValue
269+
property._location = location!
270+
return (signalChanged ? location!.wasRead : false) || locationChanged
271+
}
272+
func getState<V>(type _: V.Type) -> Binding<V>? {
273+
guard Value.self == V.self,
274+
let location
275+
else {
276+
return nil
277+
}
278+
let value = location.get()
279+
let binding = Binding(value: value, location: location)
280+
return binding as? Binding<V>
281+
}
282+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
//
2+
// StoredLocation.swift
3+
// OpenSwiftUI
4+
//
5+
// Audited for RELEASE_2021
6+
// Status: WIP
7+
// ID: EBDC911C9EE054BAE3D86F947C24B7C3
8+
9+
internal import OpenGraphShims
10+
internal import COpenSwiftUI
11+
12+
class StoredLocationBase<Value>: AnyLocation<Value>, Location {
13+
private struct LockedData {
14+
var currentValue: Value
15+
var savedValue: [Value]
16+
var cache: LocationProjectionCache
17+
18+
init(currentValue: Value, savedValue: [Value], cache: LocationProjectionCache = LocationProjectionCache()) {
19+
self.currentValue = currentValue
20+
self.savedValue = savedValue
21+
self.cache = cache
22+
}
23+
}
24+
25+
fileprivate struct BeginUpdate: GraphMutation {
26+
weak var box: StoredLocationBase<Value>?
27+
28+
func apply() {
29+
box?.beginUpdate()
30+
}
31+
32+
func combine<Mutation: GraphMutation>(with mutation: Mutation) -> Bool {
33+
guard let otherBeginUpdate = mutation as? BeginUpdate,
34+
let box,
35+
let otherBox = otherBeginUpdate.box,
36+
box === otherBox
37+
else {
38+
return false
39+
}
40+
41+
box.$data.withMutableData { data in
42+
_ = data.savedValue.removeFirst()
43+
}
44+
return true
45+
}
46+
}
47+
48+
@UnsafeLockedPointer
49+
private var data: LockedData
50+
51+
var _wasRead: Bool
52+
53+
init(initialValue value: Value) {
54+
_wasRead = false
55+
_data = UnsafeLockedPointer(wrappedValue: LockedData(currentValue: value, savedValue: []))
56+
super.init()
57+
}
58+
59+
fileprivate var isValid: Bool { true }
60+
61+
// MARK: - abstract method
62+
63+
fileprivate var isUpdating: Bool {
64+
fatalError("abstract")
65+
}
66+
67+
fileprivate func commit(transaction: Transaction, mutation: BeginUpdate) {
68+
fatalError("abstract")
69+
}
70+
71+
fileprivate func notifyObservers() {
72+
fatalError("abstract")
73+
}
74+
75+
// MARK: - AnyLocation
76+
77+
override var wasRead: Bool {
78+
get { _wasRead }
79+
set { _wasRead = newValue }
80+
}
81+
82+
override func get() -> Value {
83+
data.currentValue
84+
}
85+
86+
override func set(_ value: Value, transaction: Transaction) {
87+
guard !isUpdating else {
88+
Log.runtimeIssues("Modifying state during view update, this will cause undefined behavior.")
89+
return
90+
}
91+
guard isValid else {
92+
$data.withMutableData { data in
93+
data.savedValue.removeAll()
94+
}
95+
return
96+
}
97+
let shouldCommit = $data.withMutableData { data in
98+
guard !compareValues(data.currentValue, value) else {
99+
return false
100+
}
101+
data.savedValue.append(data.currentValue)
102+
data.currentValue = value
103+
return true
104+
}
105+
guard shouldCommit else {
106+
return
107+
}
108+
var newTransaction = transaction
109+
newTransaction.override(.current)
110+
performOnMainThread { [weak self] in
111+
guard let self else {
112+
return
113+
}
114+
let update = BeginUpdate(box: self)
115+
commit(transaction: newTransaction, mutation: update)
116+
}
117+
}
118+
119+
override func projecting<P: Projection>(_ projection: P) -> AnyLocation<P.Projected> where Value == P.Base {
120+
data.cache.reference(for: projection, on: self)
121+
}
122+
123+
override func update() -> (Value, Bool) {
124+
_wasRead = true
125+
return (updateValue, true)
126+
}
127+
128+
// MARK: - final properties and methods
129+
130+
deinit {
131+
$data.destroy()
132+
}
133+
134+
final var updateValue: Value {
135+
$data.withMutableData { data in
136+
data.savedValue.first ?? data.currentValue
137+
}
138+
}
139+
140+
private final func beginUpdate() {
141+
data.savedValue.removeFirst()
142+
notifyObservers()
143+
}
144+
}
145+
146+
final class StoredLocation<Value>: StoredLocationBase<Value> {
147+
weak var host: GraphHost?
148+
@WeakAttribute var signal: Void?
149+
150+
init(initialValue value: Value, host: GraphHost?, signal: WeakAttribute<Void>) {
151+
self.host = host
152+
_signal = signal
153+
super.init(initialValue: value)
154+
}
155+
156+
override fileprivate var isValid: Bool {
157+
host?.isValid ?? false
158+
}
159+
160+
override fileprivate var isUpdating: Bool {
161+
host?.isUpdating ?? false
162+
}
163+
164+
override fileprivate func commit(transaction: Transaction, mutation: StoredLocationBase<Value>.BeginUpdate) {
165+
host?.asyncTransaction(
166+
transaction,
167+
mutation: mutation,
168+
style: ._1,
169+
mayDeferUpdate: true
170+
)
171+
}
172+
173+
override fileprivate func notifyObservers() {
174+
$signal?.invalidateValue()
175+
}
176+
}

0 commit comments

Comments
 (0)