iOS13のUserDefaultsにつまずいた


# はじめに
もうネタがない...と思って色々漁っていたところ下記のようなものを見つけました:joy_cat:

[iOS 13 - Attempting to store >= 4194304 bytes of data in CFPreferences/NSUserDefaults on this platform is invalid](https://forums.developer.apple.com/thread/121527)

iOS13 で UserDefaults に 4194304 bytes 以上のデータを保存できない模様:exclamation::question:

# 検証
とりあえず検証してみました。
## 方法
1. Macでスクショを撮る（画面サイズのやつ(13328104 bytes)）
2. Xcode のプロジェクトに追加する
3. UserDefaults に画像を保存する(処理は下記)  
  
```swift
let url = Bundle.main.url(forResource: "test", withExtension: "png")!
UserDefaults.standard.set(try? Data(contentsOf: url), forKey: "image")
```

## 結果
### iOS13
コンソールに下記の警告が出ました。

>[User Defaults] CFPrefsPlistSource<0x6000024e0800> (Domain: アプリ, User: kCFPreferencesCurrentUser, ByHost: No, Container: (null), Contents Need Refresh: No): Attempting to store >= 4194304 bytes of data in CFPreferences/NSUserDefaults on this platform is invalid. This is a bug in アプリ or a library it uses

>[User Defaults] CFPrefsPlistSource<0x6000024e0800> (Domain: アプリ, User: kCFPreferencesCurrentUser, ByHost: No, Container: (null), Contents Need Refresh: No): Transitioning into direct mode

コンソールに何か出ましたが、下記の処理でデータは取れてるようでした。

```swift
if let data = UserDefaults.standard.data(forKey: "image") {
  print(data.count)
  imageView.image = UIImage(data: data)
}
```

print の表示も 13328104 なのでサイズはそのままでした。アプリを再起動しても取得できました。

その後、アプリを再起動して下記の処理を書くと再度コンソールに警告が出ました。

```swift
UserDefaults.standard.set("しんぐるとん", forKey: "Singleton")
```

>[User Defaults] CFPrefsPlistSource<0x600000df4480> (Domain: アプリ, User: kCFPreferencesCurrentUser, ByHost: No, Container: (null), Contents Need Refresh: Yes): Attempting to store >= 4194304 bytes of data in CFPreferences/NSUserDefaults on this platform is invalid. This is a bug in アプリ or a library it uses

>[User Defaults] CFPrefsPlistSource<0x600000df4480> (Domain: アプリ, User: kCFPreferencesCurrentUser, ByHost: No, Container: (null), Contents Need Refresh: No): Transitioning into direct mode

微妙に違う？？ `Contents Need Refresh: Yes` Yes になってる？？

画像データも文字列も `UserDefaults` から取得することができました.

### iOS12.4.1
コンソールには何も表示されることなく読み書きできました:tada:

## 4194304 byte とは
4194304 byte ってどれくらい保存できるんだ？ってことなんですが下記のような `struct` を作って保存すると(文字数にもよりますが...)49 byte でした。

```swift
let user = User(userID: "1234567890", userName: "SingleSingle") // 49 byte
UserDefaults.standard.write(user, key: .user)
        
struct User: Codable {
    let userID: String
    let userName: String
}

extension UserDefaults {
    enum Key: String {
        case user
    }
    
    @discardableResult
    func write<T: Codable>(_ value: T?, key: Key) -> Bool {
        let data = try? JSONEncoder().encode(value)
        set(data, forKey: key.rawValue)
        return synchronize()
    }
    
    func read<T: Codable>(_ type: T.Type, key: Key) -> T? {
        guard let data = data(forKey: key.rawValue) else {
            return nil
        }
        return try? JSONDecoder().decode(type, from: data)
    }
}
```

だいたい 85598 件くらい上記のデータが保存できるみたいです(十分な容量がありそう)。

関係ないですが `syncronize()` は[公式ドキュメント](https://developer.apple.com/documentation/foundation/userdefaults/1414005-synchronize)に
> this method is unnecessary and shouldn't be used.

とあるので必要なさそうですが担当のアプリでこれがないとバグったのでとりあえずまだ入れてます。

# 結論
ちょっと調べてみましたが公式のドキュメントは見当たりませんでした...

とりあえずは iOS12 でも iOS13 でも `UserDefaults` で 4194304 bytes 以上のデータの読み書きはできるようでした。

しかし
> Attempting to store >= 4194304 bytes of data in CFPreferences/NSUserDefaults on this platform is invalid.

という警告が出てる以上このまま使うのは良くない:neutral_face:

大きなデータは他の方法で保存するよう修正した方がいいでしょう:punch:

下記のようにとにかく `UserDefaults` 以外の保存方法に書き換えるしかなさそう？？

* [Data の `write(to:options:)`](https://developer.apple.com/documentation/foundation/data/1779858-write) でファイルに書き出す
* CoreData を使う
* [Realm](https://github.com/realm/realm-cocoa/tree/master/RealmSwift) を使う
* その他 SQLite など使う

# さいごに
そもそも `UserDefaults` にそんなでかいデータ入れる実装ってどうなんだ？というのはあります:expressionless: