アクターで Realm を使用する - Swift SDK
項目一覧
Realm Swift SDK バージョン 10.39.0 以降、Realm は、Swift アクターで Realm を使用するための組み込み機能をサポートします。 Realm のアクター サポートは、非同期作業を実行するためのスレッドまたはディスパッチ キューを管理する代替手段を提供します。 Realm をアクターとともに使用するには、次のような方法があります。
アクター分離された Realm を持つ特定のアクター上でのみRealm と連携します
アプリケーションのニーズに基づいて、アクター間で Realm を使用する
すべての Realm アクセスを 1 人のアクターに制限する場合は、アクター分離された Realm を使用することをお勧めします。 これにより、アクター境界を超えてデータを渡す必要がなくなり、データ競合のデバッグを簡素化できます。
複数のアクターで異なるタイプの作業を実行したい場合は、複数のアクター間で Realm を使用することをお勧めします。 たとえば、mainActor でオブジェクトを読み取りたいが、大規模な書き込みにはバックグラウンド アクターを使用したい場合があります。
Swiftアクターに関する一般的な情報については、 Apple のアクター ドキュメント を参照してください。
前提条件
Swift アクターで Realm を使用するには、プロジェクトが次の条件を満たす必要があります。
Realm Swift SDK バージョン 10.39.0 以降を使用
Swift 5.8/Xcode 14.3 を使用
さらに、プロジェクトで次の設定を有効にすることを強くお勧めします。
SWIFT_STRICT_CONCURRENCY=complete
: 厳密な同時実行チェックを有効にしますOTHER_SWIFT_FLAGS=-Xfrontend-enable-actor-data-race-checks
: ランタイムアクター データ競合の検出を有効にします
このページの例について
このページの例では、次のモデルを使用します。
class Todo: Object { true) var _id: ObjectId (primaryKey: var name: String var owner: String var status: String }
アクター分離された Realm を開く
Swift async/await 構文を使用して、Realm が開かれるのを待機できます。
try await Realm()
を使用して Realm を初期化すると、mainActor が分離された Realm が開きます。 あるいは、 await
構文で Realm を開くときにアクターを明示的に指定することもできます。
func mainThreadFunction() async throws { // These are identical: the async init produces a // MainActor-isolated Realm if no actor is supplied let realm1 = try await Realm() let realm2 = try await Realm(actor: MainActor.shared) try await useTheRealm(realm: realm1) }
アクター分離された Realm を開くときに、デフォルト構成を指定することも、構成をカスタマイズすることもできます。
func mainThreadFunction() async throws { let username = "Galadriel" // Customize the default realm config var config = Realm.Configuration.defaultConfiguration config.fileURL!.deleteLastPathComponent() config.fileURL!.appendPathComponent(username) config.fileURL!.appendPathExtension("realm") // Open an actor-isolated realm with a specific configuration let realm = try await Realm(configuration: config, actor: MainActor.shared) try await useTheRealm(realm: realm) }
Realm の構成に関する一般的な情報については、「 Realm の構成とオープン - Swift SDK 」を参照してください。
同期された Realm をアクター分離された Realm として開くことができます。
func mainThreadFunction() async throws { // Initialize the app client and authenticate a user let app = App(id: APPID) let user = try await app.login(credentials: Credentials.anonymous) // Configure the synced realm var flexSyncConfig = user.flexibleSyncConfiguration(initialSubscriptions: { subs in subs.append(QuerySubscription<Todo>(name: "all_todos"))}) flexSyncConfig.objectTypes = [Todo.self] // Open and use the synced realm let realm = try await Realm(configuration: flexSyncConfig, actor: MainActor.shared, downloadBeforeOpen: .always) try await useTheSyncedRealm(realm: realm) }
同期された Realm を開くに関する一般的な情報については、「 同期された Realm を構成して開きます - Swift SDK 」を参照してください。
カスタム Realm アクターの定義
非同期コンテキストで Realm を管理するための特定のアクターを定義できます。 このアクターを使用して、Realm アクセスを管理し、書込み操作を実行できます。
actor RealmActor { // An implicitly-unwrapped optional is used here to let us pass `self` to // `Realm(actor:)` within `init` var realm: Realm! init() async throws { realm = try await Realm(actor: self) } var count: Int { realm.objects(Todo.self).count } func createTodo(name: String, owner: String, status: String) async throws { try await realm.asyncWrite { realm.create(Todo.self, value: [ "_id": ObjectId.generate(), "name": name, "owner": owner, "status": status ]) } } func getTodoOwner(forTodoNamed name: String) -> String { let todo = realm.objects(Todo.self).where { $0.name == name }.first! return todo.owner } struct TodoStruct { var id: ObjectId var name, owner, status: String } func getTodoAsStruct(forTodoNamed name: String) -> TodoStruct { let todo = realm.objects(Todo.self).where { $0.name == name }.first! return TodoStruct(id: todo._id, name: todo.name, owner: todo.owner, status: todo.status) } func updateTodo(_id: ObjectId, name: String, owner: String, status: String) async throws { try await realm.asyncWrite { realm.create(Todo.self, value: [ "_id": _id, "name": name, "owner": owner, "status": status ], update: .modified) } } func deleteTodo(id: ObjectId) async throws { try await realm.asyncWrite { let todoToDelete = realm.object(ofType: Todo.self, forPrimaryKey: id) realm.delete(todoToDelete!) } } func close() { realm = nil } }
アクター分離された Realm は、ローカル アクターまたはグローバル アクターのいずれかで使用できます。
// A simple example of a custom global actor actor BackgroundActor: GlobalActor { static var shared = BackgroundActor() } func backgroundThreadFunction() async throws { // Explicitly specifying the actor is required for anything that is not MainActor let realm = try await Realm(actor: BackgroundActor.shared) try await realm.asyncWrite { _ = realm.create(Todo.self, value: [ "name": "Pledge fealty and service to Gondor", "owner": "Pippin", "status": "In Progress" ]) } // Thread-confined Realms would sometimes throw an exception here, as we // may end up on a different thread after an `await` let todoCount = realm.objects(Todo.self).count print("The number of Realm objects is: \(todoCount)") } func mainThreadFunction() async throws { try await backgroundThreadFunction() }
分離された関数での Realm アクターの同期使用
関数が特定のアクターに制限されている場合、アクターを分離した Realm を同期的に使用できます。
func createObject(in actor: isolated RealmActor) async throws { // Because this function is isolated to this actor, you can use // realm synchronously in this context without async/await keywords try actor.realm.write { actor.realm.create(Todo.self, value: [ "name": "Keep it secret", "owner": "Frodo", "status": "In Progress" ]) } let taskCount = actor.count print("The actor currently has \(taskCount) tasks") } let actor = try await RealmActor() try await createObject(in: actor)
非同期関数での Realm アクターの使用
関数が特定のアクターに限定されない場合は、Swift の async/await 構文で Realm アクターを使用できます。
func createObject() async throws { // Because this function is not isolated to this actor, // you must await operations completed on the actor try await actor.createTodo(name: "Take the ring to Mount Doom", owner: "Frodo", status: "In Progress") let taskCount = await actor.count print("The actor currently has \(taskCount) tasks") } let actor = try await RealmActor() try await createObject()
アクター分離された Realm への書き込み
アクター分離された Realm では、非同期書き込みに Swift async/await 構文を使用できます。 try await realm.asyncWrite { ... }
を使用すると、現在のタスクが一時停止され、現在のスレッドをブロックせずに書込みロックが取得され、そのブロックが呼び出されます。 Realm はバックグラウンド スレッドでデータをディスクに書込み、完了時にタスクを再開します。
上記で定義された例RealmActor
のこの関数は、アクター分離された Realm に書き込む方法を示しています。
func createTodo(name: String, owner: String, status: String) async throws { try await realm.asyncWrite { realm.create(Todo.self, value: [ "_id": ObjectId.generate(), "name": name, "owner": owner, "status": status ]) } }
そして、Swift の非同期構文を使用してこの書き込みを実行できます。
func createObject() async throws { // Because this function is not isolated to this actor, // you must await operations completed on the actor try await actor.createTodo(name: "Take the ring to Mount Doom", owner: "Frodo", status: "In Progress") let taskCount = await actor.count print("The actor currently has \(taskCount) tasks") } let actor = try await RealmActor() try await createObject()
これは書き込みを待機している間は、呼び出しスレッドをブロックしません。 It does not perform I/O on the calling thread. 小規模な書込み(write)の場合、UI をブロックせずに@MainActor
関数から安全に使用できます。 複雑さやプラットフォーム リソースの制約によりアプリのパフォーマンスに悪影響を与える書き込みは、バックグラウンド スレッドで実行するとメリットがある場合があります。
非同期書込みは、アクター分離された Realm または@MainActor
関数でのみサポートされます。
アクターの境界全体で Realm データを渡す
Realm オブジェクトは 送信 できません 、 、および は、アクター境界を直接超えることはできません。アクター境界を超えて Realm データを渡すには、次の 2 つのオプションがあります。
アクターから または に
ThreadSafeReference
を渡す値を直接渡したり、アクター境界をまたがる構造体を作成したりして、送信可能な他のタイプを渡します
スレッドセーフリファレンスを渡す
オブジェクトへのアクセス権を持つアクターにスレッドセーフリファレンスを作成できます。 この場合では、 MainActor
にThreadSafeReference
を作成します。 次に、宛先アクターにThreadSafeReference
を渡します。
// We can pass a thread-safe reference to an object to update it on a different actor. let todo = todoCollection.where { $0.name == "Arrive safely in Bree" }.first! let threadSafeReferenceToTodo = ThreadSafeReference(to: todo) try await backgroundActor.deleteTodo(tsrToTodo: threadSafeReferenceToTodo)
On the destination actor, you must resolve()
the reference within a write transaction before you can use it. これにより、そのアクターにローカルなバージョンの オブジェクトが検索されます。
actor BackgroundActor { public func deleteTodo(tsrToTodo tsr: ThreadSafeReference<Todo>) throws { let realm = try! Realm() try realm.write { // Resolve the thread safe reference on the Actor where you want to use it. // Then, do something with the object. let todoOnActor = realm.resolve(tsr) realm.delete(todoOnActor!) } } }
重要
ThreadSafeReference
は 1 回だけ解決する必要があります。 それ以外の場合、参照が解放されるまで、ソース邦土は固定されたままになります。 このため、 ThreadSafeReference
の有効期間は短くする必要があります。
同じ Realm オブジェクトを複数回、アクター間で共有する必要がある場合は、プライマリキーを共有し、それを使用するアクターでクエリを実行することをお勧めします。 例については、このページの「プライマリ キーを渡し、別のアクター上のオブジェクトをクエリする」セクションを参照してください。
送信可能なタイプを渡す
Realm オブジェクトは送信不可に 送信可能なタイプを渡し、アクターの境界をまたがるデータを操作するには、いくつかの戦略を使用できます。
完全な Realm オブジェクトの代わりに、送信可能な Realm タイプまたはプリミティブ値を渡します
オブジェクトのプライマリキーを渡し、別のアクター上のオブジェクトに対するクエリを渡す
構造体など、Realm オブジェクトの送信可能な表現を作成する
送信可能な Realm タイプとプリミティブ値を渡す
String
やInt
など、Realm オブジェクトからの情報の一部だけが必要な場合は、Realm オブジェクトを渡す代わりにアクター間で値を直接渡すことができます。 どの Realm タイプが送信可能か完全なリストについては、「 送信可能タイプ、非送信タイプ、スレッド構成タイプ 」を参照してください。
func mainThreadFunction() async throws { // Create an object in an actor-isolated realm. // Pass primitive data to the actor instead of // creating the object here and passing the object. let actor = try await RealmActor() try await actor.createTodo(name: "Prepare fireworks for birthday party", owner: "Gandalf", status: "In Progress") // Later, get information off the actor-confined realm let todoOwner = await actor.getTodoOwner(forTodoNamed: "Prepare fireworks for birthday party") }
別のアクター上のオブジェクトに対するプライマリキーとクエリのクエリ
別のアクターで Realm オブジェクトを使用する場合は、使用するアクターでプライマリキーを共有し、それをクエリできます。
// Execute code on a specific actor - in this case, the @MainActor func mainThreadFunction() async throws { // Create an object off the main actor func createObject(in actor: isolated BackgroundActor) async throws -> ObjectId { let realm = try await Realm(actor: actor) let newTodo = try await realm.asyncWrite { return realm.create(Todo.self, value: [ "name": "Pledge fealty and service to Gondor", "owner": "Pippin", "status": "In Progress" ]) } // Share the todo's primary key so we can easily query for it on another actor return newTodo._id } // Initialize an actor where you want to perform background work let actor = BackgroundActor() let newTodoId = try await createObject(in: actor) let realm = try await Realm() let todoOnMainActor = realm.object(ofType: Todo.self, forPrimaryKey: newTodoId) }
オブジェクトの送信可能な表現の作成
単純な値以上の値を操作する必要があるが、 ThreadSafeReferences
を渡したり、異なるアクター上のオブジェクトをクエリしたりするオーバーヘッドを避けたい場合は、アクター境界をまたがるデータの 構造体またはその他の送信可能な表現 を作成できます。 。
たとえば、アクターには、Realm オブジェクトの構造体表現を作成する関数があるとします。
struct TodoStruct { var id: ObjectId var name, owner, status: String } func getTodoAsStruct(forTodoNamed name: String) -> TodoStruct { let todo = realm.objects(Todo.self).where { $0.name == name }.first! return TodoStruct(id: todo._id, name: todo.name, owner: todo.owner, status: todo.status) }
次に、関数を呼び出して、別のアクターの 構造体としてデータを取得できます。
func mainThreadFunction() async throws { // Create an object in an actor-isolated realm. let actor = try await RealmActor() try await actor.createTodo(name: "Leave the ring on the mantle", owner: "Bilbo", status: "In Progress") // Get information as a struct or other Sendable type. let todoAsStruct = await actor.getTodoAsStruct(forTodoNamed: "Leave the ring on the mantle") }
別のアクターの通知を監視
Swift の async/await 構文を使用して、アクター分離された Realm で通知を監視できます。
await object.observe(on: Actor)
またはawait collection.observe(on: Actor)
を呼び出すと、オブジェクトまたはコレクションが変更されるたびに呼び出されるブロックが登録されます。
SDK は、指定されたアクターの実行プログラム上のブロックを非同期に呼び出します。
異なるスレッドまたは異なるプロセスで実行された書込みトランザクションの場合、Realm が 変更を含むバージョンに (自動)更新されるときに、SDK は ブロックを呼び出します。 ローカル書込みの場合、SDK は書込みトランザクションがコミットされた後の将来のある時点でブロックを呼び出します。
他の Realm 通知と同様に、Realm によって管理されるオブジェクトまたはコレクションのみを観察できます。 更新を監視するには、返されたトークンを保持する必要があります。
メインスレッドまたは別のアクターで監視対象の Realm の状態を手動で進める必要がある場合は、 await realm.asyncRefresh()
を呼び出します。 これにより、Realm によって管理される Realm と未処理のオブジェクトが更新され、最新データを指し、該当する通知が送信されます。
観察の制限
.observe()
メソッドは呼び出せません。
書込みトランザクション中
含まれているRealmが読み取り専用の場合
アクターの外部から
コレクション変更リスナーの登録
SDK は、次のような書込みトランザクションごとにコレクション通知ブロックを呼び出します。
コレクションからオブジェクトを削除します。
コレクションにオブジェクトを挿入します。
コレクション内のオブジェクトの管理されているプロパティのいずれかを変更します。 これには、プロパティをその既存の値に設定する自己割り当てが含まれます。
重要
順序は重要
コレクション通知ハンドラーでは、削除、挿入、変更の順に常に変更を適用します。 削除前に挿入を処理すると、予期しない動作が発生する可能性があります。
これらの通知は、変更が発生したアクターに関する情報を提供します。 非アクター分離コレクション通知と同様に、書込みトランザクション中にどのオブジェクトが削除、追加、または変更されたかを報告するchange
パラメーターも提供します。 このRealmCollectionchangeは、 UITableView
のバッチ更新メソッドに渡すことができるインデックス パスの配列に解決されます。
// Create a simple actor actor BackgroundActor { public func deleteTodo(tsrToTodo tsr: ThreadSafeReference<Todo>) throws { let realm = try! Realm() try realm.write { // Resolve the thread safe reference on the Actor where you want to use it. // Then, do something with the object. let todoOnActor = realm.resolve(tsr) realm.delete(todoOnActor!) } } } // Execute some code on a different actor - in this case, the MainActor func mainThreadFunction() async throws { let backgroundActor = BackgroundActor() let realm = try! await Realm() // Create a todo item so there is something to observe try await realm.asyncWrite { realm.create(Todo.self, value: [ "_id": ObjectId.generate(), "name": "Arrive safely in Bree", "owner": "Merry", "status": "In Progress" ]) } // Get the collection of todos on the current actor let todoCollection = realm.objects(Todo.self) // Register a notification token, providing the actor where you want to observe changes. // This is only required if you want to observe on a different actor. let token = await todoCollection.observe(on: backgroundActor, { actor, changes in print("A change occurred on actor: \(actor)") switch changes { case .initial: print("The initial value of the changed object was: \(changes)") case .update(_, let deletions, let insertions, let modifications): if !deletions.isEmpty { print("An object was deleted: \(changes)") } else if !insertions.isEmpty { print("An object was inserted: \(changes)") } else if !modifications.isEmpty { print("An object was modified: \(changes)") } case .error(let error): print("An error occurred: \(error.localizedDescription)") } }) // Update an object to trigger the notification. // This example triggers a notification that the object is deleted. // We can pass a thread-safe reference to an object to update it on a different actor. let todo = todoCollection.where { $0.name == "Arrive safely in Bree" }.first! let threadSafeReferenceToTodo = ThreadSafeReference(to: todo) try await backgroundActor.deleteTodo(tsrToTodo: threadSafeReferenceToTodo) // Invalidate the token when done observing token.invalidate() }
オブジェクト変更リスナーの登録
SDK は、次のような書込みトランザクションごとにオブジェクト通知ブロックを呼び出します。
オブジェクトを削除します。
オブジェクトの管理されているプロパティのいずれかを変更します。 これには、プロパティをその既存の値に設定する自己割り当てが含まれます。
ブロックには、リクエストされたアクターに分離されたオブジェクトのコピーと、変更された内容に関する情報が渡されます。 このオブジェクトは、そのアクターで安全に使用できます。
デフォルトでは、オブジェクトのプロパティに対する直接変更のみが通知を生成します。 リンクされたオブジェクトに対する変更では通知は生成されません。 nil 以外の、空でないキーパス配列が渡される場合、それらのキーパスで識別されるプロパティに対する変更のみが変更通知を生成します。 キーパスは、リンクプロパティを走査して、リンクされたオブジェクトの変更に関する情報を受け取る場合があります。
// Execute some code on a specific actor - in this case, the MainActor func mainThreadFunction() async throws { // Initialize an instance of another actor // where you want to do background work let backgroundActor = BackgroundActor() // Create a todo item so there is something to observe let realm = try! await Realm() let scourTheShire = try await realm.asyncWrite { return realm.create(Todo.self, value: [ "_id": ObjectId.generate(), "name": "Scour the Shire", "owner": "Merry", "status": "In Progress" ]) } // Register a notification token, providing the actor let token = await scourTheShire.observe(on: backgroundActor, { actor, change in print("A change occurred on actor: \(actor)") switch change { case .change(let object, let properties): for property in properties { print("Property '\(property.name)' of object \(object) changed to '\(property.newValue!)'") } case .error(let error): print("An error occurred: \(error)") case .deleted: print("The object was deleted.") } }) // Update the object to trigger the notification. // This triggers a notification that the object's `status` property has been changed. try await realm.asyncWrite { scourTheShire.status = "Complete" } // Invalidate the token when done observing token.invalidate() }