[macOS][iOS] 複数デバイス間でのデータ同期方法の紹介(1: NSUbiquitousKeyValueStore)

複数の Apple デバイス間でのデータ同期の方法を説明します。その1は、NSUbiquitousKeyValueStore です。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Monterey 12.0.1
  • Xcode 13 beta5
  • iOS 15

複数デバイスでのデータ同期

Apple のドキュメントに、CloudKit を使うのが適切かを判断する というドキュメントがあります。

# 和訳された文章はないので、日本語タイトルは私訳です。

その中では、以下の方法が紹介されています。

  • Store Data as Files
  • Store Unstructured Data That Syncs Across Devices
  • Store Objects That Sync Across Devices

それぞれ、iCloud Document, NSUbiquitousKeyValueStore, CoreData のことなんですが、面白かったので、1つづつ紹介していきます。

今回は、2つめの NSUbiquitousKeyValueStore です。

注意
NSUbiquitousKeyValueStore に限りませんが、iCloud を使うものは、Apple Developer Program への加入が必要です。

NSUbiquitousKeyValueStore

Apple のドキュメントは、こちら

key-value のペアをデバイス間で共有するための仕組みです。

イメージ的には、デバイス間で共有できる UserDefaults です。

以下のような特徴があります。

  • 保存できるデータ型は、Bool, NSNumber, NSString, NSData, NSDate, NSArray, NSDictionary
  • iCloud 経由で同期される
  • iCloud にログインしていない時は、ローカルに保存され、ログインされたら、(システムにとって)都合の良いタイミングで同期される
  • iCloud に接続されているすべてのデバイスから、書き込むことができる
  • 変更は、NotificationCenter から通知を受け取ることで検知できる
  • 全体で、1 MBまでしか保存できない。1つのキーに対しても最大値は 1MB。制限を超えて書き込もうとするとエラーとなり変更されない
  • キー値は、1024 個までしか使用できない(キー値の長さは、UTF8 で 64 byte まで。それ以上では、runtime error となる)
  • NSUbiquitousKeyValueStore を使用するアプリは、プロジェクトの設定で、NSUbiquitousKeyValueStore 使用を宣言する必要がある。

使い方

プロジェクトセットアップ

Xcode でプロジェクトに対して以下の設定が必要となります。

  • capabilities に iCloud を追加
  • iCloud の中で、"Key-value storage" にチェックを入れる
    iCloudSetupforiOS

    iCloudSetupformacOS

    iOS, macOS どちらも、設定方法は同じです。

NSUbiquitousKeyValueStore へのアクセス方法

使い方は、UserDefaults と似ています。default で共有インスタンスを取得し、操作します。

例えば bool 値を取得するのであれば、bool(forKey:String) を使用することで取得できますし、設定する時は、set(Bool, forKey:String) を使うことで可能です。

以下の例では、"Save" ボタンを押下すると NSUbiquitousKeyValueStore に Int と Bool 値を保存し、"Load" ボタンが押されると NSUbiquitousKeyValueStore から値を取得してきます。

# Int はそのままでは保存できないため、Int64 に変換して操作しています。

macOSApp

同じコードで iOS アプリでも動作します。 iOS アプリ版を作って 同時に動作させると iOS デバイスと macOS でデータを共有できていることを確認できます。


//
//  AppRootView.swift
//
//  Created by : Tomoaki Yagishita on 2021/10/25
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct AppRootView: View {
    @EnvironmentObject var ubiqStore: UbiqStore
    var body: some View {
        VStack {
            Toggle("Bool Value", isOn: $ubiqStore.boolValue)
                .padding()
            Stepper(value: $ubiqStore.intValue) {
                Text("IntValue: \(ubiqStore.intValue)")
                    .fixedSize()
            }
            .padding()
            Text(ubiqStore.message)
            Button(action: {
                ubiqStore.save()
            }, label: {Text("Save")}).padding()
            Button(action: {
                ubiqStore.load()
            }, label: {Text("Load")}).padding()
        }
        .padding(50)

    }
}

