diff --git a/0296-cross-platform-pt7/Counter/Package.swift b/0296-cross-platform-pt7/Counter/Package.swift index 8023df41..e6822974 100644 --- a/0296-cross-platform-pt7/Counter/Package.swift +++ b/0296-cross-platform-pt7/Counter/Package.swift @@ -18,6 +18,14 @@ let package = Package( name: "FactClientLive", targets: ["FactClientLive"] ), + .library( + name: "StorageClient", + targets: ["StorageClient"] + ), + .library( + name: "StorageClientLive", + targets: ["StorageClientLive"] + ), ], dependencies: [ .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"), @@ -32,6 +40,7 @@ let package = Package( dependencies: [ "Counter", "FactClientLive", + "StorageClientLive", .product(name: "SwiftNavigation", package: "swift-navigation"), .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), .product(name: "JavaScriptKit", package: "JavaScriptKit"), @@ -41,6 +50,7 @@ let package = Package( name: "Counter", dependencies: [ "FactClient", + "StorageClient", .product(name: "SwiftNavigation", package: "swift-navigation"), .product(name: "Perception", package: "swift-perception") ] @@ -59,6 +69,20 @@ let package = Package( .product(name: "JavaScriptKit", package: "JavaScriptKit", condition: .when(platforms: [.wasi])), .product(name: "JavaScriptEventLoop", package: "JavaScriptKit", condition: .when(platforms: [.wasi])), ] + ), + .target( + name: "StorageClient", + dependencies: [ + .product(name: "Dependencies", package: "swift-dependencies"), + .product(name: "DependenciesMacros", package: "swift-dependencies"), + ] + ), + .target( + name: "StorageClientLive", + dependencies: [ + "StorageClient", + .product(name: "JavaScriptKit", package: "JavaScriptKit", condition: .when(platforms: [.wasi])), + ] ) ], swiftLanguageVersions: [.v6] diff --git a/0296-cross-platform-pt7/Counter/Sources/Counter/CounterModel.swift b/0296-cross-platform-pt7/Counter/Sources/Counter/CounterModel.swift index 1e1fe9db..bfd4b4d2 100644 --- a/0296-cross-platform-pt7/Counter/Sources/Counter/CounterModel.swift +++ b/0296-cross-platform-pt7/Counter/Sources/Counter/CounterModel.swift @@ -2,6 +2,7 @@ import Dependencies import FactClient import Foundation import Perception +import StorageClient import SwiftNavigation @MainActor @@ -11,6 +12,8 @@ public class CounterModel: HashableObject { @Dependency(FactClient.self) var factClient @PerceptionIgnored @Dependency(\.continuousClock) var clock + @PerceptionIgnored + @Dependency(StorageClient.self) var storageClient public var count = 0 { didSet { @@ -24,7 +27,18 @@ public class CounterModel: HashableObject { print("isTextFocused", isTextFocused) } } - public var savedFacts: [String] = [] + public var savedFacts: [String] = [] { + didSet { + do { + try storageClient.save( + JSONEncoder().encode(savedFacts), + to: .savedFactsKey + ) + } catch { + // + } + } + } public var text = "" private var timerTask: Task? @@ -40,7 +54,14 @@ public class CounterModel: HashableObject { public var id: String { value } } - public init() {} + public init() { + do { + savedFacts = try JSONDecoder().decode([String].self, from: storageClient.load(.savedFactsKey)) + } catch { + // TODO: error handling + savedFacts = [] + } + } public func incrementButtonTapped() { count += 1 @@ -119,3 +140,7 @@ public class CounterModel: HashableObject { } } } + +extension String { + fileprivate static let savedFactsKey = "saved-facts" +} diff --git a/0296-cross-platform-pt7/Counter/Sources/StorageClient/StorageClient.swift b/0296-cross-platform-pt7/Counter/Sources/StorageClient/StorageClient.swift new file mode 100644 index 00000000..fa8c5c4f --- /dev/null +++ b/0296-cross-platform-pt7/Counter/Sources/StorageClient/StorageClient.swift @@ -0,0 +1,13 @@ +import Foundation +import Dependencies +import DependenciesMacros + +@DependencyClient +public struct StorageClient: Sendable { + public var load: @Sendable (String) throws -> Data + public var save: @Sendable (Data, _ to: String) throws -> Void +} + +extension StorageClient: TestDependencyKey { + public static let testValue = StorageClient() +} diff --git a/0296-cross-platform-pt7/Counter/Sources/StorageClientLive/StorageClientLive.swift b/0296-cross-platform-pt7/Counter/Sources/StorageClientLive/StorageClientLive.swift new file mode 100644 index 00000000..d0a05d21 --- /dev/null +++ b/0296-cross-platform-pt7/Counter/Sources/StorageClientLive/StorageClientLive.swift @@ -0,0 +1,42 @@ +import Dependencies +import Foundation +import StorageClient + +#if os(WASI) +import JavaScriptKit +#endif + +extension StorageClient: DependencyKey { +#if os(WASI) + public static let liveValue = Self( + load: { key in + guard let value = JSObject.global.window.localStorage.getItem(key).string + else { + struct DataLoadingError: Error {} + throw DataLoadingError() + } + return Data(value.utf8) + }, + save: { data, key in + JSObject.global.window.localStorage.setItem( + key, + String(decoding: data, as: UTF8.self) + ) + } + ) +#else + public static let liveValue = Self( + load: { key in + let url = URL.documentsDirectory + .appendingPathComponent(key) + .appendingPathExtension("json") + return try Data(contentsOf: url) + }, + save: { data, key in + try data.write(to: URL.documentsDirectory + .appendingPathComponent(key) + .appendingPathExtension("json")) + } + ) +#endif +} diff --git a/0296-cross-platform-pt7/ModernUIKit.xcodeproj/project.pbxproj b/0296-cross-platform-pt7/ModernUIKit.xcodeproj/project.pbxproj index 4e666c16..9f04ee15 100644 --- a/0296-cross-platform-pt7/ModernUIKit.xcodeproj/project.pbxproj +++ b/0296-cross-platform-pt7/ModernUIKit.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 2A154CF22C5D56E1009FC7D9 /* CounterFeatureEpoxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A154CF12C5D56E1009FC7D9 /* CounterFeatureEpoxy.swift */; }; 2A1CEB272BFD271600753A66 /* Perception in Frameworks */ = {isa = PBXBuildFile; productRef = 2A1CEB262BFD271600753A66 /* Perception */; }; 2A6230292BFEA5C600930179 /* AppFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A6230282BFEA5C600930179 /* AppFeature.swift */; }; + 2A7507FF2C7FB39E00CAF2BA /* StorageClientLive in Frameworks */ = {isa = PBXBuildFile; productRef = 2A7507FE2C7FB39E00CAF2BA /* StorageClientLive */; }; 4B530F5C2C7F91F200B8B2F4 /* Counter in Frameworks */ = {isa = PBXBuildFile; productRef = 4B530F5B2C7F91F200B8B2F4 /* Counter */; }; 4B530F5E2C7F953D00B8B2F4 /* FactClientLive in Frameworks */ = {isa = PBXBuildFile; productRef = 4B530F5D2C7F953D00B8B2F4 /* FactClientLive */; }; 4B54C2252BFD0D1900E95174 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C2242BFD0D1900E95174 /* App.swift */; }; @@ -49,6 +50,7 @@ CA5C1D632C5932B500F8882D /* SwiftNavigation in Frameworks */, CA5C1D672C5932B500F8882D /* UIKitNavigation in Frameworks */, 4B530F5C2C7F91F200B8B2F4 /* Counter in Frameworks */, + 2A7507FF2C7FB39E00CAF2BA /* StorageClientLive in Frameworks */, 2A1CEB272BFD271600753A66 /* Perception in Frameworks */, CA5C1D652C5932B500F8882D /* SwiftUINavigation in Frameworks */, ); @@ -131,6 +133,7 @@ 2A154CEF2C5D56C7009FC7D9 /* Epoxy */, 4B530F5B2C7F91F200B8B2F4 /* Counter */, 4B530F5D2C7F953D00B8B2F4 /* FactClientLive */, + 2A7507FE2C7FB39E00CAF2BA /* StorageClientLive */, ); productName = ModernUIKit; productReference = 4B54C2212BFD0D1900E95174 /* ModernUIKit.app */; @@ -443,6 +446,10 @@ package = 2A1CEB252BFD271600753A66 /* XCRemoteSwiftPackageReference "swift-perception" */; productName = Perception; }; + 2A7507FE2C7FB39E00CAF2BA /* StorageClientLive */ = { + isa = XCSwiftPackageProductDependency; + productName = StorageClientLive; + }; 4B530F5B2C7F91F200B8B2F4 /* Counter */ = { isa = XCSwiftPackageProductDependency; productName = Counter; diff --git a/0296-cross-platform-pt7/ModernUIKit/CounterFeatureUIKit.swift b/0296-cross-platform-pt7/ModernUIKit/CounterFeatureUIKit.swift index b2b9332c..36e3ed04 100644 --- a/0296-cross-platform-pt7/ModernUIKit/CounterFeatureUIKit.swift +++ b/0296-cross-platform-pt7/ModernUIKit/CounterFeatureUIKit.swift @@ -103,6 +103,7 @@ final class CounterViewController: UIViewController { self?.model.deleteFactButtonTapped(fact: fact) }) deleteButton.setTitle("Delete", for: .normal) + deleteButton.setContentCompressionResistancePriority(.required, for: .horizontal) let factStack = UIStackView(arrangedSubviews: [ factLabel,