From 8df5c8d52300c16e8b6d0f5bd8b3f631ca9f1eb8 Mon Sep 17 00:00:00 2001 From: Dachary Carey Date: Fri, 3 Nov 2023 14:49:26 -0400 Subject: [PATCH 01/11] Update failing Realm Actor tests --- examples/ios/Examples/RealmActor.swift | 244 +++++++++++------- .../RealmExamples.xcodeproj/project.pbxproj | 2 + .../xcschemes/Test Examples.xcscheme | 12 - ...ealmActor.snippet.define-realm-actor.swift | 6 +- .../RealmActor.snippet.delete-async.swift | 5 +- .../RealmActor.snippet.delete-object.swift | 7 +- ....snippet.observe-collection-on-actor.swift | 88 ++++--- ...ctor.snippet.observe-object-on-actor.swift | 69 ++--- .../RealmActor.snippet.read-objects.swift | 13 +- .../RealmActor.snippet.update-object.swift | 15 +- 10 files changed, 272 insertions(+), 189 deletions(-) diff --git a/examples/ios/Examples/RealmActor.swift b/examples/ios/Examples/RealmActor.swift index 04534bb429..9071f1d185 100644 --- a/examples/ios/Examples/RealmActor.swift +++ b/examples/ios/Examples/RealmActor.swift @@ -40,7 +40,14 @@ actor RealmActor { } } // :snippet-end: - + // :remove-start: + func getObjectId(forTodoNamed name: String) async -> ObjectId { + let todo = realm.objects(RealmActor_Todo.self).where { + $0.name == name + }.first! + return todo._id + } + // :remove-end: // :snippet-start: update-async func updateTodo(_id: ObjectId, name: String, owner: String, status: String) async throws { try await realm.asyncWrite { @@ -55,9 +62,10 @@ actor RealmActor { // :snippet-end: // :snippet-start: delete-async - func deleteTodo(todo: RealmActor_Todo) async throws { + func deleteTodo(id: ObjectId) async throws { try await realm.asyncWrite { - realm.delete(todo) + let todoToDelete = realm.object(ofType: RealmActor_Todo.self, forPrimaryKey: id) + realm.delete(todoToDelete!) } } // :snippet-end: @@ -145,26 +153,36 @@ class RealmActorTests: XCTestCase { // :snippet-start: update-object // :snippet-start: read-objects let actor = try await RealmActor() + try await createObject(in: actor) // :remove: - try await createObject(in: actor) - - let todo = await actor.realm.objects(RealmActor_Todo.self).where { - $0.name == "Keep it safe" - }.first! + // Read objects in functions isolated to the actor and pass primitive values to the caller + func getObjectId(in actor: isolated RealmActor, forTodoNamed name: String) async -> ObjectId { + let todo = actor.realm.objects(RealmActor_Todo.self).where { + $0.name == name + }.first! + return todo._id + } + let objectId = await getObjectId(in: actor, forTodoNamed: "Keep it safe") // :snippet-end: - XCTAssertNotNil(todo) // :remove: - - try await actor.updateTodo(_id: todo._id, name: todo.name, owner: todo.owner, status: "Completed") + XCTAssertNotNil(objectId) // :remove: + + try await actor.updateTodo(_id: objectId, name: "Keep it safe", owner: "Frodo", status: "Completed") // :snippet-end: - sleep(1) - XCTAssertEqual(todo.status, "Completed") + func getTodoStatus(in actor: isolated RealmActor, for id: ObjectId) async -> String { + let todo = actor.realm.objects(RealmActor_Todo.self).where { + $0._id == id + }.first! + return todo.status + } + let todoStatus = await getTodoStatus(in: actor, for: objectId) + XCTAssertEqual(todoStatus, "Completed") await actor.close() } func testActorIsolatedRealmDelete() async throws { - func createObject(in actor: isolated RealmActor) async throws -> RealmActor_Todo { - return try actor.realm.write { - return actor.realm.create(RealmActor_Todo.self, value: [ + func createObject(in actor: isolated RealmActor) async throws { + try actor.realm.write { + actor.realm.create(RealmActor_Todo.self, value: [ "name": "Keep Mr. Frodo safe from that Gollum", "owner": "Sam", "status": "In Progress" @@ -173,14 +191,15 @@ class RealmActorTests: XCTestCase { } // :snippet-start: delete-object let actor = try await RealmActor() - - let todo = try await createObject(in: actor) - XCTAssertNotNil(todo) // :remove: - print("Successfully created an object with id: \(todo._id)") + // :remove-start: + try await createObject(in: actor) let todoCount = await actor.count - XCTAssertEqual(todoCount, 1) // :remove: + XCTAssertEqual(todoCount, 1) + // :remove-end: + let todoId = await actor.getObjectId(forTodoNamed: "Keep Mr. Frodo safe from that Gollum") + XCTAssertNotNil(todoId) // :remove: - try await actor.deleteTodo(todo: todo) + try await actor.deleteTodo(id: todoId) let updatedTodoCount = await actor.count XCTAssertEqual(updatedTodoCount, 0) // :remove: if updatedTodoCount == todoCount - 1 { @@ -346,92 +365,129 @@ class RealmActorTests: XCTestCase { func testObserveCollectionOnActor() async throws { let expectation = expectation(description: "A notification is triggered") // :snippet-start: observe-collection-on-actor - let actor = try await RealmActor() - - // Add a todo to the realm so the collection has something to observe - try await actor.createTodo(name: "Arrive safely in Bree", owner: "Merry", status: "In Progress") - let todoCount = await actor.count - print("The actor currently has \(todoCount) tasks") - XCTAssertEqual(todoCount, 1) // :remove: - - // Get a collection - let todos = await actor.realm.objects(RealmActor_Todo.self) - - // Register a notification token, providing the actor - let token = await todos.observe(on: actor, { 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)") - expectation.fulfill() // :remove: + // Create a simple actor + @globalActor actor BackgroundActor: GlobalActor { + static var shared = BackgroundActor() + + public func deleteTodo(tsrToTodo tsr: ThreadSafeReference) throws { + let realm = try! Realm() + try realm.write { + let todoOnActor = realm.resolve(tsr) + realm.delete(todoOnActor!) } - case .error(let error): - print("An error occurred: \(error.localizedDescription)") } - }) - - // Make an update to an object to trigger the notification - await actor.realm.writeAsync { - todos.first!.status = "Completed" } - - await fulfillment(of: [expectation], timeout: 2) // :remove: - // Invalidate the token when done observing - token.invalidate() + + // Execute some code on a different actor - in this case, the MainActor + @MainActor + func mainThreadFunction() async throws { + let realm = try! await Realm() + + // Create a todo item so there is something to observe + try await realm.asyncWrite { + return realm.create(RealmActor_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(RealmActor_Todo.self) + XCTAssertEqual(todoCollection.count, 1) // :remove: + + // 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.shared, { actor, changes in + print("A change occurred on actor: \(actor)") + switch changes { + case .initial: + print("The initial value of the changed object was: \(changes)") + expectation.fulfill() // :remove: + 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. + // We can pass a thread-safe reference to an object to update it on a different actor. + // This triggers a notification that the object is deleted. + let threadSafeReferenceToTodo = ThreadSafeReference(to: todoCollection.first!) + try await BackgroundActor.shared.deleteTodo(tsrToTodo: threadSafeReferenceToTodo) + + // Invalidate the token when done observing + token.invalidate() + } // :snippet-end: - await actor.close() + + try await mainThreadFunction() + + await fulfillment(of: [expectation], timeout: 5) } func testObserveObjectOnActor() async throws { let expectation = expectation(description: "A notification is triggered") // :snippet-start: observe-object-on-actor - let actor = try await RealmActor() - - // Add a todo to the realm so we can observe it - try await actor.createTodo(name: "Scour the Shire", owner: "Merry", status: "In Progress") - let todoCount = await actor.count - print("The actor currently has \(todoCount) tasks") - XCTAssertEqual(todoCount, 1) // :remove: - - // Get an object - let todo = await actor.realm.objects(RealmActor_Todo.self).where { - $0.name == "Scour the Shire" - }.first! - - // Register a notification token, providing the actor - let token = await todo.observe(on: actor, { 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!)'") + // Create a simple actor + @globalActor actor BackgroundActor: GlobalActor { + static var shared = BackgroundActor() + } + + // Execute some code on a different actor - in this case, the MainActor + @MainActor + func mainThreadFunction() async throws { + // Create a todo item so there is something to observe + let realm = try! await Realm() + let scourTheShire = try await realm.asyncWrite { + return realm.create(RealmActor_Todo.self, value: [ + "_id": ObjectId.generate(), + "name": "Scour the Shire", + "owner": "Merry", + "status": "In Progress" + ]) + } + XCTAssertNotNil(scourTheShire) // :remove: + + // Register a notification token, providing the actor + let token = await scourTheShire.observe(on: BackgroundActor.shared, { 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!)'") + } expectation.fulfill() // :remove: + case .error(let error): + print("An error occurred: \(error)") + case .deleted: + print("The object was deleted.") } - case .error(let error): - print("An error occurred: \(error)") - case .deleted: - print("The object was deleted.") + }) + XCTAssertEqual(scourTheShire.status, "In Progress") // :remove: + + // 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" } - }) - - // Make an update to an object to trigger the notification - await actor.realm.writeAsync { - todo.status = "Completed" + + // Invalidate the token when done observing + token.invalidate() } - - await fulfillment(of: [expectation], timeout: 2) // :remove: - // Invalidate the token when done observing - token.invalidate() // :snippet-end: - XCTAssertEqual(todo.status, "Completed") - await actor.close() + + try await mainThreadFunction() + + await fulfillment(of: [expectation], timeout: 5) } #endif } diff --git a/examples/ios/RealmExamples.xcodeproj/project.pbxproj b/examples/ios/RealmExamples.xcodeproj/project.pbxproj index 9db65a97d4..b7a669c9f7 100644 --- a/examples/ios/RealmExamples.xcodeproj/project.pbxproj +++ b/examples/ios/RealmExamples.xcodeproj/project.pbxproj @@ -1030,6 +1030,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RealmExamplesHostApp.app/RealmExamplesHostApp"; @@ -1053,6 +1054,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.mongodb.docs.RealmExamples; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RealmExamplesHostApp.app/RealmExamplesHostApp"; diff --git a/examples/ios/RealmExamples.xcodeproj/xcshareddata/xcschemes/Test Examples.xcscheme b/examples/ios/RealmExamples.xcodeproj/xcshareddata/xcschemes/Test Examples.xcscheme index c7a88069ac..a58a3ef028 100644 --- a/examples/ios/RealmExamples.xcodeproj/xcshareddata/xcschemes/Test Examples.xcscheme +++ b/examples/ios/RealmExamples.xcodeproj/xcshareddata/xcschemes/Test Examples.xcscheme @@ -33,18 +33,6 @@ ReferencedContainer = "container:RealmExamples.xcodeproj"> - - - - - - - - diff --git a/source/examples/generated/code/start/RealmActor.snippet.define-realm-actor.swift b/source/examples/generated/code/start/RealmActor.snippet.define-realm-actor.swift index fd66d59272..dc301a1eb9 100644 --- a/source/examples/generated/code/start/RealmActor.snippet.define-realm-actor.swift +++ b/source/examples/generated/code/start/RealmActor.snippet.define-realm-actor.swift @@ -20,7 +20,6 @@ actor RealmActor { ]) } } - func updateTodo(_id: ObjectId, name: String, owner: String, status: String) async throws { try await realm.asyncWrite { realm.create(Todo.self, value: [ @@ -32,9 +31,10 @@ actor RealmActor { } } - func deleteTodo(todo: Todo) async throws { + func deleteTodo(id: ObjectId) async throws { try await realm.asyncWrite { - realm.delete(todo) + let todoToDelete = realm.object(ofType: Todo.self, forPrimaryKey: id) + realm.delete(todoToDelete!) } } diff --git a/source/examples/generated/code/start/RealmActor.snippet.delete-async.swift b/source/examples/generated/code/start/RealmActor.snippet.delete-async.swift index 3cf54e7b52..76d86274f8 100644 --- a/source/examples/generated/code/start/RealmActor.snippet.delete-async.swift +++ b/source/examples/generated/code/start/RealmActor.snippet.delete-async.swift @@ -1,5 +1,6 @@ -func deleteTodo(todo: Todo) async throws { +func deleteTodo(id: ObjectId) async throws { try await realm.asyncWrite { - realm.delete(todo) + let todoToDelete = realm.object(ofType: Todo.self, forPrimaryKey: id) + realm.delete(todoToDelete!) } } diff --git a/source/examples/generated/code/start/RealmActor.snippet.delete-object.swift b/source/examples/generated/code/start/RealmActor.snippet.delete-object.swift index 904465c1b3..af166b514e 100644 --- a/source/examples/generated/code/start/RealmActor.snippet.delete-object.swift +++ b/source/examples/generated/code/start/RealmActor.snippet.delete-object.swift @@ -1,10 +1,7 @@ let actor = try await RealmActor() +let todoId = await actor.getObjectId(forTodoNamed: "Keep Mr. Frodo safe from that Gollum") -let todo = try await createObject(in: actor) -print("Successfully created an object with id: \(todo._id)") -let todoCount = await actor.count - -try await actor.deleteTodo(todo: todo) +try await actor.deleteTodo(id: todoId) let updatedTodoCount = await actor.count if updatedTodoCount == todoCount - 1 { print("Successfully deleted the todo") diff --git a/source/examples/generated/code/start/RealmActor.snippet.observe-collection-on-actor.swift b/source/examples/generated/code/start/RealmActor.snippet.observe-collection-on-actor.swift index 13c7649d7f..c3def7180c 100644 --- a/source/examples/generated/code/start/RealmActor.snippet.observe-collection-on-actor.swift +++ b/source/examples/generated/code/start/RealmActor.snippet.observe-collection-on-actor.swift @@ -1,36 +1,60 @@ -let actor = try await RealmActor() - -// Add a todo to the realm so the collection has something to observe -try await actor.createTodo(name: "Arrive safely in Bree", owner: "Merry", status: "In Progress") -let todoCount = await actor.count -print("The actor currently has \(todoCount) tasks") - -// Get a collection -let todos = await actor.realm.objects(Todo.self) - -// Register a notification token, providing the actor -let token = await todos.observe(on: actor, { 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)") +// Create a simple actor +@globalActor actor BackgroundActor: GlobalActor { + static var shared = BackgroundActor() + + public func deleteTodo(tsrToTodo tsr: ThreadSafeReference) throws { + let realm = try! Realm() + try realm.write { + let todoOnActor = realm.resolve(tsr) + realm.delete(todoOnActor!) } - case .error(let error): - print("An error occurred: \(error.localizedDescription)") } -}) - -// Make an update to an object to trigger the notification -await actor.realm.writeAsync { - todos.first!.status = "Completed" } -// Invalidate the token when done observing -token.invalidate() +// Execute some code on a different actor - in this case, the MainActor +@MainActor +func mainThreadFunction() async throws { + let realm = try! await Realm() + + // Create a todo item so there is something to observe + try await realm.asyncWrite { + return 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.shared, { 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. + // We can pass a thread-safe reference to an object to update it on a different actor. + // This triggers a notification that the object is deleted. + let threadSafeReferenceToTodo = ThreadSafeReference(to: todoCollection.first!) + try await BackgroundActor.shared.deleteTodo(tsrToTodo: threadSafeReferenceToTodo) + + // Invalidate the token when done observing + token.invalidate() +} diff --git a/source/examples/generated/code/start/RealmActor.snippet.observe-object-on-actor.swift b/source/examples/generated/code/start/RealmActor.snippet.observe-object-on-actor.swift index 18b591aab2..127ac40066 100644 --- a/source/examples/generated/code/start/RealmActor.snippet.observe-object-on-actor.swift +++ b/source/examples/generated/code/start/RealmActor.snippet.observe-object-on-actor.swift @@ -1,34 +1,43 @@ -let actor = try await RealmActor() - -// Add a todo to the realm so we can observe it -try await actor.createTodo(name: "Scour the Shire", owner: "Merry", status: "In Progress") -let todoCount = await actor.count -print("The actor currently has \(todoCount) tasks") - -// Get an object -let todo = await actor.realm.objects(Todo.self).where { - $0.name == "Scour the Shire" -}.first! +// Create a simple actor +@globalActor actor BackgroundActor: GlobalActor { + static var shared = BackgroundActor() +} -// Register a notification token, providing the actor -let token = await todo.observe(on: actor, { 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!)'") +// Execute some code on a different actor - in this case, the MainActor +@MainActor +func mainThreadFunction() async throws { + // 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.shared, { 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.") } - 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" } -}) - -// Make an update to an object to trigger the notification -await actor.realm.writeAsync { - todo.status = "Completed" + + // Invalidate the token when done observing + token.invalidate() } - -// Invalidate the token when done observing -token.invalidate() diff --git a/source/examples/generated/code/start/RealmActor.snippet.read-objects.swift b/source/examples/generated/code/start/RealmActor.snippet.read-objects.swift index be3a1bf89e..486c6ea45e 100644 --- a/source/examples/generated/code/start/RealmActor.snippet.read-objects.swift +++ b/source/examples/generated/code/start/RealmActor.snippet.read-objects.swift @@ -1,7 +1,10 @@ let actor = try await RealmActor() -try await createObject(in: actor) - -let todo = await actor.realm.objects(Todo.self).where { - $0.name == "Keep it safe" -}.first! +// Read objects in functions isolated to the actor and pass primitive values to the caller +func getObjectId(in actor: isolated RealmActor, forTodoNamed name: String) async -> ObjectId { + let todo = actor.realm.objects(Todo.self).where { + $0.name == name + }.first! + return todo._id +} +let objectId = await getObjectId(in: actor, forTodoNamed: "Keep it safe") diff --git a/source/examples/generated/code/start/RealmActor.snippet.update-object.swift b/source/examples/generated/code/start/RealmActor.snippet.update-object.swift index d53d891bed..a1f0f51105 100644 --- a/source/examples/generated/code/start/RealmActor.snippet.update-object.swift +++ b/source/examples/generated/code/start/RealmActor.snippet.update-object.swift @@ -1,9 +1,12 @@ let actor = try await RealmActor() -try await createObject(in: actor) +// Read objects in functions isolated to the actor and pass primitive values to the caller +func getObjectId(in actor: isolated RealmActor, forTodoNamed name: String) async -> ObjectId { + let todo = actor.realm.objects(Todo.self).where { + $0.name == name + }.first! + return todo._id +} +let objectId = await getObjectId(in: actor, forTodoNamed: "Keep it safe") -let todo = await actor.realm.objects(Todo.self).where { - $0.name == "Keep it safe" -}.first! - -try await actor.updateTodo(_id: todo._id, name: todo.name, owner: todo.owner, status: "Completed") +try await actor.updateTodo(_id: objectId, name: "Keep it safe", owner: "Frodo", status: "Completed") From 327b8a04782a98967705f5dcfb00bccdaf37dd87 Mon Sep 17 00:00:00 2001 From: Dachary Carey Date: Mon, 6 Nov 2023 17:28:57 -0500 Subject: [PATCH 02/11] Correct errors and update docs for Actor support and Swift concurrency --- examples/ios/Examples/RealmActor.swift | 107 +++++++++++++- ...ealmActor.snippet.define-realm-actor.swift | 20 +++ ...et.get-actor-confined-data-as-struct.swift | 9 ++ ....snippet.observe-collection-on-actor.swift | 9 +- ...almActor.snippet.pass-data-as-struct.swift | 11 ++ ...et.pass-primitive-data-across-actors.swift | 11 ++ ...pet.pass-tsr-across-actor-boundaries.swift | 6 + ...ppet.query-for-data-on-another-actor.swift | 26 ++++ ...lmActor.snippet.resolve-tsr-on-actor.swift | 13 ++ source/sdk/swift/actor-isolated-realm.txt | 139 ++++++++++++++---- source/sdk/swift/crud/threading.txt | 31 ++-- source/sdk/swift/react-to-changes.txt | 22 +-- source/sdk/swift/swift-concurrency.txt | 25 ++-- 13 files changed, 361 insertions(+), 68 deletions(-) create mode 100644 source/examples/generated/code/start/RealmActor.snippet.get-actor-confined-data-as-struct.swift create mode 100644 source/examples/generated/code/start/RealmActor.snippet.pass-data-as-struct.swift create mode 100644 source/examples/generated/code/start/RealmActor.snippet.pass-primitive-data-across-actors.swift create mode 100644 source/examples/generated/code/start/RealmActor.snippet.pass-tsr-across-actor-boundaries.swift create mode 100644 source/examples/generated/code/start/RealmActor.snippet.query-for-data-on-another-actor.swift create mode 100644 source/examples/generated/code/start/RealmActor.snippet.resolve-tsr-on-actor.swift diff --git a/examples/ios/Examples/RealmActor.swift b/examples/ios/Examples/RealmActor.swift index 9071f1d185..11c39f1858 100644 --- a/examples/ios/Examples/RealmActor.swift +++ b/examples/ios/Examples/RealmActor.swift @@ -48,6 +48,28 @@ actor RealmActor { return todo._id } // :remove-end: + + func getTodoOwner(forTodoNamed name: String) -> String { + let todo = realm.objects(RealmActor_Todo.self).where { + $0.name == name + }.first! + return todo.owner + } + + // :snippet-start: pass-data-as-struct + struct TodoStruct { + var id: ObjectId + var name, owner, status: String + } + + func getTodoAsStruct(forTodoNamed name: String) -> TodoStruct { + let todo = realm.objects(RealmActor_Todo.self).where { + $0.name == name + }.first! + return TodoStruct(id: todo._id, name: todo.name, owner: todo.owner, status: todo.status) + } + // :snippet-end: + // :snippet-start: update-async func updateTodo(_id: ObjectId, name: String, owner: String, status: String) async throws { try await realm.asyncWrite { @@ -366,17 +388,21 @@ class RealmActorTests: XCTestCase { let expectation = expectation(description: "A notification is triggered") // :snippet-start: observe-collection-on-actor // Create a simple actor + // :snippet-start: resolve-tsr-on-actor @globalActor actor BackgroundActor: GlobalActor { static var shared = BackgroundActor() public func deleteTodo(tsrToTodo tsr: ThreadSafeReference) 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!) } } } + // :snippet-end: // Execute some code on a different actor - in this case, the MainActor @MainActor @@ -419,10 +445,15 @@ class RealmActorTests: XCTestCase { }) // Update an object to trigger the notification. + // This example triggers a notification that the object is deleted. + // :snippet-start: pass-tsr-across-actor-boundaries // We can pass a thread-safe reference to an object to update it on a different actor. - // This triggers a notification that the object is deleted. - let threadSafeReferenceToTodo = ThreadSafeReference(to: todoCollection.first!) + let todo = todoCollection.where { + $0.name == "Arrive safely in Bree" + }.first! + let threadSafeReferenceToTodo = ThreadSafeReference(to: todo) try await BackgroundActor.shared.deleteTodo(tsrToTodo: threadSafeReferenceToTodo) + // :snippet-end: // Invalidate the token when done observing token.invalidate() @@ -482,6 +513,7 @@ class RealmActorTests: XCTestCase { // Invalidate the token when done observing token.invalidate() + await realm.asyncRefresh() // :remove: } // :snippet-end: @@ -489,6 +521,77 @@ class RealmActorTests: XCTestCase { await fulfillment(of: [expectation], timeout: 5) } + + func testQueryForDataOnAnotherActor() async throws { + try await mainThreadFunction() + + // :snippet-start: query-for-data-on-another-actor + // A simple example of a custom global actor + @globalActor actor BackgroundActor: GlobalActor { + static var shared = BackgroundActor() + } + + @BackgroundActor + func createObjectOnBackgroundActor() async throws -> ObjectId { + // Explicitly specifying the actor is required for anything that is not MainActor + let realm = try await Realm(actor: BackgroundActor.shared) + let newTodo = try await realm.asyncWrite { + return realm.create(RealmActor_Todo.self, value: [ + "name": "Pledge fealty and service to Gondor", + "owner": "Pippin", + "status": "In Progress" + ]) + } + XCTAssertEqual(realm.objects(RealmActor_Todo.self).count, 1) // :remove: + // Share the todo's primary key so we can easily query for it on another actor + return newTodo._id + } + + @MainActor + func mainThreadFunction() async throws { + let newTodoId = try await createObjectOnBackgroundActor() + let realm = try await Realm() + let todoOnMainActor = realm.object(ofType: RealmActor_Todo.self, forPrimaryKey: newTodoId) + XCTAssertNotNil(todoOnMainActor) // :remove: + } + // :snippet-end: + } + + func testPassPrimitiveDataAcrossActors() async throws { + try await mainThreadFunction() + + // :snippet-start: pass-primitive-data-across-actors + @MainActor + 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") + XCTAssertEqual(todoOwner, "Gandalf") // :remove: + } + // :snippet-end: + } + + func testGetDataAsStruct() async throws { + try await mainThreadFunction() + + // :snippet-start: get-actor-confined-data-as-struct + @MainActor + 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") + XCTAssertNotNil(todoAsStruct.id) // :remove: + } + // :snippet-end: + } #endif } // :replace-end: diff --git a/source/examples/generated/code/start/RealmActor.snippet.define-realm-actor.swift b/source/examples/generated/code/start/RealmActor.snippet.define-realm-actor.swift index dc301a1eb9..f3f7a37c3e 100644 --- a/source/examples/generated/code/start/RealmActor.snippet.define-realm-actor.swift +++ b/source/examples/generated/code/start/RealmActor.snippet.define-realm-actor.swift @@ -20,6 +20,26 @@ actor RealmActor { ]) } } + + 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: [ diff --git a/source/examples/generated/code/start/RealmActor.snippet.get-actor-confined-data-as-struct.swift b/source/examples/generated/code/start/RealmActor.snippet.get-actor-confined-data-as-struct.swift new file mode 100644 index 0000000000..c8910500da --- /dev/null +++ b/source/examples/generated/code/start/RealmActor.snippet.get-actor-confined-data-as-struct.swift @@ -0,0 +1,9 @@ +@MainActor +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") +} diff --git a/source/examples/generated/code/start/RealmActor.snippet.observe-collection-on-actor.swift b/source/examples/generated/code/start/RealmActor.snippet.observe-collection-on-actor.swift index c3def7180c..c87f081d04 100644 --- a/source/examples/generated/code/start/RealmActor.snippet.observe-collection-on-actor.swift +++ b/source/examples/generated/code/start/RealmActor.snippet.observe-collection-on-actor.swift @@ -5,6 +5,8 @@ public func deleteTodo(tsrToTodo tsr: ThreadSafeReference) 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!) } @@ -50,9 +52,12 @@ func mainThreadFunction() async throws { }) // 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. - // This triggers a notification that the object is deleted. - let threadSafeReferenceToTodo = ThreadSafeReference(to: todoCollection.first!) + let todo = todoCollection.where { + $0.name == "Arrive safely in Bree" + }.first! + let threadSafeReferenceToTodo = ThreadSafeReference(to: todo) try await BackgroundActor.shared.deleteTodo(tsrToTodo: threadSafeReferenceToTodo) // Invalidate the token when done observing diff --git a/source/examples/generated/code/start/RealmActor.snippet.pass-data-as-struct.swift b/source/examples/generated/code/start/RealmActor.snippet.pass-data-as-struct.swift new file mode 100644 index 0000000000..0facac7687 --- /dev/null +++ b/source/examples/generated/code/start/RealmActor.snippet.pass-data-as-struct.swift @@ -0,0 +1,11 @@ +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) +} diff --git a/source/examples/generated/code/start/RealmActor.snippet.pass-primitive-data-across-actors.swift b/source/examples/generated/code/start/RealmActor.snippet.pass-primitive-data-across-actors.swift new file mode 100644 index 0000000000..c1258b92cf --- /dev/null +++ b/source/examples/generated/code/start/RealmActor.snippet.pass-primitive-data-across-actors.swift @@ -0,0 +1,11 @@ +@MainActor +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") +} diff --git a/source/examples/generated/code/start/RealmActor.snippet.pass-tsr-across-actor-boundaries.swift b/source/examples/generated/code/start/RealmActor.snippet.pass-tsr-across-actor-boundaries.swift new file mode 100644 index 0000000000..b1fdb9c50b --- /dev/null +++ b/source/examples/generated/code/start/RealmActor.snippet.pass-tsr-across-actor-boundaries.swift @@ -0,0 +1,6 @@ +// 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.shared.deleteTodo(tsrToTodo: threadSafeReferenceToTodo) diff --git a/source/examples/generated/code/start/RealmActor.snippet.query-for-data-on-another-actor.swift b/source/examples/generated/code/start/RealmActor.snippet.query-for-data-on-another-actor.swift new file mode 100644 index 0000000000..e1a73b5cc6 --- /dev/null +++ b/source/examples/generated/code/start/RealmActor.snippet.query-for-data-on-another-actor.swift @@ -0,0 +1,26 @@ +// A simple example of a custom global actor +@globalActor actor BackgroundActor: GlobalActor { + static var shared = BackgroundActor() +} + +@BackgroundActor +func createObjectOnBackgroundActor() async throws -> ObjectId { + // Explicitly specifying the actor is required for anything that is not MainActor + let realm = try await Realm(actor: BackgroundActor.shared) + 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 +} + +@MainActor +func mainThreadFunction() async throws { + let newTodoId = try await createObjectOnBackgroundActor() + let realm = try await Realm() + let todoOnMainActor = realm.object(ofType: Todo.self, forPrimaryKey: newTodoId) +} diff --git a/source/examples/generated/code/start/RealmActor.snippet.resolve-tsr-on-actor.swift b/source/examples/generated/code/start/RealmActor.snippet.resolve-tsr-on-actor.swift new file mode 100644 index 0000000000..2d9552b74a --- /dev/null +++ b/source/examples/generated/code/start/RealmActor.snippet.resolve-tsr-on-actor.swift @@ -0,0 +1,13 @@ +@globalActor actor BackgroundActor: GlobalActor { + static var shared = BackgroundActor() + + public func deleteTodo(tsrToTodo tsr: ThreadSafeReference) 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!) + } + } +} diff --git a/source/sdk/swift/actor-isolated-realm.txt b/source/sdk/swift/actor-isolated-realm.txt index 0566e9b945..65b19e7e66 100644 --- a/source/sdk/swift/actor-isolated-realm.txt +++ b/source/sdk/swift/actor-isolated-realm.txt @@ -1,7 +1,8 @@ .. _swift-actor-isolated-realm: +.. _swift-use-realm-with-actors: ================================= -Actor-Isolated Realms - Swift SDK +Use Realm with Actors - Swift SDK ================================= .. contents:: On this page @@ -10,23 +11,28 @@ Actor-Isolated Realms - Swift SDK :depth: 2 :class: singlecol -Starting with Realm Swift SDK version 10.39.0, Realm supports actor-isolated -realm instances. You might want to use an actor-isolated realm if your app -uses Swift concurrency language features. This functionality provides -an alternative to managing threads or dispatch queues to perform asynchronous -work. +Starting with Realm Swift SDK version 10.39.0, Realm supports built-in +functionality for using Realm with Swift Actors. Realm's actor support +provides an alternative to managing threads or dispatch queues to perform +asynchronous work. You can use Realm with Actors in a few different ways: -With an actor-isolated realm, you can use Swift's async/await syntax to: +- Work with realm *only* on a specific Actor with an actor-isolated realm +- Use Realm across actors based on the needs of your application -- Open a realm -- Write to a realm -- Listen for notifications +You might want to use an actor-isolated realm if you want to restrict all +realm access to a single actor. This negates the need to pass data across +the actor boundary, and can simplify data race debugging. + +You might want to use realms across actors in cases where you want to +perform different types of work on different actors. For example, you might +want to read objects on the MainActor but use a background actor for large +writes. For general information about Swift actors, refer to :apple:`Apple's Actor documentation `. Prerequisites -~~~~~~~~~~~~~ +------------- To use Realm in a Swift actor, your project must: @@ -40,7 +46,7 @@ In addition, we strongly recommend enabling these settings in your project: runtime actor data-race detection About the Examples on This Page -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------- The examples on this page use the following model: @@ -50,7 +56,7 @@ The examples on this page use the following model: .. _swift-open-actor-confined-realm: Open an Actor-Isolated Realm -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------- You can use the Swift async/await syntax to await opening a realm. @@ -81,7 +87,7 @@ For more general information about opening a synced realm, refer to .. _swift-define-realm-actor: Define a Custom Realm Actor -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------- You can define a specific actor to manage Realm in asynchronous contexts. You can use this actor to manage realm access, perform write operations, @@ -98,7 +104,7 @@ An actor-isolated realm may be used with either local or global actors. .. _swift-actor-synchronous-isolated-function: Use a Realm Actor Synchronously in an Isolated Function -```````````````````````````````````````````````````````` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When a function is confined to a specific actor, you can use the actor-isolated realm synchronously. @@ -109,7 +115,7 @@ realm synchronously. .. _swift-actor-async-nonisolated-function: Use a Realm Actor in Async Functions -```````````````````````````````````` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When a function isn't confined to a specific actor, you can use your Realm actor with Swift's async/await syntax. @@ -120,10 +126,10 @@ with Swift's async/await syntax. .. _swift-write-to-actor-confined-realm: Write to an Actor-Isolated Realm -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------- Actor-isolated realms can use Swift async/await syntax for asynchronous -writes. Using ``try await realm.writeAsync { ... }`` suspends the current task, +writes. Using ``try await realm.asyncWrite { ... }`` suspends the current task, acquires the write lock without blocking the current thread, and then invokes the block. Realm writes the data to disk on a background thread and resumes the task when that completes. @@ -148,16 +154,92 @@ resource constraints may still benefit from being done on a background thread. Asynchronous writes are only supported for actor-isolated Realms or in ``@MainActor`` functions. -.. _swift-observe-notifications-on-actor-confined-realm: +.. _swift-realm-cannot-cross-actor-boundary: + +Pass Realm Data Across the Actor Boundary +----------------------------------------- + +Realm objects are not :apple:`Sendable `, +and cannot cross the actor boundary directly. To pass Realm data across +the actor boundary, you have two options: + +- Pass a ``ThreadSafeReference`` to or from the actor +- Pass other types that *are* Sendable, such as passing values directly + or by creating structs to pass across actor boundaries -Observe Notifications on an Actor-Isolated Realm -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. _swift-pass-thread-safe-reference-across-actors: + +Pass a ThreadSafeReference +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can create a +:swift-sdk:`ThreadSafeReference ` on an +actor where you have access to the object. In this case, we create a +``ThreadSafeReference`` on the ``MainActor``. + +.. literalinclude:: /examples/generated/code/start/RealmActor.snippet.actor-isolated-realm-async.swift + :language: swift + +Then, pass the ``ThreadSafeReference`` to the destination actor. +On the destination actor, you must ``resolve()`` the reference within a +write transaction before you can use it. This retrieves a version of the +object local to that actor. + +.. literalinclude:: /examples/generated/code/start/RealmActor.snippet.actor-isolated-realm-async.swift + :language: swift + +You can only resolve a ``ThreadSafeReference`` once. If you may need +to share the same realm object across actors more than once, you may prefer +to share the :ref:`primary key ` and +:ref:`query for it ` on the actor +where you want to use it. + +Pass Types that are Sendable +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you only need a piece of information from the Realm object, such as a +``String`` or ``Int``, you can pass the value directly across actors instead +of passing the Realm object. For a full list of which Realm types are Sendable, +refer to :ref:`concurrency-page-sendable-thread-confined-reference`. + +.. literalinclude:: /examples/generated/code/start/RealmActor.snippet.pass-primitive-data-across-actors.swift + :language: swift + +If you want to use a Realm object on another actor, you can share the +:ref:`primary key ` and +:ref:`query for it ` on the actor +where you want to use it. + +.. literalinclude:: /examples/generated/code/start/RealmActor.snippet.query-for-data-on-another-actor.swift + :language: swift + +If you need to work with more than a simple value, but don't want the +overhead of passing around ``ThreadSafeReferences`` or querying objects on +different actors, you can create a struct or other Sendable representation +of your data to pass across the actor boundary. + +For example, your actor might have a function that creates a struct +representation of the Realm object. + +.. literalinclude:: /examples/generated/code/start/RealmActor.snippet.pass-data-as-struct.swift + :language: swift + +Then, you can call a function to get the data as a struct on another actor. + +.. literalinclude:: /examples/generated/code/start/RealmActor.snippet.get-actor-confined-data-as-struct.swift + :language: swift + +.. _swift-observe-notifications-on-another-actor: + +Observe Notifications on a Different Actor +------------------------------------------ You can observe notifications on an actor-isolated realm using Swift's async/await syntax. -Calling ``await object.observe()`` or ``await collection.observe`` registers -a block to be called each time the object or collection changes. +Calling ``await object.observe(on: Actor)`` or +``await collection.observe(on: Actor)`` registers a block to be called +each time the object or collection changes. The SDK asynchronously calls the block on the given actor's executor. @@ -166,12 +248,12 @@ processes, the SDK calls the block when the realm is (auto)refreshed to a version including the changes. For local writes, the SDK calls the block at some point in the future after the write transaction is committed. -Like :ref:`non-actor-confined notifications `, you can +Like :ref:`other Realm notifications `, you can only observe objects or collections managed by a realm. You must retain the returned token for as long as you want to watch for updates. If you need to manually advance the state of an observed realm on the main -thread or an actor-isolated realm, call ``await realm.asyncRefresh()``. +thread or on another actor, call ``await realm.asyncRefresh()``. This updates the realm and outstanding objects managed by the Realm to point to the most recent data and deliver any applicable notifications. @@ -180,10 +262,13 @@ the most recent data and deliver any applicable notifications. You cannot call the ``observe`` method during a write transaction or when the containing realm is read-only. + You cannot call the ``observe`` method on an actor-confined realm from + outside the actor. + .. _swift-actor-collection-change-listener: Register a Collection Change Listener -````````````````````````````````````` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The SDK calls a collection notification block after each write transaction which: @@ -214,7 +299,7 @@ batch update methods. .. _swift-actor-object-change-listener: Register an Object Change Listener -`````````````````````````````````` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The SDK calls an object notification block after each write transaction which: diff --git a/source/sdk/swift/crud/threading.txt b/source/sdk/swift/crud/threading.txt index 9db644fa9a..90ac860cfa 100644 --- a/source/sdk/swift/crud/threading.txt +++ b/source/sdk/swift/crud/threading.txt @@ -37,7 +37,7 @@ simplify this for you. ` or :ref:`frozen objects ` across threads. -Actor-Isolated Realms +Use Realm with Actors --------------------- This page describes how to manually manage realm files and objects across threads. @@ -90,11 +90,11 @@ Perform a Background Write .. versionadded:: 10.26.0 You can add, modify, or delete objects in the background using -:swift-sdk:`writeAsync `. +:swift-sdk:`asyncWrite `. With async write, you don't need to pass a :ref:`thread-safe reference ` or :ref:`frozen objects ` -across threads. Instead, call ``realm.writeAsync``. You can provide +across threads. Instead, call ``realm.asyncWrite``. You can provide a completion block for the method to execute on the source thread after the write completes or fails. @@ -115,7 +115,7 @@ performing an async write. The ` variable becomes ``true`` after a call to one of: -- ``writeAsync`` +- ``asyncWrite`` - ``beginAsyncWrite`` - ``commitAsyncWrite`` @@ -131,17 +131,17 @@ To complete an async write, you or the SDK must call either: - :swift-sdk:`commitAsyncWrite ` - :swift-sdk:`cancelAsyncWrite ` -When you use the ``writeAsync`` method, the SDK handles committing or +When you use the ``asyncWrite`` method, the SDK handles committing or canceling the transaction. This provides the convenience of the async write without the need to manually keep state tied to the scope of the object. -However, while in the writeAsync block, you *can* explicitly call +However, while in the asyncWrite block, you *can* explicitly call ``commitAsyncWrite`` or ``cancelAsyncWrite``. If you return without -calling one of these methods, ``writeAsync`` either: +calling one of these methods, ``asyncWrite`` either: - Commits the write after executing the instructions in the write block - Returns an error -In either case, this completes the ``writeAsync`` operation. +In either case, this completes the ``asyncWrite`` operation. For more control over when to commit or cancel the async write transaction, use the ``beginAsyncWrite`` method. When you use this method, you must @@ -207,9 +207,10 @@ on your use case: - To keep and share many read-only views of the object in your app, copy the object from the realm. -- To share an instance of a realm or specific object with another thread, share - a :ref:`thread_safe_reference ` to the realm - instance or object. +- To share an instance of a realm or specific object with another thread or + across actor boundaries, share a :ref:`thread_safe_reference + ` to the realm instance or object. For more + information, refer to :ref:`swift-pass-thread-safe-reference-across-actors`. .. _ios-use-serial-queues-for-non-main-threads: @@ -281,6 +282,7 @@ regardless of whether it has a primary key. .. literalinclude:: /examples/generated/code/start/Threading.snippet.threadsafe-wrapper-function-parameter.swift :language: swift +.. _ios-thread-safe-reference: Use ThreadSafeReference (Legacy Swift / Objective-C) ```````````````````````````````````````````````````` @@ -399,6 +401,13 @@ to other threads. You can freely share the frozen object across threads without concern for thread issues. When you freeze a realm, its child objects also become frozen. +.. tip:: Use ThreadSafeReference with Swift Actors + + Realm does not currently support using ``thaw()`` with Swift Actors. + To work with Realm data across actor boundaries, use + ``ThreadSafeReference`` instead of frozen objects. For more information, + refer to :ref:`swift-pass-thread-safe-reference-across-actors`. + Frozen objects are not live and do not automatically update. They are effectively snapshots of the object state at the time of freezing. Thawing an object returns a live version of the frozen diff --git a/source/sdk/swift/react-to-changes.txt b/source/sdk/swift/react-to-changes.txt index f7632f54c3..786cb11a0f 100644 --- a/source/sdk/swift/react-to-changes.txt +++ b/source/sdk/swift/react-to-changes.txt @@ -322,21 +322,21 @@ does support this for code compatibility. Examples of using Realm with :github:`ReactiveCocoa from Objective-C`, and :github:`ReactKit from Swift`. -.. _swift-react-to-changes-actor-confined-realm: +.. _swift-react-to-changes-different-actor: -React to Changes in an Actor-Isolated Realm -------------------------------------------- +React to Changes on a Different Actor +------------------------------------- -You can observe notifications on an actor-isolated realm using Swift's -async/await syntax. Calling ``await object.observe()`` or -``await collection.observe`` registers a block to be called each time the -object or collection changes. +You can observe notifications on a different actor. Calling +``await object.observe(on: Actor)`` or +``await collection.observe(on: Actor)`` registers a block to be called each +time the object or collection changes. .. literalinclude:: /examples/generated/code/start/RealmActor.snippet.observe-collection-on-actor.swift :language: swift -For more information about change notifications on actor-isolated realms, -refer to :ref:`swift-observe-notifications-on-actor-confined-realm`. +For more information about change notifications on another actor, +refer to :ref:`swift-observe-notifications-on-another-actor`. .. _ios-react-to-changes-to-a-class-projection: @@ -454,7 +454,7 @@ checking ``realm.isInWriteTransaction``, and if so making changes, calling notifications and potential for error make this manual manipulation error-prone and difficult to debug. -You can use the :ref:`writeAsync API ` to sidestep complexity +You can use the :ref:`asyncWrite API ` to sidestep complexity if you don't need fine-grained change information from inside your write block. Observing an async write similar to this provides notifications even if the notification happens to be delivered inside a write transaction: @@ -464,7 +464,7 @@ notification happens to be delivered inside a write transaction: let token = dog.observe(keyPaths: [\Dog.age]) { change in guard case let .change(dog, _) = change else { return } - dog.realm!.writeAsync { + dog.realm!.asyncWrite { dog.isPuppy = dog.age < 2 } } diff --git a/source/sdk/swift/swift-concurrency.txt b/source/sdk/swift/swift-concurrency.txt index 0f2f75e186..66295864f5 100644 --- a/source/sdk/swift/swift-concurrency.txt +++ b/source/sdk/swift/swift-concurrency.txt @@ -17,14 +17,10 @@ topic `__. While the considerations on this page broadly apply to using realm with Swift concurrency features, Realm Swift SDK version 10.39.0 adds support -for actor-isolated realms. Actor-isolated realms support Swift concurrency -features including: +for using Realm with Swift Actors. You can use Realm isolated to a single +actor, or work with realm across actors. -- Opening an actor-isolated realm -- Reading and writing from an actor-isolated realm -- Getting notifications on actor-isolated objects and collections - -This provides support for using realm in a MainActor and background actor +Realm's actor support simplifies using realm in a MainActor and background actor context, and supersedes much of the advice on this page regarding concurrency considerations. For more information, refer to :ref:`swift-actor-isolated-realm`. @@ -102,7 +98,7 @@ Perform Background Writes A commonly-requested use case for asynchronous code is to perform write operations in the background without blocking the main thread. While the Realm Swift SDK does not currently support an async/await API for this, it -does have an API specifically for performing background writes: ``writeAsync``. +does have an API specifically for performing background writes: ``asyncWrite``. This API allows you to add, update, or delete objects in the background without using frozen objects or passing a thread-safe reference. With this @@ -113,7 +109,7 @@ frozen objects or passing references across threads. However, while the write block itself is executed, this does block new transactions on the calling thread. This means that a large write using -the ``writeAsync`` API could block small, quick writes while it executes. +the ``asyncWrite`` API could block small, quick writes while it executes. For more information, including a code example, refer to: :ref:`ios-async-write`. @@ -192,7 +188,7 @@ To avoid threading-related issues in code that uses Swift concurrency features: where you access the realm asynchronously with ``@MainActor`` to ensure it always runs on the main thread. Remember that ``await`` marks a suspension point that could change to a different thread. -- Apps that do not use actor-isolated realms can use the ``writeAsync`` API to +- Apps that do not use actor-isolated realms can use the ``asyncWrite`` API to :ref:`perform a background write `. This manages realm access in a thread-safe way without requiring you to write specialized code to do it yourself. This is a special API that outsources aspects of the @@ -202,11 +198,10 @@ To avoid threading-related issues in code that uses Swift concurrency features: in your code. - If you want to explicitly write concurrency code that is not actor-isolated where accessing a realm is done in a thread-safe way, you can explicitly - :ref:`pass instances across threads ` or use - :ref:`frozen objects ` where applicable to avoid - threading-related crashes. This does require a good understanding of - Realm's threading model, as well as being mindful of Swift concurrency - threading behaviors. + :ref:`pass instances across threads ` where + applicable to avoid threading-related crashes. This does require a good + understanding of Realm's threading model, as well as being mindful of + Swift concurrency threading behaviors. .. _concurrency-page-sendable-thread-confined-reference: From 7617e69a224ba7a0cc021634e63c436c7baffbc2 Mon Sep 17 00:00:00 2001 From: Dachary Carey Date: Mon, 6 Nov 2023 17:37:26 -0500 Subject: [PATCH 03/11] Rename the page --- config/redirects | 4 ++++ source/sdk/swift.txt | 2 +- .../{actor-isolated-realm.txt => use-realm-with-actors.txt} | 0 3 files changed, 5 insertions(+), 1 deletion(-) rename source/sdk/swift/{actor-isolated-realm.txt => use-realm-with-actors.txt} (100%) diff --git a/config/redirects b/config/redirects index c11398638e..8742b95a30 100644 --- a/config/redirects +++ b/config/redirects @@ -1262,3 +1262,7 @@ raw: ${prefix}/sdk/swift/sync/network-connection -> ${base}/sdk/swift/sync/sync- # DOCSP-30339: Move Configure & Open a Realm page up a level raw: ${prefix}/sdk/kotlin/realm-database/realm-files/open-and-close-a-realm -> ${base}/sdk/kotlin/realm-database/open-and-close-a-realm/ + +# Update Swift Actor support documentation + +raw: ${prefix}/sdk/swift/actor-isolated-realm -> ${base}/sdk/swift/use-realm-with-actors/ diff --git a/source/sdk/swift.txt b/source/sdk/swift.txt index 5d08039671..0306ebaa18 100644 --- a/source/sdk/swift.txt +++ b/source/sdk/swift.txt @@ -30,7 +30,7 @@ Realm Swift SDK CRUD React to Changes SwiftUI - Actor-Isolated Realms + Use Realm with Actors Swift Concurrency Test and Debug Logging diff --git a/source/sdk/swift/actor-isolated-realm.txt b/source/sdk/swift/use-realm-with-actors.txt similarity index 100% rename from source/sdk/swift/actor-isolated-realm.txt rename to source/sdk/swift/use-realm-with-actors.txt From b1101632260d44ffc8e427c15b65f4923acd2702 Mon Sep 17 00:00:00 2001 From: Dachary Carey Date: Mon, 6 Nov 2023 17:46:04 -0500 Subject: [PATCH 04/11] Fix ambiguous ref warning --- source/sdk/swift/crud/threading.txt | 2 +- source/sdk/swift/swift-concurrency.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/source/sdk/swift/crud/threading.txt b/source/sdk/swift/crud/threading.txt index 90ac860cfa..0a594eedf4 100644 --- a/source/sdk/swift/crud/threading.txt +++ b/source/sdk/swift/crud/threading.txt @@ -282,7 +282,7 @@ regardless of whether it has a primary key. .. literalinclude:: /examples/generated/code/start/Threading.snippet.threadsafe-wrapper-function-parameter.swift :language: swift -.. _ios-thread-safe-reference: +.. _ios-legacy-thread-safe-reference: Use ThreadSafeReference (Legacy Swift / Objective-C) ```````````````````````````````````````````````````` diff --git a/source/sdk/swift/swift-concurrency.txt b/source/sdk/swift/swift-concurrency.txt index 66295864f5..44f900a7c9 100644 --- a/source/sdk/swift/swift-concurrency.txt +++ b/source/sdk/swift/swift-concurrency.txt @@ -51,7 +51,7 @@ mechanisms for :ref:`sharing objects across threads your code to do some explicit handling to safely pass data across threads. You can use some of these mechanisms, such as :ref:`frozen objects -` or the :ref:`@ThreadSafe property wrapper +` or the :ref:`ThreadSafeReference `, to safely use Realm objects and instances across threads with the ``await`` keyword. You can also avoid threading-related issues by marking any asynchronous Realm code with @@ -198,7 +198,7 @@ To avoid threading-related issues in code that uses Swift concurrency features: in your code. - If you want to explicitly write concurrency code that is not actor-isolated where accessing a realm is done in a thread-safe way, you can explicitly - :ref:`pass instances across threads ` where + :ref:`pass instances across threads ` where applicable to avoid threading-related crashes. This does require a good understanding of Realm's threading model, as well as being mindful of Swift concurrency threading behaviors. From fd341625cfc8b5c8afe938f1059cb0029797302e Mon Sep 17 00:00:00 2001 From: Dachary Carey Date: Mon, 6 Nov 2023 18:05:54 -0500 Subject: [PATCH 05/11] Fixups --- source/sdk/swift/use-realm-with-actors.txt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/source/sdk/swift/use-realm-with-actors.txt b/source/sdk/swift/use-realm-with-actors.txt index 65b19e7e66..eed9f85810 100644 --- a/source/sdk/swift/use-realm-with-actors.txt +++ b/source/sdk/swift/use-realm-with-actors.txt @@ -90,8 +90,7 @@ Define a Custom Realm Actor --------------------------- You can define a specific actor to manage Realm in asynchronous contexts. -You can use this actor to manage realm access, perform write operations, -and get notifications for changes. +You can use this actor to manage realm access and perform write operations. .. literalinclude:: /examples/generated/code/start/RealmActor.snippet.define-realm-actor.swift :language: swift @@ -175,17 +174,16 @@ Pass a ThreadSafeReference You can create a :swift-sdk:`ThreadSafeReference ` on an actor where you have access to the object. In this case, we create a -``ThreadSafeReference`` on the ``MainActor``. +``ThreadSafeReference`` on the ``MainActor``. Then, pass the ``ThreadSafeReference`` to the destination actor. -.. literalinclude:: /examples/generated/code/start/RealmActor.snippet.actor-isolated-realm-async.swift +.. literalinclude:: /examples/generated/code/start/RealmActor.snippet.pass-tsr-across-actor-boundaries.swift :language: swift -Then, pass the ``ThreadSafeReference`` to the destination actor. On the destination actor, you must ``resolve()`` the reference within a write transaction before you can use it. This retrieves a version of the object local to that actor. -.. literalinclude:: /examples/generated/code/start/RealmActor.snippet.actor-isolated-realm-async.swift +.. literalinclude:: /examples/generated/code/start/RealmActor.snippet.resolve-tsr-on-actor.swift :language: swift You can only resolve a ``ThreadSafeReference`` once. If you may need From c8531aed77d50e19fcf38a873b3d76da27d177d3 Mon Sep 17 00:00:00 2001 From: Dachary Carey Date: Mon, 6 Nov 2023 18:12:17 -0500 Subject: [PATCH 06/11] Only get changed files to score for Readability in the SDK directory --- .github/workflows/readability.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/readability.yml b/.github/workflows/readability.yml index 41467ecdd1..f9e5cee30d 100644 --- a/.github/workflows/readability.yml +++ b/.github/workflows/readability.yml @@ -14,7 +14,9 @@ jobs: uses: actions/checkout@v3 - name: Get changed files. id: changed-files - uses: tj-actions/changed-files@v23.2 + uses: tj-actions/changed-files@v40 + with: + files: source/sdk/** - name: List changed files (debugging log statement). run: | echo ${{ steps.changed-files.outputs.all_changed_files }} From 04ac9894552b03f48cd4a0893d61299a783263de Mon Sep 17 00:00:00 2001 From: Dachary Carey Date: Mon, 6 Nov 2023 18:16:47 -0500 Subject: [PATCH 07/11] Change Swift concurrency checking setting to bypass test suite build error --- examples/ios/RealmExamples.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ios/RealmExamples.xcodeproj/project.pbxproj b/examples/ios/RealmExamples.xcodeproj/project.pbxproj index b7a669c9f7..95c33e8d84 100644 --- a/examples/ios/RealmExamples.xcodeproj/project.pbxproj +++ b/examples/ios/RealmExamples.xcodeproj/project.pbxproj @@ -1030,7 +1030,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RealmExamplesHostApp.app/RealmExamplesHostApp"; @@ -1054,7 +1054,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.mongodb.docs.RealmExamples; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; - SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RealmExamplesHostApp.app/RealmExamplesHostApp"; From 3c65080198c8211bf56c6b470a677fb9b1b94677 Mon Sep 17 00:00:00 2001 From: Dachary Carey Date: Wed, 8 Nov 2023 12:46:58 -0500 Subject: [PATCH 08/11] Incorporate team review feedback --- source/sdk/swift/crud/create.txt | 2 +- source/sdk/swift/crud/delete.txt | 2 +- source/sdk/swift/crud/threading.txt | 26 ++++------ source/sdk/swift/crud/update.txt | 2 +- source/sdk/swift/react-to-changes.txt | 4 +- source/sdk/swift/swift-concurrency.txt | 58 +++++++++++++--------- source/sdk/swift/use-realm-with-actors.txt | 53 ++++++++++++++------ 7 files changed, 89 insertions(+), 58 deletions(-) diff --git a/source/sdk/swift/crud/create.txt b/source/sdk/swift/crud/create.txt index f14733b2ab..7e07064d0b 100644 --- a/source/sdk/swift/crud/create.txt +++ b/source/sdk/swift/crud/create.txt @@ -249,7 +249,7 @@ You can use Swift concurrency features to write asynchronously to an actor-isolated realm. This function from the example ``RealmActor`` :ref:`defined on the -Actor-Isolated Realms page ` shows how you might +Use Realm with Actors page ` shows how you might write to an actor-isolated realm: .. literalinclude:: /examples/generated/code/start/RealmActor.snippet.write-async.swift diff --git a/source/sdk/swift/crud/delete.txt b/source/sdk/swift/crud/delete.txt index 0bb4e3f338..c9e0df4a95 100644 --- a/source/sdk/swift/crud/delete.txt +++ b/source/sdk/swift/crud/delete.txt @@ -254,7 +254,7 @@ You can use Swift concurrency features to asynchronously delete objects using an actor-isolated realm. This function from the example ``RealmActor`` :ref:`defined on the -Actor-Isolated Realms page ` shows how you might +Use Realm with Actors page ` shows how you might delete an object in an actor-isolated realm: .. literalinclude:: /examples/generated/code/start/RealmActor.snippet.delete-async.swift diff --git a/source/sdk/swift/crud/threading.txt b/source/sdk/swift/crud/threading.txt index 0a594eedf4..c1b37f720b 100644 --- a/source/sdk/swift/crud/threading.txt +++ b/source/sdk/swift/crud/threading.txt @@ -10,9 +10,6 @@ Threading - Swift SDK :depth: 2 :class: singlecol -Overview --------- - To make your iOS and tvOS apps fast and responsive, you must balance the computing time needed to lay out the visuals and handle user interactions with the time needed to process @@ -37,9 +34,6 @@ simplify this for you. ` or :ref:`frozen objects ` across threads. -Use Realm with Actors ---------------------- - This page describes how to manually manage realm files and objects across threads. Realm also supports using a :apple:`Swift actor ` to manage realm access using Swift concurrency features. For an overview @@ -90,11 +84,11 @@ Perform a Background Write .. versionadded:: 10.26.0 You can add, modify, or delete objects in the background using -:swift-sdk:`asyncWrite `. +:swift-sdk:`writeAsync `. -With async write, you don't need to pass a :ref:`thread-safe reference +With ``writeAsync``, you don't need to pass a :ref:`thread-safe reference ` or :ref:`frozen objects ` -across threads. Instead, call ``realm.asyncWrite``. You can provide +across threads. Instead, call ``realm.writeAsync``. You can provide a completion block for the method to execute on the source thread after the write completes or fails. @@ -115,7 +109,7 @@ performing an async write. The ` variable becomes ``true`` after a call to one of: -- ``asyncWrite`` +- ``writeAsync`` - ``beginAsyncWrite`` - ``commitAsyncWrite`` @@ -131,17 +125,17 @@ To complete an async write, you or the SDK must call either: - :swift-sdk:`commitAsyncWrite ` - :swift-sdk:`cancelAsyncWrite ` -When you use the ``asyncWrite`` method, the SDK handles committing or +When you use the ``writeAsync`` method, the SDK handles committing or canceling the transaction. This provides the convenience of the async write without the need to manually keep state tied to the scope of the object. -However, while in the asyncWrite block, you *can* explicitly call +However, while in the ``writeAsync`` block, you *can* explicitly call ``commitAsyncWrite`` or ``cancelAsyncWrite``. If you return without -calling one of these methods, ``asyncWrite`` either: +calling one of these methods, ``writeAsync`` either: - Commits the write after executing the instructions in the write block - Returns an error -In either case, this completes the ``asyncWrite`` operation. +In either case, this completes the ``writeAsync`` operation. For more control over when to commit or cancel the async write transaction, use the ``beginAsyncWrite`` method. When you use this method, you must @@ -208,7 +202,7 @@ on your use case: the object from the realm. - To share an instance of a realm or specific object with another thread or - across actor boundaries, share a :ref:`thread_safe_reference + across actor boundaries, share a :ref:`thread-safe reference ` to the realm instance or object. For more information, refer to :ref:`swift-pass-thread-safe-reference-across-actors`. @@ -297,7 +291,7 @@ thread-confined instances to another thread as follows: .. important:: You must resolve a ``ThreadSafeReference`` exactly once. Otherwise, - the source realm will remain pinned until the reference gets + the source realm remains pinned until the reference gets deallocated. For this reason, ``ThreadSafeReference`` should be short-lived. diff --git a/source/sdk/swift/crud/update.txt b/source/sdk/swift/crud/update.txt index 25d8598416..63d180d4bf 100644 --- a/source/sdk/swift/crud/update.txt +++ b/source/sdk/swift/crud/update.txt @@ -234,7 +234,7 @@ You can use Swift concurrency features to asynchronously update objects using an actor-isolated realm. This function from the example ``RealmActor`` :ref:`defined on the -Actor-Isolated Realms page ` shows how you might +Use Realm with Actors page ` shows how you might update an object in an actor-isolated realm: .. literalinclude:: /examples/generated/code/start/RealmActor.snippet.update-async.swift diff --git a/source/sdk/swift/react-to-changes.txt b/source/sdk/swift/react-to-changes.txt index 786cb11a0f..4adc33f096 100644 --- a/source/sdk/swift/react-to-changes.txt +++ b/source/sdk/swift/react-to-changes.txt @@ -454,7 +454,7 @@ checking ``realm.isInWriteTransaction``, and if so making changes, calling notifications and potential for error make this manual manipulation error-prone and difficult to debug. -You can use the :ref:`asyncWrite API ` to sidestep complexity +You can use the :ref:`writeAsync API ` to sidestep complexity if you don't need fine-grained change information from inside your write block. Observing an async write similar to this provides notifications even if the notification happens to be delivered inside a write transaction: @@ -464,7 +464,7 @@ notification happens to be delivered inside a write transaction: let token = dog.observe(keyPaths: [\Dog.age]) { change in guard case let .change(dog, _) = change else { return } - dog.realm!.asyncWrite { + dog.realm!.writeAsync { dog.isPuppy = dog.age < 2 } } diff --git a/source/sdk/swift/swift-concurrency.txt b/source/sdk/swift/swift-concurrency.txt index 44f900a7c9..36795348c9 100644 --- a/source/sdk/swift/swift-concurrency.txt +++ b/source/sdk/swift/swift-concurrency.txt @@ -18,9 +18,9 @@ topic `__. While the considerations on this page broadly apply to using realm with Swift concurrency features, Realm Swift SDK version 10.39.0 adds support for using Realm with Swift Actors. You can use Realm isolated to a single -actor, or work with realm across actors. +actor or use Realm across actors. -Realm's actor support simplifies using realm in a MainActor and background actor +Realm's actor support simplifies using Realm in a MainActor and background actor context, and supersedes much of the advice on this page regarding concurrency considerations. For more information, refer to :ref:`swift-actor-isolated-realm`. @@ -76,15 +76,13 @@ examples, check out: - :ref:`Async/Await Login ` - :ref:`Manage Email/Password Users ` - :ref:`Link User Identities - Async/Await ` -- :ref:`Open a Synced Realm ` +- :ref:`Open a Synced Realm ` or a + :ref:`local realm ` +- :ref:`Await notifications from another actor ` - :ref:`Manage Flexible Sync Subscriptions ` - :ref:`Async/Await Call a Serverless Function ` - :ref:`Async/Await Query MongoDB ` - -Realm does not provide Swift async/await support for reading or writing objects. -However, the Realm Swift SDK does provide an API to safely perform background -writes without requiring you to use threading protection. For more information, -refer to **Perform Background Writes** below. +- :ref:`Async/Await CRUD operations ` If you have specific feature requests related to Swift async/await APIs, check out the `MongoDB Feedback Engine for Realm @@ -92,26 +90,39 @@ check out the `MongoDB Feedback Engine for Realm team plans to continue to develop concurrency-related features based on community feedback and Swift concurrency evolution. +.. _swift-perform-background-writes: + Perform Background Writes ~~~~~~~~~~~~~~~~~~~~~~~~~ A commonly-requested use case for asynchronous code is to perform write -operations in the background without blocking the main thread. While the -Realm Swift SDK does not currently support an async/await API for this, it -does have an API specifically for performing background writes: ``asyncWrite``. +operations in the background without blocking the main thread. + +Realm has two APIs that allow for performing asynchronous writes: +- The :swift-sdk:`writeAsync() ` + API allows for performing async writes using Swift completion handlers. +- The :swift-sdk:`asyncWrite() ` + API allows for performing async writes using Swift async/await syntax. -This API allows you to add, update, or delete objects in the background -without using frozen objects or passing a thread-safe reference. With this -API, waiting to obtain the write lock and committing a transaction occur -in the background. The write block itself runs on the calling thread. -This provides thread-safety without requiring you to manually handle -frozen objects or passing references across threads. +Both of these APIs allow you to add, update, or delete objects in the +background without using frozen objects or passing a thread-safe reference. + +With the ``writeAsync()`` API, waiting to obtain the write lock and +committing a transaction occur in the background. The write block itself +runs on the calling thread. This provides thread-safety without requiring +you to manually handle frozen objects or passing references across threads. However, while the write block itself is executed, this does block new transactions on the calling thread. This means that a large write using -the ``asyncWrite`` API could block small, quick writes while it executes. +the ``writeAsync()`` API could block small, quick writes while it executes. + +The ``asyncWrite()`` API suspends the calling task while waiting for its +turn to write rather than blocking the thread. In addition, the actual +I/O to write data to disk is done by a background worker thread. For small +writes, using this function on the main thread may block the main thread +for less time than manually dispatching the write to a background thread. -For more information, including a code example, refer to: :ref:`ios-async-write`. +For more information, including code examples, refer to: :ref:`ios-async-write`. Tasks and TaskGroups -------------------- @@ -143,11 +154,11 @@ networking activities like managing users. Actor Isolation --------------- -.. seealso:: Actor-Isolated Realms +.. seealso:: Use Realm with Swift Actors The information in this section is applicable to Realm SDK versions earlier than 10.39.0. Starting in Realm Swift SDK version 10.39.0 and newer, - the SDK supports actor-isolated realms and related async functionality. + the SDK supports using Realm with Swift Actors and related async functionality. For more information, refer to :ref:`swift-actor-isolated-realm`. @@ -188,14 +199,15 @@ To avoid threading-related issues in code that uses Swift concurrency features: where you access the realm asynchronously with ``@MainActor`` to ensure it always runs on the main thread. Remember that ``await`` marks a suspension point that could change to a different thread. -- Apps that do not use actor-isolated realms can use the ``asyncWrite`` API to +- Apps that do not use actor-isolated realms can use the ``writeAsync`` API to :ref:`perform a background write `. This manages realm access in a thread-safe way without requiring you to write specialized code to do it yourself. This is a special API that outsources aspects of the write process - where it is safe to do so - to run in an async context. Unless you are writing to an actor-isolated realm, you do not use this method with Swift's ``async/await`` syntax. Use this method synchronously - in your code. + in your code. Alternately, you can use the ``asyncWrite`` API with Swift's + ``async/await`` syntax when awaiting writes to asynchronous realms. - If you want to explicitly write concurrency code that is not actor-isolated where accessing a realm is done in a thread-safe way, you can explicitly :ref:`pass instances across threads ` where diff --git a/source/sdk/swift/use-realm-with-actors.txt b/source/sdk/swift/use-realm-with-actors.txt index eed9f85810..456bd027a3 100644 --- a/source/sdk/swift/use-realm-with-actors.txt +++ b/source/sdk/swift/use-realm-with-actors.txt @@ -14,9 +14,9 @@ Use Realm with Actors - Swift SDK Starting with Realm Swift SDK version 10.39.0, Realm supports built-in functionality for using Realm with Swift Actors. Realm's actor support provides an alternative to managing threads or dispatch queues to perform -asynchronous work. You can use Realm with Actors in a few different ways: +asynchronous work. You can use Realm with actors in a few different ways: -- Work with realm *only* on a specific Actor with an actor-isolated realm +- Work with realm *only* on a specific actor with an actor-isolated realm - Use Realm across actors based on the needs of your application You might want to use an actor-isolated realm if you want to restrict all @@ -186,14 +186,32 @@ object local to that actor. .. literalinclude:: /examples/generated/code/start/RealmActor.snippet.resolve-tsr-on-actor.swift :language: swift -You can only resolve a ``ThreadSafeReference`` once. If you may need -to share the same realm object across actors more than once, you may prefer -to share the :ref:`primary key ` and -:ref:`query for it ` on the actor -where you want to use it. +.. important:: + + You must resolve a ``ThreadSafeReference`` exactly once. Otherwise, + the source realm remains pinned until the reference gets + deallocated. For this reason, ``ThreadSafeReference`` should be + short-lived. + + If you may need to share the same realm object across actors more than + once, you may prefer to share the :ref:`primary key ` + and :ref:`query for it ` on + the actor where you want to use it. Refer to the "Pass a Primary Key + and Query for the Object on Another Actor" section on this page for an example. + +Pass a Sendable Type +~~~~~~~~~~~~~~~~~~~~ -Pass Types that are Sendable -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +While Realm objects are not Sendable, you can work around this by passing +Sendable types across actor boundaries. You can use a few strategies to +pass Sendable types and work with data across actor boundaries: + +- Pass Sendable Realm types or primitive values instead of complete Realm objects +- Pass an object's primary key and query for the object on another actor +- Create a Sendable representation of your Realm object, such as a struct + +Pass Sendable Realm Types and Primitive Values +`````````````````````````````````````````````` If you only need a piece of information from the Realm object, such as a ``String`` or ``Int``, you can pass the value directly across actors instead @@ -203,6 +221,9 @@ refer to :ref:`concurrency-page-sendable-thread-confined-reference`. .. literalinclude:: /examples/generated/code/start/RealmActor.snippet.pass-primitive-data-across-actors.swift :language: swift +Pass a Primary Key and Query for the Object on Another Actor +```````````````````````````````````````````````````````````` + If you want to use a Realm object on another actor, you can share the :ref:`primary key ` and :ref:`query for it ` on the actor @@ -211,6 +232,9 @@ where you want to use it. .. literalinclude:: /examples/generated/code/start/RealmActor.snippet.query-for-data-on-another-actor.swift :language: swift +Create a Sendable Representation of Your Object +``````````````````````````````````````````````` + If you need to work with more than a simple value, but don't want the overhead of passing around ``ThreadSafeReferences`` or querying objects on different actors, you can create a struct or other Sendable representation @@ -255,13 +279,14 @@ thread or on another actor, call ``await realm.asyncRefresh()``. This updates the realm and outstanding objects managed by the Realm to point to the most recent data and deliver any applicable notifications. -.. warning:: +Observation Limitations +~~~~~~~~~~~~~~~~~~~~~~~ - You cannot call the ``observe`` method during a write transaction - or when the containing realm is read-only. +You *cannot* call the ``.observe()`` method: - You cannot call the ``observe`` method on an actor-confined realm from - outside the actor. +- During a write transaction +- When the containing realm is read-only +- On an actor-confined realm from outside the actor .. _swift-actor-collection-change-listener: From 6194407652faa82306c84e467440e5739abf10fa Mon Sep 17 00:00:00 2001 From: Dachary Carey Date: Wed, 8 Nov 2023 18:00:57 -0500 Subject: [PATCH 09/11] Fix snooty build error --- source/sdk/swift/swift-concurrency.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/sdk/swift/swift-concurrency.txt b/source/sdk/swift/swift-concurrency.txt index 36795348c9..986b38cd8c 100644 --- a/source/sdk/swift/swift-concurrency.txt +++ b/source/sdk/swift/swift-concurrency.txt @@ -99,7 +99,8 @@ A commonly-requested use case for asynchronous code is to perform write operations in the background without blocking the main thread. Realm has two APIs that allow for performing asynchronous writes: -- The :swift-sdk:`writeAsync() ` + +- The :swift-sdk:`writeAsync() ` API allows for performing async writes using Swift completion handlers. - The :swift-sdk:`asyncWrite() ` API allows for performing async writes using Swift async/await syntax. From 708e46ac9de8827b679d3e1579db428b67d13cb7 Mon Sep 17 00:00:00 2001 From: Dachary Carey Date: Thu, 9 Nov 2023 14:26:48 -0500 Subject: [PATCH 10/11] Simplify actor examples and fix unnecessary return --- examples/ios/Examples/RealmActor.swift | 68 +++++++++---------- ....snippet.observe-collection-on-actor.swift | 11 ++- ...ctor.snippet.observe-object-on-actor.swift | 13 ++-- ...pet.pass-tsr-across-actor-boundaries.swift | 2 +- ...ppet.query-for-data-on-another-actor.swift | 40 ++++++----- ...lmActor.snippet.resolve-tsr-on-actor.swift | 4 +- 6 files changed, 65 insertions(+), 73 deletions(-) diff --git a/examples/ios/Examples/RealmActor.swift b/examples/ios/Examples/RealmActor.swift index 11c39f1858..01e52023ac 100644 --- a/examples/ios/Examples/RealmActor.swift +++ b/examples/ios/Examples/RealmActor.swift @@ -389,9 +389,7 @@ class RealmActorTests: XCTestCase { // :snippet-start: observe-collection-on-actor // Create a simple actor // :snippet-start: resolve-tsr-on-actor - @globalActor actor BackgroundActor: GlobalActor { - static var shared = BackgroundActor() - + actor BackgroundActor { public func deleteTodo(tsrToTodo tsr: ThreadSafeReference) throws { let realm = try! Realm() try realm.write { @@ -407,11 +405,12 @@ class RealmActorTests: XCTestCase { // Execute some code on a different actor - in this case, the MainActor @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 { - return realm.create(RealmActor_Todo.self, value: [ + realm.create(RealmActor_Todo.self, value: [ "_id": ObjectId.generate(), "name": "Arrive safely in Bree", "owner": "Merry", @@ -425,7 +424,7 @@ class RealmActorTests: XCTestCase { // 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.shared, { actor, changes in + let token = await todoCollection.observe(on: backgroundActor, { actor, changes in print("A change occurred on actor: \(actor)") switch changes { case .initial: @@ -452,7 +451,7 @@ class RealmActorTests: XCTestCase { $0.name == "Arrive safely in Bree" }.first! let threadSafeReferenceToTodo = ThreadSafeReference(to: todo) - try await BackgroundActor.shared.deleteTodo(tsrToTodo: threadSafeReferenceToTodo) + try await backgroundActor.deleteTodo(tsrToTodo: threadSafeReferenceToTodo) // :snippet-end: // Invalidate the token when done observing @@ -467,15 +466,15 @@ class RealmActorTests: XCTestCase { func testObserveObjectOnActor() async throws { let expectation = expectation(description: "A notification is triggered") + actor BackgroundActor { } // :snippet-start: observe-object-on-actor - // Create a simple actor - @globalActor actor BackgroundActor: GlobalActor { - static var shared = BackgroundActor() - } - - // Execute some code on a different actor - in this case, the MainActor + // Execute some code on a specific actor - in this case, the MainActor @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 { @@ -489,7 +488,7 @@ class RealmActorTests: XCTestCase { XCTAssertNotNil(scourTheShire) // :remove: // Register a notification token, providing the actor - let token = await scourTheShire.observe(on: BackgroundActor.shared, { actor, change in + 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): @@ -524,32 +523,31 @@ class RealmActorTests: XCTestCase { func testQueryForDataOnAnotherActor() async throws { try await mainThreadFunction() + actor BackgroundActor { } // :snippet-start: query-for-data-on-another-actor - // A simple example of a custom global actor - @globalActor actor BackgroundActor: GlobalActor { - static var shared = BackgroundActor() - } - - @BackgroundActor - func createObjectOnBackgroundActor() async throws -> ObjectId { - // Explicitly specifying the actor is required for anything that is not MainActor - let realm = try await Realm(actor: BackgroundActor.shared) - let newTodo = try await realm.asyncWrite { - return realm.create(RealmActor_Todo.self, value: [ - "name": "Pledge fealty and service to Gondor", - "owner": "Pippin", - "status": "In Progress" - ]) - } - XCTAssertEqual(realm.objects(RealmActor_Todo.self).count, 1) // :remove: - // Share the todo's primary key so we can easily query for it on another actor - return newTodo._id - } - + // Execute code on a specific actor - in this case, the @MainActor @MainActor func mainThreadFunction() async throws { - let newTodoId = try await createObjectOnBackgroundActor() + // 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(RealmActor_Todo.self, value: [ + "name": "Pledge fealty and service to Gondor", + "owner": "Pippin", + "status": "In Progress" + ]) + } + + XCTAssertEqual(realm.objects(RealmActor_Todo.self).count, 1) // :remove: + // 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: RealmActor_Todo.self, forPrimaryKey: newTodoId) XCTAssertNotNil(todoOnMainActor) // :remove: diff --git a/source/examples/generated/code/start/RealmActor.snippet.observe-collection-on-actor.swift b/source/examples/generated/code/start/RealmActor.snippet.observe-collection-on-actor.swift index c87f081d04..55fdcf7edc 100644 --- a/source/examples/generated/code/start/RealmActor.snippet.observe-collection-on-actor.swift +++ b/source/examples/generated/code/start/RealmActor.snippet.observe-collection-on-actor.swift @@ -1,7 +1,5 @@ // Create a simple actor -@globalActor actor BackgroundActor: GlobalActor { - static var shared = BackgroundActor() - +actor BackgroundActor { public func deleteTodo(tsrToTodo tsr: ThreadSafeReference) throws { let realm = try! Realm() try realm.write { @@ -16,11 +14,12 @@ // Execute some code on a different actor - in this case, the MainActor @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 { - return realm.create(Todo.self, value: [ + realm.create(Todo.self, value: [ "_id": ObjectId.generate(), "name": "Arrive safely in Bree", "owner": "Merry", @@ -33,7 +32,7 @@ func mainThreadFunction() async throws { // 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.shared, { actor, changes in + let token = await todoCollection.observe(on: backgroundActor, { actor, changes in print("A change occurred on actor: \(actor)") switch changes { case .initial: @@ -58,7 +57,7 @@ func mainThreadFunction() async throws { $0.name == "Arrive safely in Bree" }.first! let threadSafeReferenceToTodo = ThreadSafeReference(to: todo) - try await BackgroundActor.shared.deleteTodo(tsrToTodo: threadSafeReferenceToTodo) + try await backgroundActor.deleteTodo(tsrToTodo: threadSafeReferenceToTodo) // Invalidate the token when done observing token.invalidate() diff --git a/source/examples/generated/code/start/RealmActor.snippet.observe-object-on-actor.swift b/source/examples/generated/code/start/RealmActor.snippet.observe-object-on-actor.swift index 127ac40066..03f7d79343 100644 --- a/source/examples/generated/code/start/RealmActor.snippet.observe-object-on-actor.swift +++ b/source/examples/generated/code/start/RealmActor.snippet.observe-object-on-actor.swift @@ -1,11 +1,10 @@ -// Create a simple actor -@globalActor actor BackgroundActor: GlobalActor { - static var shared = BackgroundActor() -} - -// Execute some code on a different actor - in this case, the MainActor +// Execute some code on a specific actor - in this case, the MainActor @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 { @@ -18,7 +17,7 @@ func mainThreadFunction() async throws { } // Register a notification token, providing the actor - let token = await scourTheShire.observe(on: BackgroundActor.shared, { actor, change in + 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): diff --git a/source/examples/generated/code/start/RealmActor.snippet.pass-tsr-across-actor-boundaries.swift b/source/examples/generated/code/start/RealmActor.snippet.pass-tsr-across-actor-boundaries.swift index b1fdb9c50b..8758b6b38f 100644 --- a/source/examples/generated/code/start/RealmActor.snippet.pass-tsr-across-actor-boundaries.swift +++ b/source/examples/generated/code/start/RealmActor.snippet.pass-tsr-across-actor-boundaries.swift @@ -3,4 +3,4 @@ let todo = todoCollection.where { $0.name == "Arrive safely in Bree" }.first! let threadSafeReferenceToTodo = ThreadSafeReference(to: todo) -try await BackgroundActor.shared.deleteTodo(tsrToTodo: threadSafeReferenceToTodo) +try await backgroundActor.deleteTodo(tsrToTodo: threadSafeReferenceToTodo) diff --git a/source/examples/generated/code/start/RealmActor.snippet.query-for-data-on-another-actor.swift b/source/examples/generated/code/start/RealmActor.snippet.query-for-data-on-another-actor.swift index e1a73b5cc6..5a621b2107 100644 --- a/source/examples/generated/code/start/RealmActor.snippet.query-for-data-on-another-actor.swift +++ b/source/examples/generated/code/start/RealmActor.snippet.query-for-data-on-another-actor.swift @@ -1,26 +1,24 @@ -// A simple example of a custom global actor -@globalActor actor BackgroundActor: GlobalActor { - static var shared = BackgroundActor() -} - -@BackgroundActor -func createObjectOnBackgroundActor() async throws -> ObjectId { - // Explicitly specifying the actor is required for anything that is not MainActor - let realm = try await Realm(actor: BackgroundActor.shared) - 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 -} - +// Execute code on a specific actor - in this case, the @MainActor @MainActor func mainThreadFunction() async throws { - let newTodoId = try await createObjectOnBackgroundActor() + // 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) } diff --git a/source/examples/generated/code/start/RealmActor.snippet.resolve-tsr-on-actor.swift b/source/examples/generated/code/start/RealmActor.snippet.resolve-tsr-on-actor.swift index 2d9552b74a..00d80f082e 100644 --- a/source/examples/generated/code/start/RealmActor.snippet.resolve-tsr-on-actor.swift +++ b/source/examples/generated/code/start/RealmActor.snippet.resolve-tsr-on-actor.swift @@ -1,6 +1,4 @@ -@globalActor actor BackgroundActor: GlobalActor { - static var shared = BackgroundActor() - +actor BackgroundActor { public func deleteTodo(tsrToTodo tsr: ThreadSafeReference) throws { let realm = try! Realm() try realm.write { From 16a9ee7788e8d60f720e0e1685ee1551cf2b8bdc Mon Sep 17 00:00:00 2001 From: Dachary Carey Date: Thu, 9 Nov 2023 16:45:19 -0500 Subject: [PATCH 11/11] Extend timeout for test that is failing in CI --- examples/ios/Examples/MongoDBRemoteAccess.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ios/Examples/MongoDBRemoteAccess.swift b/examples/ios/Examples/MongoDBRemoteAccess.swift index 3753a3a811..bbb102e8df 100644 --- a/examples/ios/Examples/MongoDBRemoteAccess.swift +++ b/examples/ios/Examples/MongoDBRemoteAccess.swift @@ -1288,7 +1288,7 @@ class MongoDBRemoteAccessTestCaseAsyncAPIs: XCTestCase { print("Received event: \(event.documentValue!)") } } - await fulfillment(of: [openEx], timeout: 2.0) // :remove: + await fulfillment(of: [openEx], timeout: 5.0) // :remove: // Updating a document in the collection triggers a change event. let queryFilter: Document = ["_id": AnyBSON(objectId) ]