struct AppRootView_Previews: PreviewProvider {
    static var previews: some View {
        AppRootView()
    }
}

//
//  UbiqStore.swift
//
//  Created by : Tomoaki Yagishita on 2021/10/25
//  © 2021  SmallDeskSoftware
//

import Foundation
import Combine

class UbiqStore: ObservableObject {
    let boolKey = "BoolKey"
    let intKey = "IntKey"
    @Published var boolValue: Bool = false
    @Published var intValue: Int = 0
    @Published var message: String = "no message yet"
    
    var anyCalcellable: AnyCancellable? = nil
    
    func load() {
        // (1)
        self.boolValue = NSUbiquitousKeyValueStore.default.bool(forKey: boolKey)
        self.intValue = Int(NSUbiquitousKeyValueStore.default.longLong(forKey: intKey))
    }
    
    func save() {
        // (2)
        NSUbiquitousKeyValueStore.default.set(self.boolValue, forKey: boolKey)
        NSUbiquitousKeyValueStore.default.set(Int64(self.intValue), forKey: intKey)
        self.message = "Put my data into NSUbiquitousKeyValueStore at \(Date())"
    }
}
コード解説
  1. NSUbiquitousKeyValueStore.default から特定キー値に対応する値を取得しています。(存在しなかった時は各型ごとに設定されているデフォルト値が返ってきます)
  2. NSUbiquitousKeyValueStore.default へ、特定キー値の値を設定しています。

この状態で、一方(例えば iOS アプリ)で値を変更し "Save" ボタンを押下後、もう一方(例えば macOS アプリ)で "Load" ボタンを押下すると iOS アプリで設定した値が macOS アプリに反映されることを確認できます。

注意
ローカルデバイスから iCloud へデータをアップロードするタイミングは、システムが判断するため、"Save" ボタン押下 直後 には反映されないケースがあります。

外部での変更検知

NSUbiquitousKeyValueStore が変更された時は、通知を受け取ることができます。”didChangeExternallyNotification” という通知をチェックします。

addObserver を使用して通知を受け取る設定も可能ですが、せっかくなので(?) Combine を使用して通知を受け取るようにしました。


  let anyCalcellable = NotificationCenter.default.publisher(for: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: NSUbiquitousKeyValueStore.default)
      .sink(receiveValue: { newNotification in
         // NSUbiquitousKeyValueStore is updated !, need to do something
      })

受け取る通知の中には、通知理由についての情報もふくまれています。詳細のドキュメントはこちら

例えば、通知を受けた時に、先ほどの load() 関数を呼び出せば、NSUbiquitousKeyValueStore への変更を自動で ローカルに取得する契機とできます

以下は、別途 iOS アプリからの変更を受け取った時の macOS アプリの動画です。

注意点

データの同期タイミングを明示的に指定することはできません。システム側の判断に依存することになりますので、即時反映のようなタイミング要求の厳しいデータの同期には向きません。

試した範囲でも、15秒程度で反映される時もあれば、1分以上かかるケースもありました。

Apple は、以下のように言っています。(意訳です)オリジナルは こちら

重要
key-value store は、頻繁に変更されるデータの保存を目的にしていません。デバイスでテストした時に、もし key-value store に 頻繁な変更を行ったならば、多くの場合 システムは サーバーとのやりとりを減らすために 同期を遅らせるでしょう。
このことは、変更が別のデバイスに即時反映されないであろうことを意味します。

まとめ:データの同期方法 1: NSUbiquitousKeyValueStore

複数デバイス間でのデータの同期方法 1: NSUbiquitousKeyValueStore
  • Xcode で プロジェクトに設定が必要(iCloud 設定 + Key-value strage 設定)
  • NSUbiquitousKeyValueStore.default でアクセスする
  • 外部での変更検知は、NSUbiquitousKeyValueStore.didChangeExternallyNotification で可能
  • iCloud への反映は、システムがタイミングを決定するので注意する
  • サイズ制限と キー数制限 に注意する

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です