Property Wrapper + Combine で UserDefaults を複数のプロパティに同期＆監視する

iOS14では `@AppStorage`というUserDefaultsを同期するものが追加されるようです（多分）。
この記事ではそれと似たようなものを作ります。目標は以下の通り。

* ユーザー定義の型も読み書きできるようにする(別記事紹介)
* UserDefaultsの変更を自動で反映する。複数の離れた箇所のプロパティであっても、キー文字列が同じなら値が同期されるようにする
* 変更をCombine.Publisherとして監視できるようにする

最初に実装に必要な前提知識の紹介をして、そのあと具体的なコードを載せています。
[Swift Package Managerのライブラリとして公開もしています](https://github.com/ryotapoi/UserDefaultPropertyWrapper)。

# 実装に必要な前提知識

## UserDefaultsでstructを読み書きする
UserDefaultsでユーザー定義のstructやenumや`Int?`なども扱えると便利です。
そのため`UserDefaultCompatible`を用意して、以下のコードで読み書きができるようにしています。

```swift
public protocol UserDefaultsProtocol : NSObject {
    func value<Value : UserDefaultCompatible>(type: Value.Type, forKey key: String, default defaultValue: Value) -> Value
    func setValue<Value : UserDefaultCompatible>(_ value: Value, forKey key: String)
}

extension UserDefaults : UserDefaultsProtocol {
    public func value<Value : UserDefaultCompatible>(type: Value.Type = Value.self, forKey key: String, default defaultValue: Value) -> Value {
        guard let object = object(forKey: key) else { return defaultValue }
        return Value(userDefaultObject: object) ?? defaultValue
    }
    public func setValue<Value : UserDefaultCompatible>(_ value: Value, forKey key: String) {
        set(value.toUserDefaultObject(), forKey: key)
    }
}
```

長くなるため`UserDefaultCompatible`の詳細は省略します。興味のある方は「UserDefaultsをProperty Wrapperでカッコよく使う」の[UserDefaultsに読み書き可能な型の作成](https://qiita.com/ryotapoi/items/2f861f734a4d48aad487#userdefaultsに読み書き可能な型の作成)をご覧ください。（**宣伝**）

## UserDefaultsの変更を監視する
KVOを使うとUserDefaultsの値の変更を監視できます。

```swift
// KVOでUserDefaultsの監視開始
userDefaults.addObserver(self, forKeyPath: "key", options: .new, context: nil)
```

```swift
// KVO監視で変更があったときに呼ばれる
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    // 変更された時の処理
}
```

KeyPathを使うKVOもありますが、キーの文字列で変更を受け取りたいため、昔ながらの方法で監視します。

## Combine.Publisherの実装にCurrentValueSubjectを使う
Publisherとして値の変更を受け取れるようにします。地道にSubscriptionなどを実装することも可能ですが、ほとんどの処理を`CurrentValueSubject`に移譲すると簡単に実装可能です。

# Property Wrapperのコードサンプル
Publisherの実装に`CurrentValueSubject`を使っているため、結構コンパクトなコードだと思います。

```Publisher.swift
extension UserDefaults {
    public class Publisher<Output : UserDefaultCompatible> : NSObject, Combine.Publisher {
        public typealias Failure = Never // エラーは発生しないものとする
        
        private let key: String // キー文字列
        private let defaultValue: Output // キー文字列に対応する値がない場合の値
        private let userDefaults: UserDefaultsProtocol // UserDefaultsの本体
        private let subject: CurrentValueSubject<Output, Never> // CurrentValueSubjectに処理を移譲して簡単に実装する

        public var value: Output {
            get { subject.value }
            set {
                if newValue != subject.value {
                    subject.value = newValue
                    // 値が設定された時はUserDefaultsに保存する
                    userDefaults.setValue(newValue, forKey: key)
                }
            }
        }

        // テストなどでUserDefaultsに値を保存したくない時に備え、念のためUserDefaultsProtocolを使う
        // ただテストを考慮しても、あまり必要ない気がしている
        public init(key: String, default defaultValue: Output, userDefaults: UserDefaultsProtocol = UserDefaults.standard) {
            self.key = key
            self.defaultValue = defaultValue
            self.userDefaults = userDefaults
            self.subject = .init(userDefaults.value(type: Output.self, forKey: key, default: defaultValue))
            super.init()
            // KVOでUserDefaultsの監視開始
            userDefaults.addObserver(self, forKeyPath: key, options: .new, context: nil)
        }
        
        deinit {
            //　KVO監視終了
            userDefaults.removeObserver(self, forKeyPath: key)
        }
        
        // KVO監視で変更があったときに呼ばれる
        public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
            // UserDefaultsへのアクセスを減らすため、changeのnewKeyから値を取り出している
            //
            // `subject.value = userDefaults.value(type: Output.self, forKey: key, default: defaultValue)`
            // するだけの方がコードが読みやすく、おそらく問題も起きないと思う。
            if keyPath == key {
                if let newObject = change?[.newKey] {
                    value = Output(userDefaultObject: newObject) ?? defaultValue
                } else {
                    value = defaultValue
                }
            }
        }

        // Publisherの必須メソッド
        // 何も考えずCurrentValueSubjectに処理を移譲する
        public func receive<S>(
            subscriber: S
        ) where S : Combine.Subscriber, Failure == S.Failure, Output == S.Input {
            subject.receive(subscriber: subscriber)
        }
    }
}
```

```PropertyWrapper.swift
@propertyWrapper
public struct UserDefault<Value : UserDefaultCompatible> {
    private let publisher: UserDefaults.Publisher<Value>

    // 最初の引数 `wrappedValue` はプロパティの初期化の値が渡される
    // それ以降の引数は `@UserDefault("key", userDefaults: UserDefaults.standard)` としたときの引数が渡される
    public init(wrappedValue defaultValue: Value, _ key: String, userDefaults: UserDefaultsProtocol = UserDefaults.standard) {
        publisher = .init(key: key, default: defaultValue, userDefaults: userDefaults)
    }

    // プロパティの値の読み書き時はこれが呼ばれる
    public var wrappedValue: Value {
        get { publisher.value }
        set { publisher.value = newValue }
    }
    
    // プロパティに$を付けた時はこれが呼ばれる
    public var projectedValue: UserDefaults.Publisher<Value> { publisher }
}
```

# 使い方
実践的ではない例ですが、以下があったとして

```swift
struct First {
    @UserDefault("number")
    var number: Int = 10
}

struct Second {
    @UserDefault("number")
    var number: Int = 10
}

var first = First()
var second = Second()
```

## プロパティの自動同期と監視
```swift
first.$number.sink { print(#line, $0) }

first.number = 20
first.number // => 20
second.number // => 20

second.number = 30
first.number // => 30
second.number // => 30
```

`first` `second`どちらに値を入れても勝手に同期されます。
`sink`では初期値の10に続けて、20,30も出力されます。

## assign
```swift
let publisher = Publishers.Sequence<[Int], Never>(sequence: [40, 50, 60])
let cancelable = publisher.assign(to: \.value, on: first.$number)
first.number // => 60
second.number // => 60
```

assignでKeyPath `\.value` に値を流すことができます。
`sink`には40,50,60が流れてきます。

## Combine依存だけで読み書き
```swift
let input: PassthroughSubject<Int, Never> = .init()
let cancellable: AnyCancellable = input.assign(to: \.value, on: first.$number)
let output: AnyPublisher<Int, Never> = publisher.eraseToAnyPublisher()

output.sink { print(#line, $0) }
input.send(70)
```

* 書き込みを`PassthroughSubject`
* 読み込みを`AnyPublisher`

とするとCombineの依存だけになり（`@UserDefault`に依存せず）読み書き可能になります。`@UserDefault`をあちこちに書くのは嫌だけど、Combineへの依存はあちこちにあってもOKという考えはありだと思います。

ただ`AnyPublisher`を使うことはありそうですが、書き込みを`PassthroughSubject`でする需要はあるのでしょうか……

`assign`時のanyCancellableをどこかで保持する必要があるのが面倒です。
書き込みは、単純なメソッドやクロージャとして公開する方がいいかもしれません。

もし`PassthroughSubject`によるinputが使いたい！　となった場合は`UserDefaults.Publisher`本体にinputを埋め込みAnyCancellableを保持させるほうが楽です。

## まとめて定義
もちろんどこかのenumやstructに設定値をまとめて定義することもできます。
キー文字列をtypoしないようになど考えるとまとめて定義して必要なところに渡すほうがいいかもしれません。

# 終わりに
iOS14がリリースされれば、iOS13以降になってCombineが使える…といいな。

PassthroughSubjectによるinput必要かはまだ悩んでます🤔