Skip to content

Commit

Permalink
Replace StorageCodable with StorageValue
Browse files Browse the repository at this point in the history
  • Loading branch information
danielsaidi committed Jun 29, 2024
1 parent 935a7d1 commit e32ce12
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 98 deletions.
64 changes: 64 additions & 0 deletions Sources/SwiftUIKit/Data/Collection+RawRepresentable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// Collection+RawRepresentable.swift
// SwiftUIKit
//
// Created by Daniel Saidi on 2023-04-24.
// Copyright © 2023-2024 Daniel Saidi. All rights reserved.
//
// Inspiration: https://nilcoalescing.com/blog/SaveCustomCodableTypesInAppStorageOrSceneStorage/
//

import Foundation
import SwiftUI

/// This extension makes `Array` able to store Codable types,
/// by serializing the collection to JSON.
///
/// > Important: JSON encoding may cause important type data
/// to disappear. For instance, JSON encoding a `Color` will
/// not include any information about alternate color values
/// for light and dark mode, high constrasts, etc.
extension Array: RawRepresentable where Element: Codable {

public init?(rawValue: String) {
guard
let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data)
else { return nil }
self = result
}

public var rawValue: String {
guard
let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else { return "" }
return result
}
}

/// This extension makes `Dictionary` able to store `Codable`
/// types, by serializing the collection to JSON.
///
/// > Important: JSON encoding may cause important type data
/// to disappear. For instance, JSON encoding a `Color` will
/// not include any information about alternate color values
/// for light and dark mode, high constrasts, etc.
extension Dictionary: RawRepresentable where Key: Codable, Value: Codable {

public init?(rawValue: String) {
guard
let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Key: Value].self, from: data)
else { return nil }
self = result
}

public var rawValue: String {
guard
let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else { return "{}" }
return result
}
}
90 changes: 0 additions & 90 deletions Sources/SwiftUIKit/Data/StorageCodable.swift

This file was deleted.

87 changes: 87 additions & 0 deletions Sources/SwiftUIKit/Data/StorageValue.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//
// StorageValue.swift
// SwiftUIKit
//
// Created by Daniel Saidi on 2023-04-24.
// Copyright © 2023-2024 Daniel Saidi. All rights reserved.
//
// Inspiration: https://nilcoalescing.com/blog/SaveCustomCodableTypesInAppStorageOrSceneStorage/
//

import Foundation
import SwiftUI

/// This type can wrap any `Codable` type to make it able to
/// store the value in `AppStorage` and `SceneStorage`.
///
/// This type uses `JSONEncoder` and `JSONDecoder` to encode
/// the value to data then decode it back to the source type.
///
/// > Important: JSON encoding may cause important type data
/// to disappear. For instance, JSON encoding a `Color` will
/// not include any information about alternate color values
/// for light and dark mode, high constrasts, etc.
public struct StorageValue<Value: Codable>: RawRepresentable {

public let value: Value
}

/// This is a shorthand for ``StorageValue``.
public typealias AppStorageValue = StorageValue

/// This is a shorthand for ``StorageValue``.
public typealias SceneStorageValue = StorageValue

public extension StorageValue {

init(_ value: Value) {
self.value = value
}

init?(rawValue: String) {
guard
let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(Value.self, from: data)
else { return nil }
self = .init(result)
}

var rawValue: String {
guard
let data = try? JSONEncoder().encode(value),
let result = String(data: data, encoding: .utf8)
else { return "" }
return result
}
}

private struct User: Codable, Identifiable {

var name: String
var age: Int

var id: String { name }
}

#Preview {

struct Preview: View {

@AppStorage("com.swiftuikit.appstorage.user")
var user: AppStorageValue<User>?

var body: some View {
Text(user?.value.name ?? "-")

Button("Toggle user") {
if user == nil {
user = .init(User(name: "Daniel", age: 45))
} else {
user = nil
}
}
}
}

return Preview()
}
3 changes: 3 additions & 0 deletions Sources/SwiftUIKit/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,9 @@
}
}
}
},
"Toggle user" : {

}
},
"version" : "1.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// AppStorageCodableTests.swift
// StorageCodableTests.swift
// SwiftUIKit
//
// Created by Daniel Saidi on 2023-05-30.
Expand All @@ -10,28 +10,43 @@ import SwiftUI
import SwiftUIKit
import XCTest

private struct MyCodable: Codable, Identifiable {
private struct User: Codable, Identifiable {

var name: String
var age: Int

var id: String { name }
}

private class MyState: ObservableObject {

@AppStorage("com.example.appstorage.array", store: .standard)
var array: [MyCodable]?
@AppStorage("com.swiftuikit.appstorage.object", store: .standard)
var object: StorageValue<User>?

@AppStorage("com.example.appstorage.dict", store: .standard)
var dictionary: [MyCodable.ID: MyCodable]?
@AppStorage("com.swiftuikit.appstorage.array", store: .standard)
var array: [User]?

@AppStorage("com.swiftuikit.appstorage.dict", store: .standard)
var dictionary: [User.ID: User]?
}

final class AppStorageCodableTests: XCTestCase {

private let value = User(name: "Daniel", age: 45)

func testCanPersistObject() {
let state1 = MyState()
XCTAssertNil(state1.object)
state1.object = .init(value)
let state2 = MyState()
XCTAssertEqual(state2.object?.value.name, "Daniel")
state2.object = nil
}

func testCanPersistArray() {
let state1 = MyState()
XCTAssertNil(state1.array)
state1.array = [.init(name: "Daniel")]
state1.array = [value]
let state2 = MyState()
XCTAssertEqual(state2.array?.first?.name, "Daniel")
state2.array = nil
Expand All @@ -40,7 +55,7 @@ final class AppStorageCodableTests: XCTestCase {
func testCanPersistDictionary() {
let state1 = MyState()
XCTAssertNil(state1.dictionary)
state1.dictionary = ["foo": .init(name: "Daniel")]
state1.dictionary = ["foo": value]
let state2 = MyState()
XCTAssertEqual(state2.dictionary?["foo"]?.name, "Daniel")
state2.dictionary = nil
Expand Down

0 comments on commit e32ce12

Please sign in to comment.