Skip to content

Commit 452155f

Browse files
authored
Fix exception when using AppStorageKey with URL Value (#3098)
1 parent 3a77664 commit 452155f

File tree

2 files changed

+53
-2
lines changed

2 files changed

+53
-2
lines changed

Sources/ComposableArchitecture/SharedState/PersistenceKey/AppStorageKey.swift

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ public struct AppStorageKey<Value> {
193193

194194
public init(_ key: String) where Value == URL {
195195
@Dependency(\.defaultAppStorage) var store
196-
self.lookup = CastableLookup()
196+
self.lookup = URLLookup()
197197
self.key = key
198198
self.store = store
199199
}
@@ -251,7 +251,7 @@ public struct AppStorageKey<Value> {
251251

252252
public init(_ key: String) where Value == URL? {
253253
@Dependency(\.defaultAppStorage) var store
254-
self.lookup = OptionalLookup(base: CastableLookup())
254+
self.lookup = OptionalLookup(base: URLLookup())
255255
self.key = key
256256
self.store = store
257257
}
@@ -384,6 +384,30 @@ private struct CastableLookup<Value>: Lookup {
384384
}
385385
}
386386

387+
/// Lookup implementation tuned for URL values.
388+
/// For URLs, dedicated UserDefaults APIs for getting/setting need to be called that convert the URL from/to Data.
389+
/// Calling setValue with a URL causes a NSInvalidArgumentException exception.
390+
private struct URLLookup: Lookup {
391+
typealias Value = URL
392+
393+
func loadValue(from store: UserDefaults, at key: String, default defaultValue: URL?) -> URL? {
394+
guard let value = store.url(forKey: key)
395+
else {
396+
SharedAppStorageLocals.$isSetting.withValue(true) {
397+
store.set(defaultValue, forKey: key)
398+
}
399+
return defaultValue
400+
}
401+
return value
402+
}
403+
404+
func saveValue(_ newValue: URL, to store: UserDefaults, at key: String) {
405+
SharedAppStorageLocals.$isSetting.withValue(true) {
406+
store.set(newValue, forKey: key)
407+
}
408+
}
409+
}
410+
387411
private struct RawRepresentableLookup<Value: RawRepresentable, Base: Lookup>: Lookup
388412
where Value.RawValue == Base.Value {
389413
let base: Base

Tests/ComposableArchitectureTests/AppStorageTests.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,33 @@ final class AppStorageTests: XCTestCase {
2424
XCTAssertEqual(defaults.integer(forKey: "count"), 43)
2525
}
2626

27+
func testDefaultsReadURL() {
28+
@Dependency(\.defaultAppStorage) var defaults
29+
defaults.set(URL(string: "https://pointfree.co"), forKey: "url")
30+
@Shared(.appStorage("url")) var url: URL?
31+
XCTAssertEqual(url, URL(string: "https://pointfree.co"))
32+
}
33+
34+
func testDefaultsRegistered_URL() {
35+
@Dependency(\.defaultAppStorage) var defaults
36+
@Shared(.appStorage("url")) var url: URL = URL(string: "https://pointfree.co")!
37+
XCTAssertEqual(defaults.url(forKey: "url"), URL(string: "https://pointfree.co")!)
38+
39+
url = URL(string: "https://example.com")!
40+
XCTAssertEqual(url, URL(string: "https://example.com")!)
41+
XCTAssertEqual(defaults.url(forKey: "url"), URL(string: "https://example.com")!)
42+
}
43+
44+
func testDefaultsRegistered_Optional_URL() {
45+
@Dependency(\.defaultAppStorage) var defaults
46+
@Shared(.appStorage("url")) var url: URL? = URL(string: "https://pointfree.co")
47+
XCTAssertEqual(defaults.url(forKey: "url"), URL(string: "https://pointfree.co"))
48+
49+
url = URL(string: "https://example.com")
50+
XCTAssertEqual(url, URL(string: "https://example.com"))
51+
XCTAssertEqual(defaults.url(forKey: "url"), URL(string: "https://example.com"))
52+
}
53+
2754
func testDefaultsRegistered_Optional() {
2855
@Dependency(\.defaultAppStorage) var defaults
2956
@Shared(.appStorage("data")) var data: Data?

0 commit comments

Comments
 (0)