Recreating @AppStorage, but for Codable types
Tags: swift, ios, combine, swiftui
Published: 2023-12-04 15:40:00

Hey, everyone! A few days ago, my buddy, who recently delved into the world of SwiftUI, messaged me: 'Seen [@AppStorage](https://developer.apple.com/documentation/swiftui/appstorage)? Pretty neat stuff, but it doesn't support [Codable](https://developer.apple.com/documentation/swiftui/appstorage) types, gotta write some with [RawRepresentable](https://developer.apple.com/documentation/swiftui/appstorage).' I was relaxing on the throne when my brain reacted to SwiftUI and pooped an idea: 'Let's create our own implementation but let's jazz it up with Codable and add some UIKit support.' Brilliant plan, right? So, I strolled back to my desk.

No hard feelings, folks! I'm just a dinosaur and a fan of UIKit. So, let's roll!

## Intro

What's `@AppStorage` and how does it work? From ancient times, there's been this thing called NSUserDefaults that can persistently store files. So, @AppStorage is a sleek and reactive wrapper around this well-known API. With a simple code snippet like `@AppStorage("key") var property: Int?`, instead of the bulky `let value = (UserDefaults.standard.value(forKey: "key") as? Int) ?? 0`, this `propertyWrapper` lets you modify `UserDefaults` by key. But the most convenient part? It instantly captures any changes, triggering a rebuild of our `View`.

So, to recreate a similar functionality, we'll need knowledge about:

- UserDefaults (NSUserDefaults)
- Combine
- Objective-C (ha-ha, yes)

## Writing the `propertyWrapper`

Let's kick things off with the simplest implementation that fetches and stores values within `UserDefaults`.

```swift
@propertyWrapper
public struct CodableStroage<ValueType> {
    public init(
        _ valueKey: String,
        defaultValue: ValueType,
        userDefaults: UserDefaults = .standard
    ) {
        self.valueKey = valueKey
        self.defaultValue = defaultValue
        self.userDefaults = userDefaults
    }

    public var defaultValue: ValueType
    public var valueKey: String
    public var userDefaults: UserDefaults

    public var wrappedValue: ValueType {
        get { userDefaults.value(forKey: valueKey) as? ValueType ?? defaultValue }
        set { userDefaults.set(newValue, forKey: valueKey) }
    }
}
```

Now, to ensure our SwiftUI `View` can receive updates and rebuild, let's leverage the `DynamicProperty` protocol and complete its implementation for our `propertyWrapper`. According to the documentation, the `update()` method in this protocol has a default implementation, so we won't have to write a lot.

```swift
extension CodableStorage: DynamicProperty {}
```

Next, let's make some updates to the `CodableStorage` implementation to get that SwiftUI magic going. To achieve this, we'll use the standard `@ObservedObject` and `ObservableObject`.

First of all, we need to create a class responsible for data storage to ensure immutability in our structure. And since we want to support `Codable` types for storage, let's immediately incorporate `JSONEncoder & JSONDecoder`. Also, let's add an `Equatable` constraint to our `ValueType` to avoid unnecessarily triggering view hierarchy updates in the future.

```swift
internal final class SubscriptionStorage<ValueType>: ObservableObject
    where
    ValueType: Codable,
    ValueType: Equatable
{
    internal init(
        encoder: JSONEncoder,
        decoder: JSONDecoder,
        valueKey: String,
        defaultValue: ValueType,
        userDefaults: UserDefaults
    ) {
        self.userDefaults = userDefaults
        self.valueKey = valueKey

        self.defaultValue = defaultValue

        // 1. Retrieving the initial value from UserDefaults
        if let data = userDefaults.data(forKey: valueKey),
           let decodedValue = try? decoder.decode(ValueType.self, from: data)
        {
            self.currentValue = decodedValue
        } else {
            self.currentValue = nil
        }

        self.encoder = encoder
        self.decoder = decoder
    }
}
```

Add an `update` function to this class where the main magic will happen with `ObservableObject`. 

```swift
internal func update(_ currentValue: ValueType) {
    guard let encodedValue = try? encoder.encode(currentValue)
    else {
        return
    }

    // 1. Checking if the value is different from the previous one
    if value != encodedValue {
        // 2. Notifying all observers about pending changes
        objectWillChange.send()
    }

    // 3. Updating the value in persistent storage
    userDefaults.setValue(encodedValue, forKey: valueKey)

    // 4. Updating our in-memory storage
    self.currentValue = currentValue
}
```

Now, let's update our main `propertyWrapper` and make use of the `SubscriptionStorage` we just created.

```swift
@propertyWrapper
public struct CodableStroage<ValueType>
    where
    // 1. Adding constraints on types to allow our SubscriptionStorage to work with them
    ValueType: Codable,
    ValueType: Equatable
{
    // Including necessary parameters in .init
    public init(
        _ valueKey: String,
        defaultValue: ValueType,
        userDefaults: UserDefaults? = nil,
        decoder: JSONDecoder? = nil,
        encoder: JSONEncoder? = nil
    ) {
        self.init(
            valueKey,
            defaultValue: defaultValue,
            userDefaults: userDefaults ?? .standart,
            decoder: decoder ?? JSONDecoder(),
            encoder: encoder ?? JSONEncoder()
        )
    }

    public var wrappedValue: ValueType {
        get { storage.value }
        // 2. Highlighting that our structure is now immutable even when values change
        nonmutating set { storage.update(newValue) }
    }

    public var defaultValue: ValueType { storage.defaultValue }
    public var valueKey: String { storage.valueKey }

    // 3. Attaching `@ObservedObject` to our `ObservableObject`
    @ObservedObject
    private var storage: SubscriptionStorage<ValueType>
}
```

Also, we can add sugar to the `CodableStorage` initializer to simplify the code for optional data types.

```swift
public init(
    _ valueKey: String,
    defaultValue: ValueType = nil,
    userDefaults: UserDefaults? = nil,
    decoder: JSONDecoder? = nil,
    encoder: JSONEncoder? = nil
) where ValueType: ExpressibleByNilLiteral {
    self.init(
        valueKey,
        defaultValue: defaultValue,
        userDefaults: userDefaults ?? .standard,
        decoder: decoder ?? JSONDecoder(),
        encoder: encoder ?? JSONEncoder()
    )
}
```

Now, there's no need to write `@CodableStorage("key", defaultValue: nil)` each time; instead, we can simply use `@CodableStorage("key")`.

## First run

![screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ahngc0n08xtzyiptfaj5.gif)

Our code is functioning, but as seen in the video, it's not quite as expected. The issue lies in the fact that two different `CodableStorage` instances have separate stores and aren't synchronized at all. Let's fix that!

## Synchronization

To synchronize different instances of our `CodableStorage`, let's utilize the good old [NSUserDefaultsDidChangeNotification](https://developer.apple.com/documentation/foundation/nsuserdefaultsdidchangenotification) API. We'll write our own `Publisher` and `Subscription`, which will respond to changes from `UserDefaults` and send this information to their subscribers.

**The Subscription object**

```swift
internal final class UserDefaultsSubscription<ValueType, SubscriberType>:
    Subscription
    where
    ValueType: Equatable,
    SubscriberType: Subscriber,
    SubscriberType.Input == ValueType,
    SubscriberType.Failure == Never
{
    internal func request(_ demand: Subscribers.Demand) {
        guard demand > 0
        else {
            return
        }

        // 1. Immediately send new data to the subscriber
        userDefaultsDidChange(userDefaults)

        // 2. Subscribe to updates from all UserDefaults and wait for changes
        subscription = NotificationCenter.default.addObserver(
            forName: UserDefaults.didChangeNotification,
            object: nil,
            queue: OperationQueue(),
            using: { [weak self] notification in
                guard let userDefaults = notification.object as? UserDefaults
                else {
                    return
                }

                // 3. Process updates
                self?.userDefaultsDidChange(userDefaults)
            }
        )
    }

    private func userDefaultsDidChange(_ userDefaults: UserDefaults) {
        // 4. Verify that the subscription exists and UserDefaults matches the one we created with
        guard let subscriber, self.userDefaults == userDefaults
        else {
            return
        }

        let updatedValue = userDefaults.object(forKey: valueKey)

        // 5. Check if the value has changed
        guard let updatedValue = updatedValue as? ValueType, currentValue != updatedValue
        else {
            return
        }

        currentValue = updatedValue

        // 6. Send
        let _ = subscriber.receive(updatedValue)
    }
}
```

**The Publisher object**

```swift
internal final class UserDefaultsPublisher<ValueType>:
    Publisher
    where
    ValueType: Equatable
{
    internal typealias Output = ValueType
    internal typealias Failure = Never

    internal func receive<S>(
        subscriber: S
    ) where S: Subscriber, S.Failure == Failure, S.Input == Output {
        subscriber.receive(subscription: UserDefaultsSubscription(
            subscriber: subscriber,
            userDefaults: userDefaults,
            valueKey: valueKey,
            currentValue: initialValue
        ))
    }
}
```

Now, we'll need to update the `SubscriptionStorage` to retrieve data directly from UserDefaults using `Combine` and `UserDefaultsPublisher`.

```swift
internal final class SubscriptionStorage<ValueType>: ObservableObject
    where
    ValueType: Codable,
    ValueType: Equatable
{
    private var cancellables: Set<AnyCancellable> = .init()

    internal init(/* ... */) {
        // 1. Create UserDefaultsPublisher and subscribe to changes in our UserDefaults
        let publisher = UserDefaultsPublisher(
            initialValue: try? encoder.encode(defaultValue),
            userDefaults: userDefaults,
            valueKey: valueKey.rawValue
        ).eraseToAnyPublisher()

        publisher
            .receive(on: DispatchQueue.main) // We can only dispatch updates to the view hierarchy from the main thread
            .sink(receiveValue: { [weak self] value in
                self?.receive(value)
            })
            .store(in: &cancellables)
    }

    internal func update(_ currentValue: ValueType) {
        guard let encodedValue = try? encoder.encode(currentValue)
        else {
            return
        }

        // 3. Now, as we'll directly receive any changes from UserDefaults, here we just need to update the value on disk
        userDefaults.setValue(encodedValue, forKey: valueKey)
    }

    private func receive(_ data: Data) {
        guard let decodedValue = try? decoder.decode(ValueType.self, from: data)
        else {
            return
        }
        // 4. Verify that the value is updated and dispatch it to our subscribers
        if value != decodedValue {
            objectWillChange.send()
        }
        currentValue = decodedValue
    }
}
```

Voilà, you're amazing! Let's conduct the next experiment and try running our code again.

![screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0up9nkci2w2ew5pjke4d.gif)

And it's not working again. How come? Where did we make a mistake? Well, time to debug. And for that, we'll cover ourselves with `print()` all over the program, haha.

![meme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/45yk03bitmezhk51i9es.jpg)

Fortunately, debugging didn't take long, and the problem was quickly localized — it within `UserDefaultsSubscription`. Who can guess where the trouble lies? Hint:

```swift
private func userDefaultsDidChange(_ userDefaults: UserDefaults) {
    guard let subscriber, self.userDefaults == userDefaults
    else {
        return
    }
    // ...
}
```

Correct, you can't simply compare `UserDefaults` like that. Even instances with the same `suiteName` are not equal! So, it seems we need to figure out a way to identify `UserDefaults` differently. Let's utilize [Hopper Disassembler](https://www.hopperapp.com) for this purpose.

> If we were using the `UserDefaults` object passed in our initializer as the argument (as `object:` parameter) in the `NotificationCenter.subscribe()` method, we wouldn't receive any notifications at all. :(

![hopper](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fy31k68m2p6bvpgnyv0h.png)

Here we discover that `NSUserDefaults` actually has a certain `_identifier_`. Well, let's try to retrieve it and make use of it. Remember when I mentioned Objective-C earlier? Well, the time has come. Now we'll be fetching the value of private properties, and for this, we'll need some runtime magic. Let's write a small extension for `UserDefaults`!

```swift
import ObjectiveC

internal extension UserDefaults {
    var identifier: String {
        guard let ivar = class_getInstanceVariable(UserDefaults.self, "_identifier_"),
              let value = object_getIvar(self, ivar) as? NSString
        else {
            #if DEBUG
            fatalError("UserDefaults API has been changed")
            #else
            return UUID().uuidString
            #endif
        }

        return value as String
    }
}
```

Using the `class_getInstanceVariable` function, we retrieve a reference to the ivar, then extract its value with `object_getIvar`. Just to be safe, we'll make the app crash (only in `DEBUG` mode) if the API has changed (although that's highly unlikely). Now, let's modify the code a bit and try running it again.

```swift
private func userDefaultsDidChange(_ userDefaults: UserDefaults) {
    guard let subscriber, self.userDefaults.identifier == userDefaults.identifier
    else {
        return
    }
    // ...
}
```

![screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q602ispmw9xq269z526f.gif)

## Excuse me, sir! Do we love UIKit?

There won't be any rocket science here, just adding one method to `SubscriptionStorage` and `CodableStorage`.

```swift
extension SubscriptionStorage {
    internal func eraseToAnyPublisher() -> AnyPublisher<ValueType, Never> {
        let decoder = decoder
        let publisher = UserDefaultsPublisher(
            initialValue: encoder._encode(defaultValue),
            userDefaults: userDefaults,
            valueKey: valueKey.rawValue
        )

        return publisher.compactMap({ data in
            decoder._decode(T.self, from: data)
        }).eraseToAnyPublisher()
    }
}

extension CodableStroage {
    public var projectedValue: CodableStroage<ValueType> { self }

    func eraseToAnyPublisher() -> AnyPublisher<ValueType, Never> {
        storage.eraseToAnyPublisher()
    }
}
```

Here, we've added `projectedValue`, which allows us to access the `propertyWrapper` by using `$` before the wrapped variable's name instead. Now, if we're writing code with UIKit, we can do something like this:

```swift
class Subview: UIView {
    init() {
        super.init(frame: .zero)
        $value.eraseToAnyPublisher().sink(receiveValue: { value in
            print(value)
        }).store(in: &cancellables)
    }

    @CodableStroage("key", defaultValue: 0)
    private var value: Int

    private var cancellables: Set<AnyCancellable> = .init()
}
```

## It seems there might be a small bug

Because `CodableStorage` can accept Optional as generic `ValueType`, there's an issue when we save our data to disk. We pass our value to JSONEncoder, and it wraps `nil`, turning it into `null`. Therefore, there might be a small problem during the decoding stage. Let's fix that! To determine that the value received at runtime as `ValueType` is actually `nil`. But we can't simply add a check like `value == nil` because the compiler will complain at us, ha-ha.

```swift
// 1. Let's write our protocol called `AnyOptional`, which will allow us to determine at runtime whether it's nil or not.
private protocol AnyOptional {
    var isEmpty: Bool { get }
}

// 2. We'll make sure all Optionals are now of type `AnyOptional`.
extension Optional: AnyOptional {
    var isEmpty: Bool { self == nil }
}

// 3. We'll write a simple function.
internal func isnil(_ instance: Any) -> Bool {
    guard let _optional = instance as? AnyOptional
    else {
        return false
    }
    return _optional.isEmpty
}
```

And now, once again (for the last time, I promise!), let's update our `SubscriptionStorage`.

```swift
internal func update(_ currentValue: ValueType) {
    guard !isnil(currentValue)
    else {
        userDefaults.removeObject(forKey: valueKey)
        return
    }

    guard let encodedValue = try? encoder.encode(currentValue)
    else {
        return
    }

    userDefaults.setValue(encodedValue, forKey: valueKey)
}
```

It looks like we're all set! Now you can use `CodableStorage` to store `Codable` types inside `UserDefaults`.

Thanks for sticking around until the end! You can check out the code for the finished library here - [0xstragner/CodableStorage](https://github.com/0xstragner/CodableStorage).