Skip to content

Commit d25d92d

Browse files
committed
Improve @UserStorage
1 parent 7660b86 commit d25d92d

File tree

4 files changed

+133
-6
lines changed

4 files changed

+133
-6
lines changed

Package.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ let package = Package(
2929
dependencies: [
3030
"_SwiftUIX"
3131
]
32+
),
33+
.testTarget(
34+
name: "SwiftUIXTests",
35+
dependencies: ["SwiftUIX"],
36+
path: "Tests"
3237
)
3338
]
3439
)

Sources/SwiftUIX/Intramodular/Dynamic Properties/UserStorage.swift

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ public struct UserStorage<Value: Codable>: DynamicProperty {
3535
)
3636
}
3737

38-
public func update() {
38+
public mutating func update() {
39+
self.valueBox._SwiftUI_DynamicProperty_update_called = true
3940
self.valueBox._readInitial()
4041
}
4142

@@ -112,6 +113,8 @@ extension UserStorage: Equatable where Value: Equatable {
112113

113114
extension UserStorage {
114115
private class ValueBox: ObservableObject {
116+
fileprivate var _SwiftUI_DynamicProperty_update_called: Bool = false
117+
115118
private let key: String
116119
private let defaultValue: Value
117120
private let store: UserDefaults
@@ -126,17 +129,17 @@ extension UserStorage {
126129

127130
var value: Value {
128131
get {
129-
storedValue ?? defaultValue
132+
_readLatest()
130133
} set {
131134
do {
132135
objectWillChange.send()
133136

134137
storedValue = newValue
135138

136139
_isEncodingValueToStore = true
137-
140+
138141
try store.encode(newValue, forKey: key)
139-
142+
140143
_isEncodingValueToStore = false
141144
} catch {
142145
if _isStrict {
@@ -162,6 +165,30 @@ extension UserStorage {
162165
self._isStrict = _isStrict
163166
}
164167

168+
fileprivate func _readLatest() -> Value {
169+
if !_SwiftUI_DynamicProperty_update_called {
170+
if storedValue == nil && storeSubscription == nil {
171+
_readInitial()
172+
}
173+
}
174+
175+
let result: Value?
176+
177+
if _SwiftUI_DynamicProperty_update_called {
178+
result = storedValue ?? defaultValue
179+
} else {
180+
do {
181+
result = try self.store.decode(Value.self, forKey: key)
182+
} catch {
183+
debugPrint(error)
184+
185+
result = nil
186+
}
187+
}
188+
189+
return result ?? defaultValue
190+
}
191+
165192
fileprivate func _readInitial() {
166193
guard storeSubscription == nil else {
167194
return
@@ -178,9 +205,9 @@ extension UserStorage {
178205
.filter { _ in
179206
!self._isEncodingValueToStore
180207
}
181-
.map {
208+
.map { (value: Any) -> Value? in
182209
do {
183-
return try self.store.decode(Value.self, from: $0)
210+
return try self.store.decode(Value.self, from: value)
184211
} catch {
185212
self.handleError(error)
186213

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//
2+
// Copyright (c) Vatsal Manot
3+
//
4+
5+
import SwiftUIX
6+
import XCTest
7+
8+
final class UserStorageTests: XCTestCase {
9+
private func resetUserDefaults() {
10+
UserDefaults.standard.removeObject(forKey: "foo")
11+
UserDefaults.standard.removeObject(forKey: "bar")
12+
UserDefaults.standard.removeObject(forKey: "baz")
13+
UserDefaults.standard.synchronize()
14+
}
15+
16+
func testSimpleValue() {
17+
resetUserDefaults()
18+
19+
let testModel = TestObservableObject()
20+
let testModel2 = TestObservableObject()
21+
22+
XCTAssertEqual(testModel.foo, 42)
23+
XCTAssertEqual(testModel2.foo, 42)
24+
25+
UserDefaults.standard.setValue(69 as Int, forKey: "foo")
26+
27+
XCTAssert(UserDefaults.standard.value(forKey: "foo") as! Int == 69)
28+
29+
XCTAssertEqual(testModel.foo, 69)
30+
XCTAssertEqual(testModel2.foo, 69)
31+
32+
testModel.foo = 4269
33+
34+
XCTAssertEqual(UserDefaults.standard.value(forKey: "foo") as! Int, 4269)
35+
XCTAssertEqual(testModel2.foo, 4269)
36+
37+
resetUserDefaults()
38+
39+
XCTAssertEqual(testModel.foo, 42)
40+
XCTAssertEqual(testModel2.foo, 42)
41+
}
42+
43+
func testComplexValue() {
44+
let testModel = TestObservableObject()
45+
let testModel2 = TestObservableObject()
46+
47+
var newValue = TestObservableObject.Bar(x: 69, y: 69)
48+
49+
testModel2.bar = newValue
50+
51+
XCTAssertEqual(testModel2.bar, newValue)
52+
53+
newValue = TestObservableObject.Bar(x: 42, y: 42)
54+
55+
testModel2.bar = newValue
56+
57+
XCTAssertEqual(testModel.bar, newValue)
58+
59+
resetUserDefaults()
60+
61+
XCTAssertEqual(testModel.bar, TestObservableObject.Bar())
62+
XCTAssertEqual(testModel2.bar, TestObservableObject.Bar())
63+
}
64+
}
65+
66+
extension UserStorageTests {
67+
final class TestObservableObject: ObservableObject {
68+
@UserStorage("foo", store: .standard)
69+
var foo: Int = 42
70+
71+
struct Bar: Codable, Hashable, Sendable {
72+
let x: Int
73+
let y: Int
74+
75+
init(x: Int = 0, y: Int = 0) {
76+
self.x = x
77+
self.y = y
78+
}
79+
}
80+
81+
@UserStorage("bar", store: .standard)
82+
var bar: Bar = Bar()
83+
}
84+
}

Tests/module.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//
2+
// Copyright (c) Vatsal Manot
3+
//
4+
5+
import Foundation
6+
7+
extension UserDefaults {
8+
public static var SwiftUIX: UserDefaults = {
9+
UserDefaults(suiteName: "com.vmanot.SwiftUIX-Tests")!
10+
}()
11+
}

0 commit comments

Comments
 (0)