From 452155f129a08155e41bf3c0f84d27ac4a1a5951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Wszeborowski?= Date: Fri, 17 May 2024 00:00:32 +0200 Subject: [PATCH] Fix exception when using AppStorageKey with URL Value (#3098) --- .../PersistenceKey/AppStorageKey.swift | 28 +++++++++++++++++-- .../AppStorageTests.swift | 27 ++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/Sources/ComposableArchitecture/SharedState/PersistenceKey/AppStorageKey.swift b/Sources/ComposableArchitecture/SharedState/PersistenceKey/AppStorageKey.swift index e2d02a2d5a02..1f432972a332 100644 --- a/Sources/ComposableArchitecture/SharedState/PersistenceKey/AppStorageKey.swift +++ b/Sources/ComposableArchitecture/SharedState/PersistenceKey/AppStorageKey.swift @@ -193,7 +193,7 @@ public struct AppStorageKey { public init(_ key: String) where Value == URL { @Dependency(\.defaultAppStorage) var store - self.lookup = CastableLookup() + self.lookup = URLLookup() self.key = key self.store = store } @@ -251,7 +251,7 @@ public struct AppStorageKey { public init(_ key: String) where Value == URL? { @Dependency(\.defaultAppStorage) var store - self.lookup = OptionalLookup(base: CastableLookup()) + self.lookup = OptionalLookup(base: URLLookup()) self.key = key self.store = store } @@ -384,6 +384,30 @@ private struct CastableLookup: Lookup { } } +/// Lookup implementation tuned for URL values. +/// For URLs, dedicated UserDefaults APIs for getting/setting need to be called that convert the URL from/to Data. +/// Calling setValue with a URL causes a NSInvalidArgumentException exception. +private struct URLLookup: Lookup { + typealias Value = URL + + func loadValue(from store: UserDefaults, at key: String, default defaultValue: URL?) -> URL? { + guard let value = store.url(forKey: key) + else { + SharedAppStorageLocals.$isSetting.withValue(true) { + store.set(defaultValue, forKey: key) + } + return defaultValue + } + return value + } + + func saveValue(_ newValue: URL, to store: UserDefaults, at key: String) { + SharedAppStorageLocals.$isSetting.withValue(true) { + store.set(newValue, forKey: key) + } + } +} + private struct RawRepresentableLookup: Lookup where Value.RawValue == Base.Value { let base: Base diff --git a/Tests/ComposableArchitectureTests/AppStorageTests.swift b/Tests/ComposableArchitectureTests/AppStorageTests.swift index dbe5a1417c42..31d39de0eed8 100644 --- a/Tests/ComposableArchitectureTests/AppStorageTests.swift +++ b/Tests/ComposableArchitectureTests/AppStorageTests.swift @@ -24,6 +24,33 @@ final class AppStorageTests: XCTestCase { XCTAssertEqual(defaults.integer(forKey: "count"), 43) } + func testDefaultsReadURL() { + @Dependency(\.defaultAppStorage) var defaults + defaults.set(URL(string: "https://pointfree.co"), forKey: "url") + @Shared(.appStorage("url")) var url: URL? + XCTAssertEqual(url, URL(string: "https://pointfree.co")) + } + + func testDefaultsRegistered_URL() { + @Dependency(\.defaultAppStorage) var defaults + @Shared(.appStorage("url")) var url: URL = URL(string: "https://pointfree.co")! + XCTAssertEqual(defaults.url(forKey: "url"), URL(string: "https://pointfree.co")!) + + url = URL(string: "https://example.com")! + XCTAssertEqual(url, URL(string: "https://example.com")!) + XCTAssertEqual(defaults.url(forKey: "url"), URL(string: "https://example.com")!) + } + + func testDefaultsRegistered_Optional_URL() { + @Dependency(\.defaultAppStorage) var defaults + @Shared(.appStorage("url")) var url: URL? = URL(string: "https://pointfree.co") + XCTAssertEqual(defaults.url(forKey: "url"), URL(string: "https://pointfree.co")) + + url = URL(string: "https://example.com") + XCTAssertEqual(url, URL(string: "https://example.com")) + XCTAssertEqual(defaults.url(forKey: "url"), URL(string: "https://example.com")) + } + func testDefaultsRegistered_Optional() { @Dependency(\.defaultAppStorage) var defaults @Shared(.appStorage("data")) var data: Data?