diff --git a/Realm.xcodeproj/project.pbxproj b/Realm.xcodeproj/project.pbxproj index 82b6715fd5..de4ee3e274 100644 --- a/Realm.xcodeproj/project.pbxproj +++ b/Realm.xcodeproj/project.pbxproj @@ -2895,6 +2895,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 3FEC91542A4B59A40044BFF5 /* Static.xcconfig */; buildSettings = { + SWIFT_VERSION = 6.0; }; name = Static; }; @@ -2909,6 +2910,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 5D660FBD1BE98BEF0021E04F /* RealmSwift.xcconfig */; buildSettings = { + SWIFT_VERSION = 6.0; }; name = Static; }; @@ -3087,6 +3089,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 5D660FBD1BE98BEF0021E04F /* RealmSwift.xcconfig */; buildSettings = { + SWIFT_VERSION = 6.0; }; name = Debug; }; @@ -3094,6 +3097,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 5D660FBD1BE98BEF0021E04F /* RealmSwift.xcconfig */; buildSettings = { + SWIFT_VERSION = 6.0; }; name = Release; }; @@ -3233,6 +3237,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 5D659E6E1BE0398E006515A0 /* Debug.xcconfig */; buildSettings = { + SWIFT_VERSION = 6.0; }; name = Debug; }; @@ -3240,6 +3245,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 5D659E6F1BE0398E006515A0 /* Release.xcconfig */; buildSettings = { + SWIFT_VERSION = 6.0; }; name = Release; }; diff --git a/Realm/ObjectServerTests/AsyncSyncTests.swift b/Realm/ObjectServerTests/AsyncSyncTests.swift index d358af5b52..275aa72c3f 100644 --- a/Realm/ObjectServerTests/AsyncSyncTests.swift +++ b/Realm/ObjectServerTests/AsyncSyncTests.swift @@ -279,9 +279,14 @@ class AsyncAwaitSyncTests: SwiftSyncTestCase { let configuration = try configuration() func isolatedOpen(_ actor: isolated CustomExecutorActor) async throws { +#if compiler(<6) _ = try await Realm(configuration: configuration, actor: actor, downloadBeforeOpen: .always) +#else + _ = try await Realm.open(configuration: configuration, downloadBeforeOpen: .always) +#endif } + // Try opening the Realm with the Task being cancelled at every possible // point between executor invocations. This doesn't really test that // cancellation is *correct*; just that cancellation never results in @@ -825,7 +830,7 @@ class AsyncFlexibleSyncTests: SwiftSyncTestCase { .subscribe(name: "8 or older") realm.syncSession!.suspend() let ex = XCTestExpectation(description: "no attempt to re-create subscription, returns immediately") - Task { + Task { @MainActor in _ = try await realm.objects(SwiftPerson.self) .where { $0.firstName == name && $0.age >= 8 } .subscribe(name: "8 or older") @@ -875,7 +880,7 @@ class AsyncFlexibleSyncTests: SwiftSyncTestCase { let user = try await createUser() var config = user.flexibleSyncConfiguration() config.objectTypes = [SwiftPerson.self] - let realm = try await Realm(configuration: config, actor: MainActor.shared) + let realm = try await Realm(configuration: config) let results1 = try await realm.objects(SwiftPerson.self) .where { $0.firstName == name && $0.age > 8 }.subscribe(waitForSync: .onCreation) XCTAssertEqual(results1.count, 2) @@ -895,7 +900,11 @@ class AsyncFlexibleSyncTests: SwiftSyncTestCase { let user = try await createUser() var config = user.flexibleSyncConfiguration() config.objectTypes = [SwiftPerson.self] +#if compiler(<6) let realm = try await Realm(configuration: config, actor: CustomGlobalActor.shared) +#else + let realm = try await Realm.open(configuration: config) +#endif let results1 = try await realm.objects(SwiftPerson.self) .where { $0.firstName == name && $0.age > 8 }.subscribe(waitForSync: .onCreation) XCTAssertEqual(results1.count, 2) diff --git a/Realm/ObjectServerTests/CombineSyncTests.swift b/Realm/ObjectServerTests/CombineSyncTests.swift index 25c151b75f..4c554d354c 100644 --- a/Realm/ObjectServerTests/CombineSyncTests.swift +++ b/Realm/ObjectServerTests/CombineSyncTests.swift @@ -31,6 +31,14 @@ import RealmTestSupport import RealmSwiftTestSupport #endif +extension AnyCancellable { + func store(in lockedSet: Locked>) { + lockedSet.withLock { + self.store(in: &$0) + } + } +} + @available(macOS 13, *) @objc(CombineSyncTests) class CombineSyncTests: SwiftSyncTestCase { @@ -38,10 +46,12 @@ class CombineSyncTests: SwiftSyncTestCase { [Dog.self, SwiftPerson.self, SwiftHugeSyncObject.self] } - var subscriptions: Set = [] + nonisolated let subscriptions = Locked(Set()) override func tearDown() { - subscriptions.forEach { $0.cancel() } - subscriptions = [] + subscriptions.withLock { + $0.forEach { $0.cancel() } + $0 = [] + } super.tearDown() } @@ -62,7 +72,7 @@ class CombineSyncTests: SwiftSyncTestCase { .sink(receiveCompletion: { @Sendable _ in }) { @Sendable _ in XCTAssertFalse(Thread.isMainThread) watchEx1.wrappedValue.fulfill() - }.store(in: &subscriptions) + }.store(in: subscriptions) collection.watch() .onOpen { @@ -73,7 +83,7 @@ class CombineSyncTests: SwiftSyncTestCase { .sink(receiveCompletion: { _ in }) { _ in XCTAssertTrue(Thread.isMainThread) watchEx2.wrappedValue.fulfill() - }.store(in: &subscriptions) + }.store(in: subscriptions) for _ in 0..<3 { wait(for: [watchEx1.wrappedValue, watchEx2.wrappedValue], timeout: 60.0) @@ -116,7 +126,7 @@ class CombineSyncTests: SwiftSyncTestCase { if objectId == objectIds[0] { watchEx1.wrappedValue.fulfill() } - }.store(in: &subscriptions) + }.store(in: subscriptions) collection.watch(filterIds: [objectIds[1]]) .onOpen { @@ -134,7 +144,7 @@ class CombineSyncTests: SwiftSyncTestCase { if objectId == objectIds[1] { watchEx2.wrappedValue.fulfill() } - }.store(in: &subscriptions) + }.store(in: subscriptions) for i in 0..<3 { wait(for: [watchEx1.wrappedValue, watchEx2.wrappedValue], timeout: 60.0) @@ -187,7 +197,7 @@ class CombineSyncTests: SwiftSyncTestCase { if objectId == objectIds[0] { watchEx1.wrappedValue.fulfill() } - }.store(in: &subscriptions) + }.store(in: subscriptions) collection.watch(matchFilter: ["fullDocument._id": AnyBSON.objectId(objectIds[1])]) .onOpen { @@ -205,7 +215,7 @@ class CombineSyncTests: SwiftSyncTestCase { if objectId == objectIds[1] { watchEx2.wrappedValue.fulfill() } - }.store(in: &subscriptions) + }.store(in: subscriptions) for i in 0..<3 { wait(for: [watchEx1.wrappedValue, watchEx2.wrappedValue], timeout: 60.0) @@ -243,7 +253,7 @@ class CombineSyncTests: SwiftSyncTestCase { if triggered == 2 { appEx.fulfill() } - }.store(in: &subscriptions) + }.store(in: subscriptions) app.emailPasswordAuth.registerUser(email: email, password: password) .flatMap { @Sendable in self.app.login(credentials: .emailPassword(email: email, password: password)) } @@ -256,11 +266,11 @@ class CombineSyncTests: SwiftSyncTestCase { user.objectWillChange.sink { @Sendable user in XCTAssert(!user.isLoggedIn) loginEx.fulfill() - }.store(in: &self.subscriptions) + }.store(in: self.subscriptions) XCTAssertEqual(user.id, self.app.currentUser?.id) user.logOut { _ in } // logout user and make sure it is observed }) - .store(in: &subscriptions) + .store(in: subscriptions) wait(for: [loginEx, appEx], timeout: 30.0) XCTAssertEqual(self.app.allUsers.count, 1) XCTAssertEqual(triggered, 2) @@ -306,7 +316,7 @@ class CombineSyncTests: SwiftSyncTestCase { } .expectValue(self, chainEx) { realm in XCTAssertEqual(realm.objects(SwiftHugeSyncObject.self).count, 2) - }.store(in: &subscriptions) + }.store(in: subscriptions) wait(for: [chainEx, progressEx], timeout: 30.0) } @@ -334,7 +344,7 @@ class CombineSyncTests: SwiftSyncTestCase { if triggered == 2 { appEx.fulfill() } - }.store(in: &subscriptions) + }.store(in: subscriptions) app.emailPasswordAuth.registerUser(email: email, password: password) .flatMap { @Sendable in self.app.login(credentials: .emailPassword(email: email, password: password)) } @@ -639,10 +649,12 @@ class CombineFlexibleSyncTests: SwiftSyncTestCase { try createFlexibleSyncApp() } - var cancellables: Set = [] + nonisolated let cancellables = Locked(Set()) override func tearDown() { - cancellables.forEach { $0.cancel() } - cancellables = [] + cancellables.withLock { + $0.forEach { $0.cancel() } + $0 = [] + } super.tearDown() } @@ -669,7 +681,7 @@ class CombineFlexibleSyncTests: SwiftSyncTestCase { }) }.sink(receiveCompletion: { @Sendable _ in }, receiveValue: { @Sendable _ in ex.fulfill() } - ).store(in: &cancellables) + ).store(in: cancellables) waitForExpectations(timeout: 20.0, handler: nil) @@ -701,7 +713,7 @@ class CombineFlexibleSyncTests: SwiftSyncTestCase { } ex.fulfill() }, receiveValue: { _ in }) - .store(in: &cancellables) + .store(in: cancellables) waitForExpectations(timeout: 20.0, handler: nil) diff --git a/Realm/ObjectServerTests/RealmServer.swift b/Realm/ObjectServerTests/RealmServer.swift index 6f324394b1..75cdc84a94 100644 --- a/Realm/ObjectServerTests/RealmServer.swift +++ b/Realm/ObjectServerTests/RealmServer.swift @@ -213,15 +213,21 @@ private extension DispatchGroup { // MARK: AdminSession /// An authenticated session for using the Admin API -class AdminSession { +final class AdminSession: Sendable { /// The access token of the authenticated user - var accessToken: String + let accessToken: String /// The group id associated with the authenticated user - var groupId: String + let groupId: String init(accessToken: String, groupId: String) { self.accessToken = accessToken self.groupId = groupId + apps = .init(accessToken: accessToken, + groupId: groupId, + url: URL(string: "http://localhost:9090/api/admin/v3.0/groups/\(groupId)/apps")!) + privateApps = .init(accessToken: accessToken, + groupId: groupId, + url: URL(string: "http://localhost:9090/api/private/v1.0/groups/\(groupId)/apps")!) } // MARK: AdminEndpoint @@ -408,14 +414,10 @@ class AdminSession { } /// The initial endpoint to access the admin server - lazy var apps = AdminEndpoint(accessToken: accessToken, - groupId: groupId, - url: URL(string: "http://localhost:9090/api/admin/v3.0/groups/\(groupId)/apps")!) + let apps: AdminEndpoint/// /// The initial endpoint to access the private API - lazy var privateApps = AdminEndpoint(accessToken: accessToken, - groupId: groupId, - url: URL(string: "http://localhost:9090/api/private/v1.0/groups/\(groupId)/apps")!) + let privateApps: AdminEndpoint } // MARK: - Admin @@ -479,7 +481,7 @@ public enum SyncMode { and allows for app creation. */ @objc(RealmServer) -public class RealmServer: NSObject { +final public class RealmServer: NSObject, Sendable { public enum LogLevel: Sendable { case none, info, warn, error } @@ -488,7 +490,7 @@ public class RealmServer: NSObject { @objc public static let shared = RealmServer() /// Log level for the server and mongo processes. - public var logLevel = LogLevel.none + public let logLevel = LogLevel.none /// Process that runs the local mongo server. Should be terminated on exit. private let mongoProcess = Process() @@ -505,17 +507,17 @@ public class RealmServer: NSObject { /// The directory where mongo stores its files. This is a unique value so that /// we have a fresh mongo each run. - private lazy var tempDir = URL(fileURLWithPath: NSTemporaryDirectory(), - isDirectory: true).appendingPathComponent("realm-test-\(UUID().uuidString)") + private let tempDir = URL(fileURLWithPath: NSTemporaryDirectory(), + isDirectory: true).appendingPathComponent("realm-test-\(UUID().uuidString)") /// Whether or not this is a parent or child process. - private lazy var isParentProcess = (getenv("RLMProcessIsChild") == nil) + private let isParentProcess = (getenv("RLMProcessIsChild") == nil) /// The current admin session - private var session: AdminSession? + private let session: AdminSession /// Created appIds which should be cleaned up - private var appIds = [String]() + private let appIds = Locked([String]()) /// Check if the BaaS files are present and we can run the server @objc public class func haveServer() -> Bool { @@ -1005,7 +1007,7 @@ public class RealmServer: NSObject { try waitForSync(appServerId: appId, expectedCount: schema.count - asymmetricTables.count) if !persistent { - appIds.append(appId) + appIds.withLock { $0.append(appId) } } return clientAppId @@ -1025,11 +1027,11 @@ public class RealmServer: NSObject { /// Delete all Apps created without `persistent: true` @objc func deleteApps() throws { - for appId in appIds { + for appId in appIds.value { let app = try XCTUnwrap(session).apps[appId] _ = try app.delete().get() } - appIds = [] + appIds.value = [] } @objc func deleteApp(_ appId: String) throws { @@ -1079,9 +1081,6 @@ public class RealmServer: NSObject { } public func getSyncServiceConfiguration(appServerId: String, syncServiceId: String) throws -> [String: Any]? { - guard let session = session else { - fatalError() - } let app = session.apps[appServerId] do { return try app.services[syncServiceId].config.get().get() as? [String: Any] @@ -1101,33 +1100,23 @@ public class RealmServer: NSObject { } public func isDevModeEnabled(appServerId: String, syncServiceId: String) throws -> Bool { - guard let session = session else { - fatalError() - } let app = session.apps[appServerId] let res = try app.sync.config.get().get() as! [String: Any] return res["development_mode_enabled"] as? Bool ?? false } public func enableDevMode(appServerId: String, syncServiceId: String, syncServiceConfiguration: [String: Any]) -> Result { - guard let session = session else { - return .failure(URLError(.unknown)) - } let app = session.apps[appServerId] return app.sync.config.put(["development_mode_enabled": true]) } public func disableSync(appServerId: String, syncServiceId: String) throws -> Any? { - let session = try XCTUnwrap(session) let app = session.apps[appServerId] return app.services[syncServiceId].config.patch(["flexible_sync": ["state": ""]]) } public func enableSync(appServerId: String, syncServiceId: String, syncServiceConfiguration: [String: Any]) -> Result { var syncConfig = syncServiceConfiguration - guard let session = session else { - return .failure(URLError(.unknown)) - } let app = session.apps[appServerId] guard var syncInfo = syncConfig["flexible_sync"] as? [String: Any] else { return .failure(URLError(.unknown)) @@ -1139,10 +1128,6 @@ public class RealmServer: NSObject { public func patchRecoveryMode(flexibleSync: Bool, disable: Bool, _ appServerId: String, _ syncServiceId: String, _ syncServiceConfiguration: [String: Any]) -> Result { - guard let session = session else { - return .failure(URLError(.unknown)) - } - let configOption = flexibleSync ? "flexible_sync" : "sync" let app = session.apps[appServerId] var syncConfig = syncServiceConfiguration @@ -1168,8 +1153,7 @@ public class RealmServer: NSObject { } public func retrieveUser(_ appId: String, userId: String) -> Result { - guard let appServerId = try? RealmServer.shared.retrieveAppServerId(appId), - let session = session else { + guard let appServerId = try? RealmServer.shared.retrieveAppServerId(appId) else { return .failure(URLError(.unknown)) } return session.apps[appServerId].users[userId].get() @@ -1177,26 +1161,22 @@ public class RealmServer: NSObject { // Remove User from Atlas App Services using the Admin API public func removeUserForApp(_ appId: String, userId: String) -> Result { - guard let appServerId = try? RealmServer.shared.retrieveAppServerId(appId), - let session = session else { + guard let appServerId = try? RealmServer.shared.retrieveAppServerId(appId) else { return .failure(URLError(.unknown)) } return session.apps[appServerId].users[userId].delete() } public func revokeUserSessions(_ appId: String, userId: String) -> Result { - guard let appServerId = try? RealmServer.shared.retrieveAppServerId(appId), - let session = session else { + guard let appServerId = try? RealmServer.shared.retrieveAppServerId(appId) else { return .failure(URLError(.unknown)) } return session.apps[appServerId].users[userId].logout.put([:]) } - public func retrieveSchemaProperties(_ appId: String, className: String, _ completion: @escaping (Result<[String], Error>) -> Void) { - guard let appServerId = try? RealmServer.shared.retrieveAppServerId(appId), - let session = session else { - fatalError() - } + public func retrieveSchemaProperties(_ appId: String, className: String, + _ completion: @escaping (Result<[String], Error>) -> Void) { + let appServerId = try! RealmServer.shared.retrieveAppServerId(appId) guard let schemasList = try? session.apps[appServerId].schemas.get().get(), let schemas = schemasList as? [[String: Any]], diff --git a/Realm/ObjectServerTests/SwiftObjectServerTests.swift b/Realm/ObjectServerTests/SwiftObjectServerTests.swift index 711cd66384..8adc8762e2 100644 --- a/Realm/ObjectServerTests/SwiftObjectServerTests.swift +++ b/Realm/ObjectServerTests/SwiftObjectServerTests.swift @@ -32,13 +32,13 @@ import RealmSwiftTestSupport #endif func assertAppError(_ error: AppError, _ code: AppError.Code, _ message: String, - line: UInt = #line, file: StaticString = #file) { + line: UInt = #line, file: StaticString = #filePath) { XCTAssertEqual(error.code, code, file: file, line: line) XCTAssertEqual(error.localizedDescription, message, file: file, line: line) } func assertSyncError(_ error: Error, _ code: SyncError.Code, _ message: String, - line: UInt = #line, file: StaticString = #file) { + line: UInt = #line, file: StaticString = #filePath) { let e = error as NSError XCTAssertEqual(e.domain, RLMSyncErrorDomain, file: file, line: line) XCTAssertEqual(e.code, code.rawValue, file: file, line: line) diff --git a/Realm/ObjectServerTests/SwiftSyncTestCase.swift b/Realm/ObjectServerTests/SwiftSyncTestCase.swift index ed77eefb32..1e0412e264 100644 --- a/Realm/ObjectServerTests/SwiftSyncTestCase.swift +++ b/Realm/ObjectServerTests/SwiftSyncTestCase.swift @@ -60,7 +60,7 @@ open class SwiftSyncTestCase: RLMSyncTestCase { // Must be overriden in each subclass to specify which types will be used // in this test case. - open var objectTypes: [ObjectBase.Type] { + nonisolated open var objectTypes: [ObjectBase.Type] { [SwiftPerson.self] } @@ -68,7 +68,7 @@ open class SwiftSyncTestCase: RLMSyncTestCase { objectTypes } - public func executeChild(file: StaticString = #file, line: UInt = #line) { + public func executeChild(file: StaticString = #filePath, line: UInt = #line) { XCTAssert(0 == runChildAndWait(), "Tests in child process failed", file: file, line: line) } diff --git a/Realm/ObjectServerTests/SwiftUIServerTests.swift b/Realm/ObjectServerTests/SwiftUIServerTests.swift index 76e1fa1235..52698d8028 100644 --- a/Realm/ObjectServerTests/SwiftUIServerTests.swift +++ b/Realm/ObjectServerTests/SwiftUIServerTests.swift @@ -26,7 +26,7 @@ import RealmSwiftSyncTestSupport import RealmSyncTestSupport #endif -protocol AsyncOpenStateWrapper { +@MainActor protocol AsyncOpenStateWrapper { func cancel() var wrappedValue: AsyncOpenState { get } var projectedValue: Published.Publisher { get } @@ -565,7 +565,7 @@ class PBSSwiftUIServerTests: SwiftUIServerTests { @available(macOS 13, *) @MainActor -class FLXSwiftUIServerTests: SwiftUIServerTests { +class FLXSwiftUIServerTests: SwiftUIServerTests, Sendable { override func createApp() throws -> String { try createFlexibleSyncApp() } diff --git a/Realm/ObjectServerTests/TimeoutProxyServer.swift b/Realm/ObjectServerTests/TimeoutProxyServer.swift index e7ea3cc41d..47e35b83b5 100644 --- a/Realm/ObjectServerTests/TimeoutProxyServer.swift +++ b/Realm/ObjectServerTests/TimeoutProxyServer.swift @@ -64,7 +64,7 @@ public class TimeoutProxyServer: NSObject, @unchecked Sendable { @objc public func start() throws { listener = try NWListener(using: NWParameters.tcp, on: port) - listener.newConnectionHandler = { [weak self] incomingConnection in + listener.newConnectionHandler = { @Sendable [weak self] incomingConnection in guard let self = self else { return } self.connections.append(incomingConnection) incomingConnection.start(queue: self.queue) diff --git a/Realm/RLMUser.h b/Realm/RLMUser.h index ab034a8da8..db0a43ad6e 100644 --- a/Realm/RLMUser.h +++ b/Realm/RLMUser.h @@ -309,7 +309,7 @@ RLM_SWIFT_SENDABLE // internally thread-safe /** Refresh a user's custom data. This will, in effect, refresh the user's auth session. */ -- (void)refreshCustomDataWithCompletion:(RLMUserCustomDataBlock)completion; +- (void)refreshCustomDataWithCompletion:(RLMUserCustomDataBlock)completion NS_REFINED_FOR_SWIFT; /** Links the currently authenticated user with a new identity, where the identity is defined by the credential diff --git a/Realm/Tests/Swift/SwiftRealmTests.swift b/Realm/Tests/Swift/SwiftRealmTests.swift index 0425eca50f..a70db81864 100644 --- a/Realm/Tests/Swift/SwiftRealmTests.swift +++ b/Realm/Tests/Swift/SwiftRealmTests.swift @@ -24,8 +24,7 @@ import RealmTestSupport #endif @available(iOS 13.0, *) // For @MainActor -@MainActor -class SwiftRLMRealmTests: RLMTestCase { +class SwiftRLMRealmTests: RLMTestCase, @unchecked Sendable { // No models @@ -68,6 +67,7 @@ class SwiftRLMRealmTests: RLMTestCase { XCTAssertEqual((objects[0] as! SwiftRLMStringObject).stringCol, "b", "Expecting column to be 'b'") } + @MainActor func testRealmIsUpdatedAfterBackgroundUpdate() { let realm = realmWithTestPath() @@ -116,6 +116,7 @@ class SwiftRLMRealmTests: RLMTestCase { XCTAssertEqual(retrievedObject.objectSchema.properties.count, 2, "Only 'name' and 'age' properties should be detected by Realm") } + @MainActor func testUpdatingSortedArrayAfterBackgroundUpdate() { let realm = realmWithTestPath() let objs = SwiftRLMIntObject.allObjects(in: realm) @@ -146,6 +147,7 @@ class SwiftRLMRealmTests: RLMTestCase { token.invalidate() } + @MainActor func testRealmIsUpdatedImmediatelyAfterBackgroundUpdate() { let realm = realmWithTestPath() @@ -203,6 +205,7 @@ class SwiftRLMRealmTests: RLMTestCase { XCTAssertEqual((objects[0] as! StringObject).stringCol!, "b", "Expecting column to be 'b'") } + @MainActor func testRealmIsUpdatedAfterBackgroundUpdate_objc() { let realm = realmWithTestPath() @@ -230,6 +233,7 @@ class SwiftRLMRealmTests: RLMTestCase { XCTAssertEqual((objects[0] as! StringObject).stringCol!, "string", "Value of first column should be 'string'") } + @MainActor func testRealmIsUpdatedImmediatelyAfterBackgroundUpdate_objc() { let realm = realmWithTestPath() diff --git a/Realm/Tests/Swift/SwiftSchemaTests.swift b/Realm/Tests/Swift/SwiftSchemaTests.swift index 9836dcec0c..9d01c1eb5c 100644 --- a/Realm/Tests/Swift/SwiftSchemaTests.swift +++ b/Realm/Tests/Swift/SwiftSchemaTests.swift @@ -150,10 +150,12 @@ class OnlyComputedNoBacklinksProps: FakeObject { } } -@MainActor class RequiresObjcName: RLMObject { +#if compiler(>=5.10) + nonisolated(unsafe) static var enable = false +#else static var enable = false - @MainActor +#endif override class func _realmIgnoreClass() -> Bool { return !enable } diff --git a/RealmSwift/EmbeddedObject.swift b/RealmSwift/EmbeddedObject.swift index e9a845b08c..6cb13f1fbe 100644 --- a/RealmSwift/EmbeddedObject.swift +++ b/RealmSwift/EmbeddedObject.swift @@ -217,6 +217,7 @@ extension EmbeddedObject: _RealmCollectionValueInsideOptional { return _observe(on: queue, block) } +#if compiler(<6) /** Registers a block to be called each time the object changes. @@ -258,7 +259,7 @@ extension EmbeddedObject: _RealmCollectionValueInsideOptional { ) async -> NotificationToken { await with(self, on: actor) { actor, obj in await obj._observe(keyPaths: keyPaths, on: actor, block) - } ?? NotificationToken() + } } /** @@ -302,6 +303,91 @@ extension EmbeddedObject: _RealmCollectionValueInsideOptional { ) async -> NotificationToken { await observe(keyPaths: keyPaths.map(_name(for:)), on: actor, block) } +#else + /** + Registers a block to be called each time the object changes. + + The block will be asynchronously called on the given actor's executor after + each write transaction which deletes the object or modifies any of the managed + properties of the object, including self-assignments that set a property to its + existing value. The block is passed a copy of the object isolated to the + requested actor which can be safely used on that actor along with information + about what changed. + + For write transactions performed on different threads or in different + processes, the block will be called when the managing Realm is + (auto)refreshed to a version including the changes, while for local write + transactions it will be called at some point in the future after the write + transaction is committed. + + Only objects which are managed by a Realm can be observed in this way. You + must retain the returned token for as long as you want updates to be sent + to the block. To stop receiving updates, call `invalidate()` on the token. + + By default, only direct changes to the object's properties will produce + notifications, and not changes to linked objects. Note that this is different + from collection change notifications. If a non-nil, non-empty keypath array is + passed in, only changes to the properties identified by those keypaths will + produce change notifications. The keypaths may traverse link properties to + receive information about changes to linked objects. + + - warning: This method cannot be called during a write transaction, or when + the containing Realm is read-only. + - parameter actor: The actor to isolate notifications to. + - parameter block: The block to call with information about changes to the object. + - returns: A token which must be held for as long as you want updates to be delivered. + */ + @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) + public func observe( + keyPaths: [String]? = nil, on actor: A, _isolation: isolated (any Actor)? = #isolation, + _ block: @Sendable @escaping (isolated A, ObjectChange) -> Void + ) async -> NotificationToken { + await with(self, on: actor) { actor, obj in + await obj._observe(keyPaths: keyPaths, on: actor, block) + } + } + + /** + Registers a block to be called each time the object changes. + + The block will be asynchronously called on the given actor's executor after + each write transaction which deletes the object or modifies any of the managed + properties of the object, including self-assignments that set a property to its + existing value. The block is passed a copy of the object isolated to the + requested actor which can be safely used on that actor along with information + about what changed. + + For write transactions performed on different threads or in different + processes, the block will be called when the managing Realm is + (auto)refreshed to a version including the changes, while for local write + transactions it will be called at some point in the future after the write + transaction is committed. + + Only objects which are managed by a Realm can be observed in this way. You + must retain the returned token for as long as you want updates to be sent + to the block. To stop receiving updates, call `invalidate()` on the token. + + By default, only direct changes to the object's properties will produce + notifications, and not changes to linked objects. Note that this is different + from collection change notifications. If a non-nil, non-empty keypath array is + passed in, only changes to the properties identified by those keypaths will + produce change notifications. The keypaths may traverse link properties to + receive information about changes to linked objects. + + - warning: This method cannot be called during a write transaction, or when + the containing Realm is read-only. + - parameter actor: The actor to isolate notifications to. + - parameter block: The block to call with information about changes to the object. + - returns: A token which must be held for as long as you want updates to be delivered. + */ + @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) + public func observe( + keyPaths: [PartialKeyPath], on actor: A, _isolation: isolated (any Actor)? = #isolation, + _ block: @Sendable @escaping (isolated A, ObjectChange) -> Void + ) async -> NotificationToken { + await observe(keyPaths: keyPaths.map(_name(for:)), on: actor, block) + } +#endif // MARK: Dynamic list diff --git a/RealmSwift/Impl/RealmCollectionImpl.swift b/RealmSwift/Impl/RealmCollectionImpl.swift index 3249e1e38c..43b825447f 100644 --- a/RealmSwift/Impl/RealmCollectionImpl.swift +++ b/RealmSwift/Impl/RealmCollectionImpl.swift @@ -120,6 +120,7 @@ extension RealmCollectionImpl { return collection.addNotificationBlock(wrapped, keyPaths: keyPaths, queue: queue) } +#if compiler(<6) @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) @_unsafeInheritExecutor public func observe( @@ -130,8 +131,22 @@ extension RealmCollectionImpl { collection.observe(keyPaths: keyPaths, on: nil) { change in actor.invokeIsolated(block, change) } - } ?? NotificationToken() + } + } +#else + @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) + public func observe( + keyPaths: [String]?, on actor: A, + _isolation: isolated (any Actor)? = #isolation, + _ block: @Sendable @escaping (isolated A, RealmCollectionChange) -> Void + ) async -> NotificationToken { + await with(self, on: actor) { actor, collection in + collection.observe(keyPaths: keyPaths, on: nil) { change in + actor.invokeIsolated(block, change) + } + } } +#endif public var isFrozen: Bool { return collection.isFrozen @@ -168,30 +183,15 @@ extension Optional: OptionalProtocol { // `with(object, on: actor) { object, actor in ... }` hands the object over // to the given actor and then invokes the callback within the actor. -// This might make sense to expose publicly. +#if compiler(<6) @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) @_unsafeInheritExecutor -internal func with( - _ value: Value, - on actor: A, - _ block: @Sendable @escaping (isolated A, Value) async throws -> Return -) async rethrows -> Return? { +internal func with( + _ value: Value, on actor: A, + _ block: @Sendable @escaping (isolated A, Value) async throws -> NotificationToken +) async rethrows -> NotificationToken { if value.realm == nil { - let unchecked = Unchecked(wrappedValue: value) - return try await actor.invoke { actor in - if !Task.isCancelled { -#if swift(>=5.10) && compiler(<6) - // As of Swift 5.10 the compiler incorrectly thinks that this - // is an async hop even though the isolation context is - // unchanged. This is fixed in 5.11. - nonisolated(unsafe) let value = unchecked.wrappedValue - return try await block(actor, value) -#else - return try await block(actor, unchecked.wrappedValue) -#endif - } - return nil - } + fatalError("Change notifications are only supported for managed objects") } let tsr = ThreadSafeReference(to: value) @@ -205,7 +205,7 @@ internal func with( guard let value = tsr.resolve(in: realm) else { return nil } -#if swift(>=5.10) && compiler(<6) +#if swift(>=5.10) // As above; this is safe but 5.10's sendability checking can't prove it // nonisolated(unsafe) can't be applied to a let in guard so we need // a second variable @@ -216,3 +216,30 @@ internal func with( #endif } } +#else +@available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) +internal func with( + _ value: Value, + on actor: A, + _isolation: isolated (any Actor)? = #isolation, + _ block: @Sendable @escaping (isolated A, Value) async throws -> NotificationToken? +) async rethrows -> NotificationToken { + if value.realm == nil { + fatalError("Change notifications are only supported for managed objects") + } + + let tsr = ThreadSafeReference(to: value) + nonisolated(unsafe) let config = value.realm!.rlmRealm.configurationSharingSchema() + return try await actor.invoke { actor in + if Task.isCancelled { + return nil + } + let scheduler = RLMScheduler.actor(actor, invoke: actor.invoke, verify: actor.verifier()) + let realm = Realm(try! RLMRealm(configuration: config, confinedTo: scheduler)) + guard let value = tsr.resolve(in: realm) else { + return nil + } + return try await block(actor, value) + } ?? NotificationToken() +} +#endif diff --git a/RealmSwift/Map.swift b/RealmSwift/Map.swift index 4f5d48af25..d65e6b3780 100644 --- a/RealmSwift/Map.swift +++ b/RealmSwift/Map.swift @@ -520,6 +520,7 @@ public final class Map: RLMSwiftColle return rlmDictionary.addNotificationBlock(wrapped, keyPaths: keyPaths, queue: queue) } +#if compiler(<6) /** Registers a block to be called each time the map changes. @@ -609,7 +610,7 @@ public final class Map: RLMSwiftColle collection.observe(keyPaths: keyPaths, on: nil) { change in actor.invokeIsolated(block, change) } - } ?? NotificationToken() + } } /** @@ -696,6 +697,185 @@ public final class Map: RLMSwiftColle ) async -> NotificationToken where Value: OptionalProtocol, Value.Wrapped: ObjectBase { await observe(keyPaths: keyPaths.map(_name(for:)), on: actor, block) } +#else + /** + Registers a block to be called each time the map changes. + + The block will be asynchronously called on the actor with the initial map, and + then called again after each write transaction which changes either which keys + are present in the map or the values of any of the objects. + + The `change` parameter that is passed to the block reports, in the form of keys + within the map, which of the key-value pairs were added, removed, or modified + during each write transaction. + + Notifications are delivered to a function isolated to the given actor, on that + actors executor. If the actor is performing blocking work, multiple + notifications may be coalesced into a single notification. This can include the + notification with the initial collection, and changes are only reported for + writes which occur after the initial notification is delivered. + + If no key paths are given, the block will be executed on any insertion, + modification, or deletion for all object properties and the properties of any + nested, linked objects. If a key path or key paths are provided, then the block + will be called for changes which occur only on the provided key paths. For + example, if: + ```swift + class Dog: Object { + @Persisted var name: String + @Persisted var age: Int + @Persisted var toys: List + } + // ... + let dogs = myObject.mapOfDogs + let token = dogs.observe(keyPaths: ["name"], on: actor) { actor, changes in + switch changes { + case .initial(let dogs): + // ... + case .update: + // This case is hit: + // - after the token is initialized + // - when the name property of an object in the collection is modified + // - when an element is inserted or removed from the collection. + // This block is not triggered: + // - when a value other than name is modified on one of the elements. + case .error: + // No longer possible and left for backwards compatibility + } + } + ``` + - If the observed key path were `["toys.brand"]`, then any insertion or + deletion to the `toys` list on any of the collection's elements would trigger + the block. Changes to the `brand` value on any `Toy` that is linked to a `Dog` + in this collection will trigger the block. Changes to a value other than + `brand` on any `Toy` that is linked to a `Dog` in this collection would not + trigger the block. Any insertion or removal to the `Dog` type collection being + observed would also trigger a notification. + - If the above example observed the `["toys"]` key path, then any insertion, + deletion, or modification to the `toys` list for any element in the collection + would trigger the block. Changes to any value on any `Toy` that is linked to a + `Dog` in this collection would *not* trigger the block. Any insertion or + removal to the `Dog` type collection being observed would still trigger a + notification. + + You must retain the returned token for as long as you want updates to be sent + to the block. To stop receiving updates, call `invalidate()` on the token. + + - warning: This method cannot be called during a write transaction, or when + the containing Realm is read-only. + + - parameter keyPaths: Only properties contained in the key paths array will + trigger the block when they are modified. If `nil`, notifications will be + delivered for any property change on the object. String key paths which do not + correspond to a valid a property will throw an exception. See description above + for more detail on linked properties. + - note: The keyPaths parameter refers to object properties of the collection + type and *does not* refer to particular key/value pairs within the Map. + - parameter actor: The actor which notifications should be delivered on. The + block is passed this actor as an isolated parameter, allowing you to access the + actor synchronously from within the callback. + - parameter block: The block to be called whenever a change occurs. + - returns: A token which must be held for as long as you want updates to be delivered. + */ + @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) + public func observe( + keyPaths: [String]? = nil, on actor: A, + _isolation: isolated (any Actor)? = #isolation, + _ block: @Sendable @escaping (isolated A, RealmMapChange) -> Void + ) async -> NotificationToken { + nonisolated(unsafe) let collection = self + return await with(collection, on: actor) { actor, collection in + collection.observe(keyPaths: keyPaths, on: nil) { change in + actor.invokeIsolated(block, change) + } + } + } + + /** + Registers a block to be called each time the map changes. + + The block will be asynchronously called on the actor with the initial map, and + then called again after each write transaction which changes either which keys + are present in the map or the values of any of the objects. + + The `change` parameter that is passed to the block reports, in the form of keys + within the map, which of the key-value pairs were added, removed, or modified + during each write transaction. + + Notifications are delivered to a function isolated to the given actor, on that + actors executor. If the actor is performing blocking work, multiple + notifications may be coalesced into a single notification. This can include the + notification with the initial collection, and changes are only reported for + writes which occur after the initial notification is delivered. + + The block will be called for changes which occur only on the provided key + paths. For example, if: + ```swift + class Dog: Object { + @Persisted var name: String + @Persisted var age: Int + @Persisted var toys: List + } + // ... + let dogs = myObject.mapOfDogs + let token = dogs.observe(keyPaths: [\.name], on: actor) { actor, changes in + switch changes { + case .initial(let dogs): + // ... + case .update: + // This case is hit: + // - after the token is initialized + // - when the name property of an object in the collection is modified + // - when an element is inserted or removed from the collection. + // This block is not triggered: + // - when a value other than name is modified on one of the elements. + case .error: + // No longer possible and left for backwards compatibility + } + } + ``` + - If the observed key path were `[\.toys.brand]`, then any insertion or + deletion to the `toys` list on any of the collection's elements would trigger + the block. Changes to the `brand` value on any `Toy` that is linked to a `Dog` + in this collection will trigger the block. Changes to a value other than + `brand` on any `Toy` that is linked to a `Dog` in this collection would not + trigger the block. Any insertion or removal to the `Dog` type collection being + observed would also trigger a notification. + - If the above example observed the `[\.toys]` key path, then any insertion, + deletion, or modification to the `toys` list for any element in the collection + would trigger the block. Changes to any value on any `Toy` that is linked to a + `Dog` in this collection would *not* trigger the block. Any insertion or + removal to the `Dog` type collection being observed would still trigger a + notification. + + You must retain the returned token for as long as you want updates to be sent + to the block. To stop receiving updates, call `invalidate()` on the token. + + - warning: This method cannot be called during a write transaction, or when + the containing Realm is read-only. + + - parameter keyPaths: Only properties contained in the key paths array will + trigger the block when they are modified. If `nil`, notifications will be + delivered for any property change on the object. String key paths which do not + correspond to a valid a property will throw an exception. See description above + for more detail on linked properties. + - note: The keyPaths parameter refers to object properties of the collection + type and *does not* refer to particular key/value pairs within the Map. + - parameter actor: The actor which notifications should be delivered on. The + block is passed this actor as an isolated parameter, allowing you to access the + actor synchronously from within the callback. + - parameter block: The block to be called whenever a change occurs. + - returns: A token which must be held for as long as you want updates to be delivered. + */ + @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) + public func observe( + keyPaths: [PartialKeyPath], on actor: A, + _isolation: isolated (any Actor)? = #isolation, + _ block: @Sendable @escaping (isolated A, RealmMapChange) -> Void + ) async -> NotificationToken where Value: OptionalProtocol, Value.Wrapped: ObjectBase { + await observe(keyPaths: keyPaths.map(_name(for:)), on: actor, block) + } +#endif // MARK: Frozen Objects diff --git a/RealmSwift/MongoClient.swift b/RealmSwift/MongoClient.swift index 7a2622a416..67447d9c93 100644 --- a/RealmSwift/MongoClient.swift +++ b/RealmSwift/MongoClient.swift @@ -603,12 +603,7 @@ extension MongoCollection { .compactMap(ObjectiveCSupport.convertBson(object:)) } - // These uses of `@_unsafeInheritExecutor` should instead be marking the - // options parameters as `@Copy`. Unfortunately, as of Swift 5.8 that doesn't - // actually work due to https://github.com/apple/swift/issues/61358 - - // NEXT-MAJOR: make the options parameter non-optional and default to .init() - // instead of nil with nil-coalescing internally. +#if compiler(<6) /// Finds the documents in this collection which match the provided filter. /// - Parameters: /// - filter: A `Document` as bson that should match the query. @@ -635,6 +630,48 @@ extension MongoCollection { options: options ?? .init()) .map(ObjectiveCSupport.convert) } +#else + /// Finds the documents in this collection which match the provided filter. + /// - Parameters: + /// - filter: A `Document` as bson that should match the query. + /// - options: `FindOptions` to use when executing the command. + /// - Returns: Array of `Document` filtered. + public func find(filter: Document, options: FindOptions = .init(), + _isolation: isolated (any Actor)? = #isolation) async throws -> [Document] { + try await withCheckedThrowingContinuation { continuation in + __findWhere(ObjectiveCSupport.convert(filter), + options: options) { bson, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: bson!.map(ObjectiveCSupport.convert)) + } + } + } + } + + /// Returns one document from a collection or view which matches the + /// provided filter. If multiple documents satisfy the query, this method + /// returns the first document according to the query's sort order or natural + /// order. + /// - Parameters: + /// - filter: A `Document` as bson that should match the query. + /// - options: `FindOptions` to use when executing the command. + /// - Returns: `Document` filtered. + public func findOneDocument(filter: Document, options: FindOptions = .init(), + _isolation: isolated (any Actor)? = #isolation) async throws -> Document? { + try await withCheckedThrowingContinuation { continuation in + __findOneDocumentWhere(ObjectiveCSupport.convert(filter), + options: options) { bson, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: ObjectiveCSupport.convert(bson!)) + } + } + } + } +#endif /// Runs an aggregation framework pipeline against this collection. /// - Parameters: @@ -699,8 +736,7 @@ extension MongoCollection { upsert: upsert ?? false) } - // NEXT-MAJOR: make the options parameter non-optional and default to .init() - // instead of nil with nil-coalescing internally. +#if compiler(<6) /// Updates a single document in a collection based on a query filter and /// returns the document in either its pre-update or post-update form. Unlike /// `updateOneDocument`, this action allows you to atomically find, update, and @@ -758,6 +794,85 @@ extension MongoCollection { options: options ?? .init()) .map(ObjectiveCSupport.convert) } +#else + /// Updates a single document in a collection based on a query filter and + /// returns the document in either its pre-update or post-update form. Unlike + /// `updateOneDocument`, this action allows you to atomically find, update, and + /// return a document with the same command. This avoids the risk of other + /// update operations changing the document between separate find and update + /// operations. + /// - Parameters: + /// - filter: A bson `Document` representing the match criteria. + /// - update: A bson `Document` representing the update to be applied to a matching document. + /// - options: `RemoteFindOneAndModifyOptions` to use when executing the command. + /// - Returns: `Document` result of the attempt to update a document or `nil` if document wasn't found. + public func findOneAndUpdate(filter: Document, update: Document, + options: FindOneAndModifyOptions = .init(), + _isolation: isolated (any Actor)? = #isolation) async throws -> Document? { + try await withCheckedThrowingContinuation { continuation in + __findOneAndUpdateWhere(ObjectiveCSupport.convert(filter), + updateDocument: ObjectiveCSupport.convert(update), + options: options) { bson, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: ObjectiveCSupport.convert(bson!)) + } + } + } + } + + /// Overwrites a single document in a collection based on a query filter and + /// returns the document in either its pre-replacement or post-replacement + /// form. Unlike `updateOneDocument`, this action allows you to atomically find, + /// replace, and return a document with the same command. This avoids the + /// risk of other update operations changing the document between separate + /// find and update operations. + /// - Parameters: + /// - filter: A `Document` that should match the query. + /// - replacement: A `Document` describing the replacement. + /// - options: `FindOneAndModifyOptions` to use when executing the command. + /// - Returns: `Document`result of the attempt to reaplce a document or `nil` if document wasn't found. + public func findOneAndReplace(filter: Document, replacement: Document, + options: FindOneAndModifyOptions = .init(), + _isolation: isolated (any Actor)? = #isolation) async throws -> Document? { + try await withCheckedThrowingContinuation { continuation in + __findOneAndReplaceWhere(ObjectiveCSupport.convert(filter), + replacementDocument: ObjectiveCSupport.convert(replacement), + options: options) { bson, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: ObjectiveCSupport.convert(bson!)) + } + } + } + } + /// Removes a single document from a collection based on a query filter and + /// returns a document with the same form as the document immediately before + /// it was deleted. Unlike `deleteOneDocument`, this action allows you to atomically + /// find and delete a document with the same command. This avoids the risk of + /// other update operations changing the document between separate find and + /// delete operations. + /// - Parameters: + /// - filter: A `Document` that should match the query. + /// - options: `FindOneAndModifyOptions` to use when executing the command. + /// - Returns: `Document` result of the attempt to delete a document or `nil` if document wasn't found. + public func findOneAndDelete(filter: Document, + options: FindOneAndModifyOptions = .init(), + _isolation: isolated (any Actor)? = #isolation) async throws -> Document? { + try await withCheckedThrowingContinuation { continuation in + __findOneAndDeleteWhere(ObjectiveCSupport.convert(filter), + options: options) { bson, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: ObjectiveCSupport.convert(bson!)) + } + } + } + } +#endif } private class ChangeEventDelegateProxy: RLMChangeEventDelegate { diff --git a/RealmSwift/Object.swift b/RealmSwift/Object.swift index 292407f9f8..bffff74af1 100644 --- a/RealmSwift/Object.swift +++ b/RealmSwift/Object.swift @@ -406,6 +406,7 @@ extension Object: _RealmCollectionValueInsideOptional { _observe(keyPaths: keyPaths.map(_name(for:)), on: queue, block) } +#if compiler(<6) /** Registers a block to be called each time the object changes. @@ -447,7 +448,7 @@ extension Object: _RealmCollectionValueInsideOptional { ) async -> NotificationToken { await with(self, on: actor) { actor, obj in await obj._observe(keyPaths: keyPaths, on: actor, block) - } ?? NotificationToken() + } } /** @@ -491,6 +492,93 @@ extension Object: _RealmCollectionValueInsideOptional { ) async -> NotificationToken { await observe(keyPaths: keyPaths.map(_name(for:)), on: actor, block) } +#else + /** + Registers a block to be called each time the object changes. + + The block will be asynchronously called on the given actor's executor after + each write transaction which deletes the object or modifies any of the managed + properties of the object, including self-assignments that set a property to its + existing value. The block is passed a copy of the object isolated to the + requested actor which can be safely used on that actor along with information + about what changed. + + For write transactions performed on different threads or in different + processes, the block will be called when the managing Realm is + (auto)refreshed to a version including the changes, while for local write + transactions it will be called at some point in the future after the write + transaction is committed. + + Only objects which are managed by a Realm can be observed in this way. You + must retain the returned token for as long as you want updates to be sent + to the block. To stop receiving updates, call `invalidate()` on the token. + + By default, only direct changes to the object's properties will produce + notifications, and not changes to linked objects. Note that this is different + from collection change notifications. If a non-nil, non-empty keypath array is + passed in, only changes to the properties identified by those keypaths will + produce change notifications. The keypaths may traverse link properties to + receive information about changes to linked objects. + + - warning: This method cannot be called during a write transaction, or when + the containing Realm is read-only. + - parameter actor: The actor to isolate notifications to. + - parameter block: The block to call with information about changes to the object. + - returns: A token which must be held for as long as you want updates to be delivered. + */ + @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) + public func observe( + keyPaths: [String]? = nil, on actor: A, + _isolation: isolated (any Actor)? = #isolation, + _ block: @Sendable @escaping (isolated A, ObjectChange) -> Void + ) async -> NotificationToken { + await with(self, on: actor) { actor, obj in + await obj._observe(keyPaths: keyPaths, on: actor, block) + } + } + + /** + Registers a block to be called each time the object changes. + + The block will be asynchronously called on the given actor's executor after + each write transaction which deletes the object or modifies any of the managed + properties of the object, including self-assignments that set a property to its + existing value. The block is passed a copy of the object isolated to the + requested actor which can be safely used on that actor along with information + about what changed. + + For write transactions performed on different threads or in different + processes, the block will be called when the managing Realm is + (auto)refreshed to a version including the changes, while for local write + transactions it will be called at some point in the future after the write + transaction is committed. + + Only objects which are managed by a Realm can be observed in this way. You + must retain the returned token for as long as you want updates to be sent + to the block. To stop receiving updates, call `invalidate()` on the token. + + By default, only direct changes to the object's properties will produce + notifications, and not changes to linked objects. Note that this is different + from collection change notifications. If a non-nil, non-empty keypath array is + passed in, only changes to the properties identified by those keypaths will + produce change notifications. The keypaths may traverse link properties to + receive information about changes to linked objects. + + - warning: This method cannot be called during a write transaction, or when + the containing Realm is read-only. + - parameter actor: The actor to isolate notifications to. + - parameter block: The block to call with information about changes to the object. + - returns: A token which must be held for as long as you want updates to be delivered. + */ + @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) + public func observe( + keyPaths: [PartialKeyPath], on actor: A, + _isolation: isolated (any Actor)? = #isolation, + _ block: @Sendable @escaping (isolated A, ObjectChange) -> Void + ) async -> NotificationToken { + await observe(keyPaths: keyPaths.map(_name(for:)), on: actor, block) + } +#endif // MARK: Dynamic list diff --git a/RealmSwift/Projection.swift b/RealmSwift/Projection.swift index 73e3b42f82..f3e514ab78 100644 --- a/RealmSwift/Projection.swift +++ b/RealmSwift/Projection.swift @@ -458,6 +458,7 @@ extension ProjectionObservable { observe(keyPaths: map(keyPaths: keyPaths), on: queue, block) } +#if compiler(<6) /** Registers a block to be called each time the projection's underlying object changes. @@ -538,7 +539,7 @@ extension ProjectionObservable { obj.observe(keyPaths: keyPaths, on: nil) { (change: ObjectChange) in actor.invokeIsolated(block, change) } - } ?? NotificationToken() + } } /** @@ -638,6 +639,188 @@ extension ProjectionObservable { return names.componentsJoined(by: ".") } } +#else + /** + Registers a block to be called each time the projection's underlying object changes. + + The block will be asynchronously called on the actor after each write transaction which + deletes the underlying object or modifies any of the projected properties of the object, + including self-assignments that set a property to its existing value. + + For write transactions performed on different threads or in different + processes, the block will be called when the managing Realm is + (auto)refreshed to a version including the changes, while for local write + transactions it will be called at some point in the future after the write + transaction is committed. + + If no key paths are given, the block will be executed on any insertion, + modification, or deletion for all projected properties, including projected properties of + any nested, linked objects. If a key path or key paths are provided, + then the block will be called for changes which occur only on the + provided key paths. For example, if: + + ```swift + class Person: Object { + @Persisted var firstName: String + @Persisted var lastName = "" + @Persisted public var friends: List + } + + class PersonProjection: Projection { + @Projected(\Person.firstName) var name + @Projected(\Person.lastName.localizedUppercase) var lastNameCaps + @Projected(\Person.friends.projectTo.firstName) var firstFriendsName: ProjectedCollection + } + + let token = projectedPerson.observe(keyPaths: ["name"], { changes in + // ... + }) + ``` + - The above notification block fires for changes to the + `Person.firstName` property of the the projection's underlying `Person` Object, + but not for any changes made to `Person.lastName` or `Person.friends` list. + - The notification block fires for changes of `PersonProjection.name` property, but not for + another projection's property change. + - If the observed key path were `["firstFriendsName"]`, then any insertion, + deletion, or modification of the `firstName` of the `friends` list will trigger the block. A change to + `someFriend.lastName` would not trigger the block (where `someFriend` + is an element contained in `friends`) + + Notifications are delivered to a function isolated to the given actor, on that + actors executor. If the actor is performing blocking work, multiple + notifications may be coalesced into a single notification. + + Unlike with Collection notifications, there is no "Initial" notification + and there is no gap between when this function returns and when changes + will first be captured. + + You must retain the returned token for as long as you want updates to be sent + to the block. To stop receiving updates, call `invalidate()` on the token. + + - warning: This method cannot be called during a write transaction, or when + the containing Realm is read-only. + - parameter keyPaths: Only properties contained in the key paths array will trigger + the block when they are modified. If `nil`, notifications + will be delivered for any projected property change on the object. + String key paths which do not correspond to a valid projected property + will throw an exception. + - parameter actor: The actor which notifications should be delivered on. The + block is passed this actor as an isolated parameter, + allowing you to access the actor synchronously from within the callback. + - parameter block: The block to call with information about changes to the object. + - returns: A token which must be held for as long as you want updates to be delivered. + */ + @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) + public func observe( + keyPaths: [String]? = nil, on actor: A, + _isolation: isolated (any Actor)? = #isolation, + _ block: @Sendable @escaping (isolated A, ObjectChange) -> Void + ) async -> NotificationToken { + await with(self, on: actor) { actor, obj in + obj.observe(keyPaths: keyPaths, on: nil) { (change: ObjectChange) in + actor.invokeIsolated(block, change) + } + } + } + + /** + Registers a block to be called each time the projection's underlying object changes. + + The block will be asynchronously called on the actor after each write transaction which + deletes the underlying object or modifies any of the projected properties of the object, + including self-assignments that set a property to its existing value. + + For write transactions performed on different threads or in different + processes, the block will be called when the managing Realm is + (auto)refreshed to a version including the changes, while for local write + transactions it will be called at some point in the future after the write + transaction is committed. + + If no key paths are given, the block will be executed on any insertion, + modification, or deletion for all projected properties, including projected properties of + any nested, linked objects. If a key path or key paths are provided, + then the block will be called for changes which occur only on the + provided key paths. For example, if: + + ```swift + class Person: Object { + @Persisted var firstName: String + @Persisted var lastName = "" + @Persisted public var friends: List + } + + class PersonProjection: Projection { + @Projected(\Person.firstName) var name + @Projected(\Person.lastName.localizedUppercase) var lastNameCaps + @Projected(\Person.friends.projectTo.firstName) var firstFriendsName: ProjectedCollection + } + + let token = projectedPerson.observe(keyPaths: [\PersonProjection.name], { changes in + // ... + }) + ``` + - The above notification block fires for changes to the + `Person.firstName` property of the the projection's underlying `Person` Object, + but not for any changes made to `Person.lastName` or `Person.friends` list. + - The notification block fires for changes of `PersonProjection.name` property, but not for + another projection's property change. + - If the observed key path were `[\.firstFriendsName]`, then any insertion, + deletion, or modification of the `firstName` of the `friends` list will trigger the block. A change to + `someFriend.lastName` would not trigger the block (where `someFriend` + is an element contained in `friends`) + + Notifications are delivered to a function isolated to the given actor, on that + actors executor. If the actor is performing blocking work, multiple + notifications may be coalesced into a single notification. + + Unlike with Collection notifications, there is no "Initial" notification + and there is no gap between when this function returns and when changes + will first be captured. + + You must retain the returned token for as long as you want updates to be sent + to the block. To stop receiving updates, call `invalidate()` on the token. + + - warning: This method cannot be called during a write transaction, or when + the containing Realm is read-only. + - parameter keyPaths: Only properties contained in the key paths array will trigger + the block when they are modified. If `nil`, notifications + will be delivered for any projected property change on the object. + String key paths which do not correspond to a valid projected property + will throw an exception. + - parameter actor: The actor which notifications should be delivered on. The + block is passed this actor as an isolated parameter, + allowing you to access the actor synchronously from within the callback. + - parameter block: The block to call with information about changes to the object. + - returns: A token which must be held for as long as you want updates to be delivered. + */ + @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) + public func observe( + keyPaths: [PartialKeyPath], on actor: A, + _isolation: isolated (any Actor)? = #isolation, + _ block: @Sendable @escaping (isolated A, ObjectChange) -> Void + ) async -> NotificationToken { + await observe(keyPaths: map(keyPaths: keyPaths), on: actor, block) + } + + fileprivate var schema: [ProjectionProperty] { + projectionSchemaCache.schema(for: self) + } + + private func map(keyPaths: [PartialKeyPath]) -> [String]? { + if keyPaths.isEmpty { + return nil + } + + let names = NSMutableArray() + let root = Root.keyPathRecorder(with: names) + let projection = Self(projecting: root) + return keyPaths.map { + names.removeAllObjects() + _ = projection[keyPath: $0] + return names.componentsJoined(by: ".") + } + } +#endif } /** Information about a specific property which changed in an `Object` change notification. diff --git a/RealmSwift/Realm.swift b/RealmSwift/Realm.swift index 5a5bed9b18..c41a038acd 100644 --- a/RealmSwift/Realm.swift +++ b/RealmSwift/Realm.swift @@ -1270,6 +1270,38 @@ extension Realm { self = Realm(rlmRealm.wrappedValue) } +#if compiler(>=6) + /** + Asynchronously obtains a `Realm` instance isolated to the current Actor. + + Opening a Realm with an actor isolates the Realm to that actor. Rather + than being confined to the specific thread which the Realm was opened on, + the Realm can instead only be used from within that actor or functions + isolated to that actor. Isolating a Realm to an actor also enables using + ``asyncWrite`` and ``asyncRefresh``. + + All initialization work to prepare the Realm for work, such as creating, + migrating, or compacting the file on disk, and waiting for synchronized + Realms to download the latest data from the server is done on a background + thread and does not block the calling executor. + + - parameter configuration: A configuration object to use when opening the Realm. + - parameter downloadBeforeOpen: When opening the Realm should first download + all data from the server. + - throws: An `NSError` if the Realm could not be initialized. + `CancellationError` if the task is cancelled. + - returns: An open Realm. + */ + public static func open(configuration: Realm.Configuration = .defaultConfiguration, + _isolation actor: isolated any Actor = #isolation, + downloadBeforeOpen: OpenBehavior = .never) async throws -> Realm { + let scheduler = RLMScheduler.actor(actor, invoke: actor.invoke, verify: actor.verifier()) + let rlmRealm = try await openRealm(configuration: configuration, scheduler: scheduler, + actor: actor, downloadBeforeOpen: downloadBeforeOpen) + return Realm(rlmRealm.wrappedValue) + } +#endif + #if compiler(<6) /** Performs actions contained within the given block inside a write transaction. @@ -1458,7 +1490,9 @@ extension Realm { if realm.inWriteTransaction { let error = await withCheckedContinuation { continuation in - realm.commitAsyncWrite(withGrouping: false, completion: continuation.resume) + realm.commitAsyncWrite(withGrouping: false) { error in + continuation.resume(returning: error) + } } if let error { throw error diff --git a/RealmSwift/RealmCollection.swift b/RealmSwift/RealmCollection.swift index aec497a8b4..10a081994f 100644 --- a/RealmSwift/RealmCollection.swift +++ b/RealmSwift/RealmCollection.swift @@ -524,6 +524,7 @@ public protocol RealmCollection: RealmCollectionBase, Equatable where Iterator = on queue: DispatchQueue?, _ block: @escaping (RealmCollectionChange) -> Void) -> NotificationToken +#if compiler(<6) /** Registers a block to be called each time the collection changes. @@ -621,6 +622,105 @@ public protocol RealmCollection: RealmCollectionBase, Equatable where Iterator = func observe(keyPaths: [String]?, on actor: A, _ block: @Sendable @escaping (isolated A, RealmCollectionChange) -> Void) async -> NotificationToken +#else + /** + Registers a block to be called each time the collection changes. + + The block will be asynchronously called with an initial version of the + collection, and then called again after each write transaction which changes + either any of the objects in the collection, or which objects are in the + collection. + + The `actor` parameter passed to the block is the actor which you pass to this + function. This parameter is required to isolate the callback to the actor. + + The `change` parameter that is passed to the block reports, in the form of + indices within the collection, which of the objects were added, removed, or + modified after the previous notification. The `collection` field in the change + enum will be isolated to the requested actor, and is safe to use within that + actor only. See the ``RealmCollectionChange`` documentation for more + information on the change information supplied and an example of how to use it + to update a `UITableView`. + + Once the initial notification is delivered, the collection will be fully + evaluated and up-to-date, and accessing it will never perform any blocking + work. This guarantee holds only as long as you do not perform a write + transaction on the same actor as notifications are being delivered to. If you + do, accessing the collection before the next notification is delivered may need + to rerun the query. + + Notifications are delivered to the given actor's executor. When notifications + can't be delivered instantly, multiple notifications may be coalesced into a + single notification. This can include the notification with the initial + collection: any writes which occur before the initial notification is delivered + may not produce change notifications. + + Adding, removing or assigning objects in the collection always produces a + notification. By default, modifying the objects which a collection links to + (and the objects which those objects link to, if applicable) will also report + that index in the collection as being modified. If a non-empty array of + keypaths is provided, then only modifications to those keypaths will mark the + object as modified. For example: + + ```swift + class Dog: Object { + @Persisted var name: String + @Persisted var age: Int + @Persisted var toys: List + } + + let dogs = realm.objects(Dog.self) + let token = await dogs.observe(keyPaths: ["name"], on: myActor) { actor, changes in + switch changes { + case .initial(let dogs): + // Query has finished running and dogs can not be used without blocking + case .update: + // This case is hit: + // - after the token is initialized + // - when the name property of an object in the collection is modified + // - when an element is inserted or removed from the collection. + // This block is not triggered: + // - when a value other than name is modified on one of the elements. + case .error: + // Can no longer happen but is left for backwards compatiblity + } + } + ``` + - If the observed key path were `["toys.brand"]`, then any insertion or + deletion to the `toys` list on any of the collection's elements would trigger + the block. Changes to the `brand` value on any `Toy` that is linked to a `Dog` + in this collection will trigger the block. Changes to a value other than + `brand` on any `Toy` that is linked to a `Dog` in this collection would not + trigger the block. Any insertion or removal to the `Dog` type collection being + observed would also trigger a notification. + - If the above example observed the `["toys"]` key path, then any insertion, + deletion, or modification to the `toys` list for any element in the collection + would trigger the block. Changes to any value on any `Toy` that is linked to a + `Dog` in this collection would *not* trigger the block. Any insertion or + removal to the `Dog` type collection being observed would still trigger a + notification. + + You must retain the returned token for as long as you want updates to be sent + to the block. To stop receiving updates, call `invalidate()` on the token. + + - warning: This method cannot be called during a write transaction, or when the containing Realm is read-only. + + - parameter keyPaths: Only properties contained in the key paths array will trigger + the block when they are modified. If `nil` or empty, notifications + will be delivered for any property change on the object. + String key paths which do not correspond to a valid a property + will throw an exception. See description above for + more detail on linked properties. + - parameter actor: The actor to isolate the notifications to. + - parameter block: The block to be called whenever a change occurs. + - returns: A token which must be held for as long as you want updates to be delivered. + */ + @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) + func observe(keyPaths: [String]?, + on actor: A, + _isolation: (any Actor)?, + _ block: @Sendable @escaping (isolated A, RealmCollectionChange) -> Void) async -> NotificationToken +#endif // MARK: Frozen Objects @@ -1127,6 +1227,7 @@ public extension RealmCollection { return self.observe(keyPaths: keyPaths, on: queue, block) } +#if compiler(<6) /** Registers a block to be called each time the collection changes. @@ -1226,6 +1327,107 @@ public extension RealmCollection { _ block: @Sendable @escaping (isolated A, RealmCollectionChange) -> Void) async -> NotificationToken { await self.observe(keyPaths: keyPaths, on: actor, block) } +#else + /** + Registers a block to be called each time the collection changes. + + The block will be asynchronously called with an initial version of the + collection, and then called again after each write transaction which changes + either any of the objects in the collection, or which objects are in the + collection. + + The `actor` parameter passed to the block is the actor which you pass to this + function. This parameter is required to isolate the callback to the actor. + + The `change` parameter that is passed to the block reports, in the form of + indices within the collection, which of the objects were added, removed, or + modified after the previous notification. The `collection` field in the change + enum will be isolated to the requested actor, and is safe to use within that + actor only. See the ``RealmCollectionChange`` documentation for more + information on the change information supplied and an example of how to use it + to update a `UITableView`. + + Once the initial notification is delivered, the collection will be fully + evaluated and up-to-date, and accessing it will never perform any blocking + work. This guarantee holds only as long as you do not perform a write + transaction on the same actor as notifications are being delivered to. If you + do, accessing the collection before the next notification is delivered may need + to rerun the query. + + Notifications are delivered to the given actor's executor. When notifications + can't be delivered instantly, multiple notifications may be coalesced into a + single notification. This can include the notification with the initial + collection: any writes which occur before the initial notification is delivered + may not produce change notifications. + + Adding, removing or assigning objects in the collection always produces a + notification. By default, modifying the objects which a collection links to + (and the objects which those objects link to, if applicable) will also report + that index in the collection as being modified. If a non-empty array of + keypaths is provided, then only modifications to those keypaths will mark the + object as modified. For example: + + ```swift + class Dog: Object { + @Persisted var name: String + @Persisted var age: Int + @Persisted var toys: List + } + + let dogs = realm.objects(Dog.self) + let token = await dogs.observe(keyPaths: ["name"], on: myActor) { actor, changes in + switch changes { + case .initial(let dogs): + // Query has finished running and dogs can not be used without blocking + case .update: + // This case is hit: + // - after the token is initialized + // - when the name property of an object in the collection is modified + // - when an element is inserted or removed from the collection. + // This block is not triggered: + // - when a value other than name is modified on one of the elements. + case .error: + // Can no longer happen but is left for backwards compatiblity + } + } + ``` + - If the observed key path were `["toys.brand"]`, then any insertion or + deletion to the `toys` list on any of the collection's elements would trigger + the block. Changes to the `brand` value on any `Toy` that is linked to a `Dog` + in this collection will trigger the block. Changes to a value other than + `brand` on any `Toy` that is linked to a `Dog` in this collection would not + trigger the block. Any insertion or removal to the `Dog` type collection being + observed would also trigger a notification. + - If the above example observed the `["toys"]` key path, then any insertion, + deletion, or modification to the `toys` list for any element in the collection + would trigger the block. Changes to any value on any `Toy` that is linked to a + `Dog` in this collection would *not* trigger the block. Any insertion or + removal to the `Dog` type collection being observed would still trigger a + notification. + + You must retain the returned token for as long as you want updates to be sent + to the block. To stop receiving updates, call `invalidate()` on the token. + + - warning: This method cannot be called during a write transaction, or when the containing Realm is read-only. + + - parameter keyPaths: Only properties contained in the key paths array will trigger + the block when they are modified. If `nil` or empty, notifications + will be delivered for any property change on the object. + String key paths which do not correspond to a valid a property + will throw an exception. See description above for + more detail on linked properties. + - parameter actor: The actor to isolate the notifications to. + - parameter block: The block to be called whenever a change occurs. + - returns: A token which must be held for as long as you want updates to be delivered. + */ + @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) + func observe(keyPaths: [String]? = nil, + on actor: A, + _isolation: isolated (any Actor)? = #isolation, + _ block: @Sendable @escaping (isolated A, RealmCollectionChange) -> Void) async -> NotificationToken { + await self.observe(keyPaths: keyPaths, on: actor, block) + } +#endif } public extension RealmCollection where Element: ObjectBase { @@ -1349,6 +1551,7 @@ public extension RealmCollection where Element: ObjectBase { return self.observe(keyPaths: keyPaths.map(_name(for:)), on: queue, block) } +#if compiler(<6) /** Registers a block to be called each time the collection changes. @@ -1447,6 +1650,106 @@ public extension RealmCollection where Element: ObjectBase { _ block: @Sendable @escaping (isolated A, RealmCollectionChange) -> Void) async -> NotificationToken { await observe(keyPaths: keyPaths.map(_name(for:)), on: actor, block) } +#else + /** + Registers a block to be called each time the collection changes. + + The block will be asynchronously called with an initial version of the + collection, and then called again after each write transaction which changes + either any of the objects in the collection, or which objects are in the + collection. + + The `actor` parameter passed to the block is the actor which you pass to this + function. This parameter is required to isolate the callback to the actor. + + The `change` parameter that is passed to the block reports, in the form of + indices within the collection, which of the objects were added, removed, or + modified after the previous notification. The `collection` field in the change + enum will be isolated to the requested actor, and is safe to use within that + actor only. See the ``RealmCollectionChange`` documentation for more + information on the change information supplied and an example of how to use it + to update a `UITableView`. + + Once the initial notification is delivered, the collection will be fully + evaluated and up-to-date, and accessing it will never perform any blocking + work. This guarantee holds only as long as you do not perform a write + transaction on the same actor as notifications are being delivered to. If you + do, accessing the collection before the next notification is delivered may need + to rerun the query. + + Notifications are delivered to the given actor's executor. When notifications + can't be delivered instantly, multiple notifications may be coalesced into a + single notification. This can include the notification with the initial + collection: any writes which occur before the initial notification is delivered + may not produce change notifications. + + Adding, removing or assigning objects in the collection always produces a + notification. By default, modifying the objects which a collection links to + (and the objects which those objects link to, if applicable) will also report + that index in the collection as being modified. If a non-empty array of + keypaths is provided, then only modifications to those keypaths will mark the + object as modified. For example: + + ```swift + class Dog: Object { + @Persisted var name: String + @Persisted var age: Int + @Persisted var toys: List + } + + let dogs = realm.objects(Dog.self) + let token = await dogs.observe(keyPaths: [\.name], on: myActor) { actor, changes in + switch changes { + case .initial(let dogs): + // Query has finished running and dogs can not be used without blocking + case .update: + // This case is hit: + // - after the token is initialized + // - when the name property of an object in the collection is modified + // - when an element is inserted or removed from the collection. + // This block is not triggered: + // - when a value other than name is modified on one of the elements. + case .error: + // Can no longer happen but is left for backwards compatiblity + } + } + ``` + - If the observed key path were `[\.toys.brand]`, then any insertion or + deletion to the `toys` list on any of the collection's elements would trigger + the block. Changes to the `brand` value on any `Toy` that is linked to a `Dog` + in this collection will trigger the block. Changes to a value other than + `brand` on any `Toy` that is linked to a `Dog` in this collection would not + trigger the block. Any insertion or removal to the `Dog` type collection being + observed would also trigger a notification. + - If the above example observed the `[\.toys]` key path, then any insertion, + deletion, or modification to the `toys` list for any element in the collection + would trigger the block. Changes to any value on any `Toy` that is linked to a + `Dog` in this collection would *not* trigger the block. Any insertion or + removal to the `Dog` type collection being observed would still trigger a + notification. + + You must retain the returned token for as long as you want updates to be sent + to the block. To stop receiving updates, call `invalidate()` on the token. + + - warning: This method cannot be called during a write transaction, or when the containing Realm is read-only. + + - parameter keyPaths: Only properties contained in the key paths array will trigger + the block when they are modified. If empty, notifications + will be delivered for any property change on the object. + String key paths which do not correspond to a valid a property + will throw an exception. See description above for + more detail on linked properties. + - parameter actor: The actor to isolate the notifications to. + - parameter block: The block to be called whenever a change occurs. + - returns: A token which must be held for as long as you want updates to be delivered. + */ + @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) + func observe(keyPaths: [PartialKeyPath], on actor: A, + _isolation: isolated (any Actor)? = #isolation, + _ block: @Sendable @escaping (isolated A, RealmCollectionChange) -> Void) async -> NotificationToken { + await observe(keyPaths: keyPaths.map(_name(for:)), on: actor, block) + } +#endif } extension RealmCollection { diff --git a/RealmSwift/Results.swift b/RealmSwift/Results.swift index 6b0d74afa8..68ad91c000 100644 --- a/RealmSwift/Results.swift +++ b/RealmSwift/Results.swift @@ -149,6 +149,7 @@ extension Projection: KeypathSortable {} // MARK: Flexible Sync +#if compiler(<6) /** Creates a SyncSubscription matching the Results' local query. After committing the subscription to the realm's local subscription set, the method @@ -194,6 +195,65 @@ extension Projection: KeypathSortable {} rlmResults = try await rlmResults.subscribe(withName: name, waitForSync: waitForSync, confinedTo: scheduler, timeout: timeout ?? 0) return self } +#else + /** + Creates a SyncSubscription matching the Results' local query. + After committing the subscription to the realm's local subscription set, the method + will wait for downloads according to `WaitForSyncMode`. + + ### Unnamed subscriptions ### + If `.subscribe()` is called without a name whose query matches an unnamed subscription, another subscription is not created. + + If `.subscribe()` is called without a name whose query matches a named subscription, an additional unnamed subscription is created. + ### Named Subscriptions ### + If `.subscribe()` is called with a name whose query matches an unnamed subscription, an additional named subscription is created. + ### Existing name and query ### + If `.subscribe()` is called with a name whose name is taken on a different query, the old subscription is updated with the new query. + + If `.subscribe()` is called with a name that's in already in use by an identical query, no new subscription is created. + + + - Note: This method will wait for all data to be downloaded before returning when `WaitForSyncMode.always` and `.onCreation` (when the subscription is first created) is used. This requires an internet connection if no timeout is set. + + - Note: This method opens a update transaction that creates or updates a subscription. + It's advised to *not* loop over this method in order to create multiple subscriptions. + This could create a performance bottleneck by opening multiple unnecessary update transactions. + To create multiple subscriptions at once use `SyncSubscription.update`. + + - parameter name: The name applied to the subscription + - parameter waitForSync: ``WaitForSyncMode`` Determines the download behavior for the subscription. Defaults to `.onCreation`. + - parameter timeout: An optional client timeout. The client will cancel waiting for subscription downloads after this time has elapsed. Reaching this timeout doesn't imply a server error. + - returns: Returns `self`. + + - warning: This function is only supported for main thread and + actor-isolated Realms. + */ + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + public func subscribe( + name: String? = nil, + waitForSync: WaitForSyncMode = .onCreation, + timeout: TimeInterval? = nil, + _isolation: isolated any Actor = #isolation + ) async throws -> Results { + guard let actor = realm?.rlmRealm.actor as? Actor else { + fatalError("`subscribe` can only be called on main thread or actor-isolated Realms") + } + + let rlmResults = ObjectiveCSupport.convert(object: self) + let scheduler = await RLMScheduler.actor(actor, invoke: actor.invoke, verify: actor.verifier()) + _ = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + rlmResults.subscribe(withName: name, waitForSync: waitForSync, confinedTo: scheduler, timeout: timeout ?? 0) { _, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } + return self + } +#endif + /** Removes a SyncSubscription matching the Results' local filter. diff --git a/RealmSwift/SectionedResults.swift b/RealmSwift/SectionedResults.swift index 04cd203393..a6d580b150 100644 --- a/RealmSwift/SectionedResults.swift +++ b/RealmSwift/SectionedResults.swift @@ -191,6 +191,7 @@ public protocol RealmSectionedResult: RandomAccessCollection, Equatable, ThreadC _ block: @escaping (SectionedResultsChange) -> Void) -> NotificationToken } +#if compiler(<6) public extension RealmSectionedResult { func observe(keyPaths: [String]? = nil, on queue: DispatchQueue? = nil, @@ -208,10 +209,33 @@ public extension RealmSectionedResult { collection.observe(keyPaths: keyPaths, on: nil) { change in actor.invokeIsolated(block, change) } - } ?? NotificationToken() + } + } +} +#else +public extension RealmSectionedResult { + func observe(keyPaths: [String]? = nil, + on queue: DispatchQueue? = nil, + _ block: @escaping (SectionedResultsChange) -> Void) -> NotificationToken { + observe(keyPaths: keyPaths, on: queue, block) + } + + @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) + func observe( + keyPaths: [String]? = nil, on actor: A, + _isolation: isolated (any Actor)? = #isolation, + _ block: @Sendable @escaping (isolated A, SectionedResultsChange) -> Void + ) async -> NotificationToken { + await with(self, on: actor) { actor, collection in + collection.observe(keyPaths: keyPaths, on: nil) { change in + actor.invokeIsolated(block, change) + } + } } } +#endif +#if compiler(<6) public extension RealmSectionedResult where Element: RealmSectionedResult, Element.Element: ObjectBase { /** Registers a block to be called each time the sectioned results collection changes. @@ -481,6 +505,277 @@ public extension RealmSectionedResult where Element: ObjectBase { await observe(keyPaths: keyPaths.map(_name(for:)), on: actor, block) } } +#else +public extension RealmSectionedResult where Element: RealmSectionedResult, Element.Element: ObjectBase { + /** + Registers a block to be called each time the sectioned results collection changes. + + The block will be asynchronously called with the initial sectioned results collection, and then called again after each write + transaction which changes either any of the objects in the sectioned results collection, or which objects are in the sectioned results collection. + + The `change` parameter that is passed to the block reports, in the form of indices within the collection, which of + the objects were added, removed, or modified during each write transaction. See the `SectionedResultsChange` + documentation for more information on the change information supplied and an example of how to use it to update a + `UITableView`. + + At the time when the block is called, the collection will be fully evaluated and up-to-date, and as long as you do + not perform a write transaction on the same thread or explicitly call `realm.refresh()`, accessing it will never + perform blocking work. + + If no queue is given, notifications are delivered via the standard run loop, and so can't be delivered while the + run loop is blocked by other activity. If a queue is given, notifications are delivered to that queue instead. When + notifications can't be delivered instantly, multiple notifications may be coalesced into a single notification. + This can include the notification with the initial sectioned results collection. + + For example, the following code performs a write transaction immediately after adding the notification block, so + there is no opportunity for the initial notification to be delivered first. As a result, the initial notification + will reflect the state of the Realm after the write transaction. + + ```swift + let dogs = realm.objects(Dog.self) + let sectionedResults = dogs.sectioned(by: \.age, ascending: true) + print("sectionedResults.count: \(sectionedResults?.count)") // => 0 + let token = sectionedResults.observe { changes in + switch changes { + case .initial(let sectionedResults): + // Will print "sectionedResults.count: 1" + print("sectionedResults.count: \(sectionedResults.count)") + break + case .update: + // Will not be hit in this example + break + case .error: + break + } + } + try! realm.write { + let dog = Dog() + dog.name = "Rex" + person.dogs.append(dog) + } + // end of run loop execution context + ``` + + If no key paths are given, the block will be executed on any insertion, + modification, or deletion for all object properties and the properties of + any nested, linked objects. If a key path or key paths are provided, + then the block will be called for changes which occur only on the + provided key paths. For example, if: + ```swift + class Dog: Object { + @Persisted var name: String + @Persisted var age: Int + @Persisted var toys: List + } + // ... + let dogs = realm.objects(Dog.self) + let sectionedResults = dogs.sectioned(by: \.age, ascending: true) + let token = sectionedResults.observe(keyPaths: ["name"]) { changes in + switch changes { + case .initial(let sectionedResults): + // ... + case .update: + // This case is hit: + // - after the token is initialized + // - when the name property of an object in the + // collection is modified + // - when an element is inserted or removed + // from the collection. + // This block is not triggered: + // - when a value other than name is modified on + // one of the elements. + case .error: + // ... + } + } + // end of run loop execution context + ``` + - If the observed key path were `["toys.brand"]`, then any insertion or + deletion to the `toys` list on any of the collection's elements would trigger the block. + Changes to the `brand` value on any `Toy` that is linked to a `Dog` in this + collection will trigger the block. Changes to a value other than `brand` on any `Toy` that + is linked to a `Dog` in this collection would not trigger the block. + Any insertion or removal to the `Dog` type collection being observed + would also trigger a notification. + - If the above example observed the `["toys"]` key path, then any insertion, + deletion, or modification to the `toys` list for any element in the collection + would trigger the block. + Changes to any value on any `Toy` that is linked to a `Dog` in this collection + would *not* trigger the block. + Any insertion or removal to the `Dog` type collection being observed + would still trigger a notification. + - Any modification to the section key path property which results in the object changing + position in the section, or changing section entirely will trigger a notification. + + - note: Multiple notification tokens on the same object which filter for + separate key paths *do not* filter exclusively. If one key path + change is satisfied for one notification token, then all notification + token blocks for that object will execute. + + You must retain the returned token for as long as you want updates to be sent to the block. To stop receiving + updates, call `invalidate()` on the token. + + - warning: This method cannot be called during a write transaction, or when the containing Realm is read-only. + + - parameter keyPaths: Only properties contained in the key paths array will trigger + the block when they are modified. If `nil`, notifications + will be delivered for any property change on the object. + - parameter queue: The serial dispatch queue to receive notification on. If + `nil`, notifications are delivered to the current thread. + - parameter block: The block to be called whenever a change occurs. + - returns: A token which must be held for as long as you want updates to be delivered. + */ + func observe(keyPaths: [PartialKeyPath], + on queue: DispatchQueue? = nil, + _ block: @escaping (SectionedResultsChange) -> Void) -> NotificationToken { + observe(keyPaths: keyPaths.map(_name(for:)), on: queue, block) + } + + @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) + func observe( + keyPaths: [PartialKeyPath], on actor: A, + _isolation: isolated (any Actor)? = #isolation, + _ block: @Sendable @escaping (isolated A, SectionedResultsChange) -> Void + ) async -> NotificationToken { + await observe(keyPaths: keyPaths.map(_name(for:)), on: actor, block) + } +} + +public extension RealmSectionedResult where Element: ObjectBase { + /** + Registers a block to be called each time the sectioned results collection changes. + + The block will be asynchronously called with the initial sectioned results collection, and then called again after each write + transaction which changes either any of the objects in the sectioned results collection, or which objects are in the sectioned results collection. + + The `change` parameter that is passed to the block reports, in the form of indices within the collection, which of + the objects were added, removed, or modified during each write transaction. See the `SectionedResultsChange` + documentation for more information on the change information supplied and an example of how to use it to update a + `UITableView`. + + At the time when the block is called, the collection will be fully evaluated and up-to-date, and as long as you do + not perform a write transaction on the same thread or explicitly call `realm.refresh()`, accessing it will never + perform blocking work. + + If no queue is given, notifications are delivered via the standard run loop, and so can't be delivered while the + run loop is blocked by other activity. If a queue is given, notifications are delivered to that queue instead. When + notifications can't be delivered instantly, multiple notifications may be coalesced into a single notification. + This can include the notification with the initial sectioned results collection. + + For example, the following code performs a write transaction immediately after adding the notification block, so + there is no opportunity for the initial notification to be delivered first. As a result, the initial notification + will reflect the state of the Realm after the write transaction. + + ```swift + let dogs = realm.objects(Dog.self) + let sectionedResults = dogs.sectioned(by: \.age, ascending: true) + print("sectionedResults.count: \(sectionedResults?.count)") // => 0 + let token = sectionedResults.observe { changes in + switch changes { + case .initial(let sectionedResults): + // Will print "sectionedResults.count: 1" + print("sectionedResults.count: \(sectionedResults.count)") + break + case .update: + // Will not be hit in this example + break + case .error: + break + } + } + try! realm.write { + let dog = Dog() + dog.name = "Rex" + person.dogs.append(dog) + } + // end of run loop execution context + ``` + + If no key paths are given, the block will be executed on any insertion, + modification, or deletion for all object properties and the properties of + any nested, linked objects. If a key path or key paths are provided, + then the block will be called for changes which occur only on the + provided key paths. For example, if: + ```swift + class Dog: Object { + @Persisted var name: String + @Persisted var age: Int + @Persisted var toys: List + } + // ... + let dogs = realm.objects(Dog.self) + let sectionedResults = dogs.sectioned(by: \.age, ascending: true) + let token = sectionedResults.observe(keyPaths: ["name"]) { changes in + switch changes { + case .initial(let sectionedResults): + // ... + case .update: + // This case is hit: + // - after the token is initialized + // - when the name property of an object in the + // collection is modified + // - when an element is inserted or removed + // from the collection. + // This block is not triggered: + // - when a value other than name is modified on + // one of the elements. + case .error: + // ... + } + } + // end of run loop execution context + ``` + - If the observed key path were `["toys.brand"]`, then any insertion or + deletion to the `toys` list on any of the collection's elements would trigger the block. + Changes to the `brand` value on any `Toy` that is linked to a `Dog` in this + collection will trigger the block. Changes to a value other than `brand` on any `Toy` that + is linked to a `Dog` in this collection would not trigger the block. + Any insertion or removal to the `Dog` type collection being observed + would also trigger a notification. + - If the above example observed the `["toys"]` key path, then any insertion, + deletion, or modification to the `toys` list for any element in the collection + would trigger the block. + Changes to any value on any `Toy` that is linked to a `Dog` in this collection + would *not* trigger the block. + Any insertion or removal to the `Dog` type collection being observed + would still trigger a notification. + - Any modification to the section key path property which results in the object changing + position in the section, or changing section entirely will trigger a notification. + + - note: Multiple notification tokens on the same object which filter for + separate key paths *do not* filter exclusively. If one key path + change is satisfied for one notification token, then all notification + token blocks for that object will execute. + + You must retain the returned token for as long as you want updates to be sent to the block. To stop receiving + updates, call `invalidate()` on the token. + + - warning: This method cannot be called during a write transaction, or when the containing Realm is read-only. + + - parameter keyPaths: Only properties contained in the key paths array will trigger + the block when they are modified. If `nil`, notifications + will be delivered for any property change on the object. + - parameter queue: The serial dispatch queue to receive notification on. If + `nil`, notifications are delivered to the current thread. + - parameter block: The block to be called whenever a change occurs. + - returns: A token which must be held for as long as you want updates to be delivered. + */ + func observe(keyPaths: [PartialKeyPath], + on queue: DispatchQueue? = nil, + _ block: @escaping (SectionedResultsChange) -> Void) -> NotificationToken { + observe(keyPaths: keyPaths.map(_name(for:)), on: queue, block) + } + + @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) + func observe( + keyPaths: [PartialKeyPath], on actor: A, + _isolation: isolated (any Actor)? = #isolation, + _ block: @Sendable @escaping (isolated A, SectionedResultsChange) -> Void + ) async -> NotificationToken { + await observe(keyPaths: keyPaths.map(_name(for:)), on: actor, block) + } +} +#endif // Shared implementation of SectionedResults and ResultsSection private protocol SectionedResultImpl: RealmSectionedResult { diff --git a/RealmSwift/SwiftUI.swift b/RealmSwift/SwiftUI.swift index 1608e2fc78..983d981375 100644 --- a/RealmSwift/SwiftUI.swift +++ b/RealmSwift/SwiftUI.swift @@ -39,6 +39,7 @@ private func thawObjectIfFrozen(_ value: Value) -> Value where Value: Obj } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +@MainActor private func createBinding( _ value: T, forKeyPath keyPath: ReferenceWritableKeyPath) -> Binding { @@ -63,6 +64,7 @@ private func createBinding( } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +@MainActor private func createCollectionBinding( _ value: T, forKeyPath keyPath: ReferenceWritableKeyPath) -> Binding { @@ -88,6 +90,7 @@ private func createCollectionBinding( _ value: T, forKeyPath keyPath: ReferenceWritableKeyPath) -> Binding { @@ -365,6 +368,7 @@ private class ObservableResultsStorage: ObservableStorage where T: RealmSu /// /// This will write the modified `isEnabled` property to the `model` object's Realm. @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +@MainActor @propertyWrapper public struct StateRealmObject: DynamicProperty { @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) @StateObject private var storage: ObservableStorage @@ -1028,9 +1032,12 @@ extension Projection: _ObservedResultsValue { } /// A property wrapper type that subscribes to an observable Realm `Object` or `List` and /// invalidates a view whenever the observable object changes. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) -@propertyWrapper public struct ObservedRealmObject: DynamicProperty where ObjectType: RealmSubscribable & ThreadConfined & ObservableObject & Equatable { +@MainActor +@propertyWrapper public struct ObservedRealmObject: DynamicProperty +where ObjectType: RealmSubscribable & ThreadConfined & ObservableObject & Equatable { /// A wrapper of the underlying observable object that can create bindings to /// its properties using dynamic member lookup. + @MainActor @dynamicMemberLookup @frozen public struct Wrapper { /// :nodoc: public var wrappedValue: ObjectType @@ -1118,14 +1125,17 @@ extension Projection: _ObservedResultsValue { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension Binding where Value: ObjectBase & ThreadConfined { /// :nodoc: + @MainActor public subscript(dynamicMember member: ReferenceWritableKeyPath) -> Binding where V: _Persistable { createBinding(wrappedValue, forKeyPath: member) } /// :nodoc: + @MainActor public subscript(dynamicMember member: ReferenceWritableKeyPath) -> Binding where V: _Persistable & RLMSwiftCollectionBase & ThreadConfined { createCollectionBinding(wrappedValue, forKeyPath: member) } /// :nodoc: + @MainActor public subscript(dynamicMember member: ReferenceWritableKeyPath) -> Binding where V: _Persistable & Equatable { createEquatableBinding(wrappedValue, forKeyPath: member) } @@ -1388,10 +1398,12 @@ extension ThreadConfined where Self: ProjectionObservable { - parameter keyPath The key path to the member property. - returns A `Binding` to the member property. */ + @MainActor public func bind(_ keyPath: ReferenceWritableKeyPath) -> Binding { createEquatableBinding(self, forKeyPath: keyPath) } /// :nodoc: + @MainActor public func bind(_ keyPath: ReferenceWritableKeyPath) -> Binding { createCollectionBinding(self, forKeyPath: keyPath) } @@ -1420,10 +1432,12 @@ extension ThreadConfined where Self: ObjectBase { - parameter keyPath The key path to the member property. - returns A `Binding` to the member property. */ + @MainActor public func bind(_ keyPath: ReferenceWritableKeyPath) -> Binding { createEquatableBinding(self, forKeyPath: keyPath) } /// :nodoc: + @MainActor public func bind(_ keyPath: ReferenceWritableKeyPath) -> Binding { createCollectionBinding(self, forKeyPath: keyPath) } @@ -1713,6 +1727,7 @@ private class ObservableAsyncOpenStorage: ObservableObject { /// ListView() /// .environment(\.realm, realm) /// +@MainActor @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) @propertyWrapper public struct AsyncOpen: DynamicProperty { @Environment(\.realmConfiguration) var configuration @@ -1824,7 +1839,7 @@ private class ObservableAsyncOpenStorage: ObservableObject { /// /// This property wrapper behaves similar as `AsyncOpen`, and in terms of declaration and use is completely identical, /// but with the difference of a offline-first approach. -/// +@MainActor @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) @propertyWrapper public struct AutoOpen: DynamicProperty { @Environment(\.realmConfiguration) var configuration diff --git a/RealmSwift/Sync.swift b/RealmSwift/Sync.swift index dcb79d860b..bb309dbeeb 100644 --- a/RealmSwift/Sync.swift +++ b/RealmSwift/Sync.swift @@ -1131,13 +1131,11 @@ extension User: ObservableObject {} #endif public extension User { - // NEXT-MAJOR: This function returns the incorrect type. It should be Document - // rather than `[AnyHashable: Any]` /// Refresh a user's custom data. This will, in effect, refresh the user's auth session. /// @completion A completion that eventually return `Result.success(Dictionary)` with user's data or `Result.failure(Error)`. @preconcurrency func refreshCustomData(_ completion: @escaping @Sendable (Result<[AnyHashable: Any], Error>) -> Void) { - self.refreshCustomData { customData, error in + self.__refreshCustomData { customData, error in if let customData = customData { completion(.success(customData)) } else { @@ -1145,6 +1143,22 @@ public extension User { } } } + + /// Refresh a user's custom data. This will, in effect, refresh the user's auth session. + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + @discardableResult + func refreshCustomData() async throws -> Document { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + self.__refreshCustomData { customData, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } + return customData + } } @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) diff --git a/RealmSwift/Tests/SwiftUITests.swift b/RealmSwift/Tests/SwiftUITests.swift index 7891042375..8cc47e0d10 100644 --- a/RealmSwift/Tests/SwiftUITests.swift +++ b/RealmSwift/Tests/SwiftUITests.swift @@ -69,48 +69,51 @@ class SwiftUITests: TestCase, @unchecked Sendable { // MARK: - List Operations - func testManagedUnmanagedListAppendPrimitive() throws { + @MainActor func testManagedUnmanagedListAppendPrimitive() throws { let object = SwiftUIObject() - let state = StateRealmObject(wrappedValue: object.primitiveList) - XCTAssertEqual(state.wrappedValue.count, 0) - state.projectedValue.append(1) - XCTAssertEqual(state.wrappedValue.count, 1) + @StateRealmObject var state = object.primitiveList + XCTAssertEqual(state.count, 0) + $state.append(1) + XCTAssertEqual(state.count, 1) let realm = inMemoryRealm(inMemoryIdentifier) try realm.write { realm.add(object) } - state.projectedValue.append(2) - XCTAssertEqual(state.wrappedValue.count, 2) + $state.append(2) + XCTAssertEqual(state.count, 2) } - func testManagedUnmanagedListAppendUnmanagedObject() throws { + + @MainActor func testManagedUnmanagedListAppendUnmanagedObject() throws { let object = SwiftUIObject() - let state = StateRealmObject(wrappedValue: object.list) - XCTAssertEqual(state.wrappedValue.count, 0) - state.projectedValue.append(SwiftBoolObject()) - XCTAssertEqual(state.wrappedValue.count, 1) + @StateRealmObject var state = object.list + XCTAssertEqual(state.count, 0) + $state.append(SwiftBoolObject()) + XCTAssertEqual(state.count, 1) let realm = inMemoryRealm(inMemoryIdentifier) try realm.write { realm.add(object) } - state.projectedValue.append(SwiftBoolObject()) - XCTAssertEqual(state.wrappedValue.count, 2) + $state.append(SwiftBoolObject()) + XCTAssertEqual(state.count, 2) } - func testManagedListAppendUnmanagedObservedObject() throws { + + @MainActor func testManagedListAppendUnmanagedObservedObject() throws { let object = SwiftUIObject() - var state = StateRealmObject(wrappedValue: object.list) - XCTAssertEqual(state.wrappedValue.count, 0) + @StateRealmObject var state = object.list + XCTAssertEqual(state.count, 0) let realm = inMemoryRealm(inMemoryIdentifier) try realm.write { realm.add(object) } - state.update() - state.projectedValue.append(SwiftBoolObject()) - XCTAssertEqual(state.wrappedValue.count, 1) + _state.update() + $state.append(SwiftBoolObject()) + XCTAssertEqual(state.count, 1) } - func testManagedListAppendFrozenObject() throws { + + @MainActor func testManagedListAppendFrozenObject() throws { let listObj = SwiftUIObject() - var state = StateRealmObject(wrappedValue: listObj.list) - XCTAssertEqual(state.wrappedValue.count, 0) + @StateRealmObject var state = listObj.list + XCTAssertEqual(state.count, 0) let realm = inMemoryRealm(inMemoryIdentifier) let obj = SwiftBoolObject() @@ -120,97 +123,99 @@ class SwiftUITests: TestCase, @unchecked Sendable { } let frozen = obj.freeze() - state.update() - state.projectedValue.append(frozen) - XCTAssertEqual(state.wrappedValue.count, 1) + _state.update() + $state.append(frozen) + XCTAssertEqual(state.count, 1) } - func testManagedUnmanagedListRemovePrimitive() throws { + + @MainActor func testManagedUnmanagedListRemovePrimitive() throws { let object = SwiftUIObject() - let state = StateRealmObject(wrappedValue: object.primitiveList) - XCTAssertEqual(state.wrappedValue.count, 0) - state.projectedValue.append(1) - XCTAssertEqual(state.wrappedValue.count, 1) + @StateRealmObject var state = object.primitiveList + XCTAssertEqual(state.count, 0) + $state.append(1) + XCTAssertEqual(state.count, 1) let realm = inMemoryRealm(inMemoryIdentifier) try realm.write { realm.add(object) } - state.projectedValue.append(2) - XCTAssertEqual(state.wrappedValue.count, 2) + $state.append(2) + XCTAssertEqual(state.count, 2) - state.projectedValue.remove(at: 0) - XCTAssertEqual(state.wrappedValue[0], 2) - XCTAssertEqual(state.wrappedValue.count, 1) + $state.remove(at: 0) + XCTAssertEqual(state[0], 2) + XCTAssertEqual(state.count, 1) } - func testManagedUnmanagedListRemoveUnmanagedObject() throws { + + @MainActor func testManagedUnmanagedListRemoveUnmanagedObject() throws { let object = SwiftUIObject() - let state = StateRealmObject(wrappedValue: object.list) - XCTAssertEqual(state.wrappedValue.count, 0) - state.projectedValue.append(SwiftBoolObject()) - XCTAssertEqual(state.wrappedValue.count, 1) - state.projectedValue.remove(at: 0) - XCTAssertEqual(state.wrappedValue.count, 0) + @StateRealmObject var state = object.list + XCTAssertEqual(state.count, 0) + $state.append(SwiftBoolObject()) + XCTAssertEqual(state.count, 1) + $state.remove(at: 0) + XCTAssertEqual(state.count, 0) } - func testManagedListAppendRemoveObservedObject() throws { + @MainActor func testManagedListAppendRemoveObservedObject() throws { let object = SwiftUIObject() - var state = StateRealmObject(wrappedValue: object.list) - XCTAssertEqual(state.wrappedValue.count, 0) + @StateRealmObject var state = object.list + XCTAssertEqual(state.count, 0) let realm = inMemoryRealm(inMemoryIdentifier) try realm.write { realm.add(object) } - state.update() - state.projectedValue.append(SwiftBoolObject()) - XCTAssertEqual(state.wrappedValue.count, 1) + _state.update() + $state.append(SwiftBoolObject()) + XCTAssertEqual(state.count, 1) - state.projectedValue.remove(at: 0) - XCTAssertEqual(state.wrappedValue.count, 0) + $state.remove(at: 0) + XCTAssertEqual(state.count, 0) } // MARK: - MutableSet Operations - func testManagedUnmanagedMutableSetInsertPrimitive() throws { + @MainActor func testManagedUnmanagedMutableSetInsertPrimitive() throws { let object = SwiftUIObject() - let state = StateRealmObject(wrappedValue: object.primitiveSet) - XCTAssertEqual(state.wrappedValue.count, 0) - state.projectedValue.insert(1) - XCTAssertEqual(state.wrappedValue.count, 1) + @StateRealmObject var state = object.primitiveSet + XCTAssertEqual(state.count, 0) + $state.insert(1) + XCTAssertEqual(state.count, 1) let realm = inMemoryRealm(inMemoryIdentifier) try realm.write { realm.add(object) } - state.projectedValue.insert(2) - XCTAssertEqual(state.wrappedValue.count, 2) + $state.insert(2) + XCTAssertEqual(state.count, 2) } - func testManagedUnmanagedMutableSetInsertUnmanagedObject() throws { + @MainActor func testManagedUnmanagedMutableSetInsertUnmanagedObject() throws { let object = SwiftUIObject() - let state = StateRealmObject(wrappedValue: object.set) - XCTAssertEqual(state.wrappedValue.count, 0) - state.projectedValue.insert(SwiftBoolObject()) - XCTAssertEqual(state.wrappedValue.count, 1) + @StateRealmObject var state = object.set + XCTAssertEqual(state.count, 0) + $state.insert(SwiftBoolObject()) + XCTAssertEqual(state.count, 1) let realm = inMemoryRealm(inMemoryIdentifier) try realm.write { realm.add(object) } - state.projectedValue.insert(SwiftBoolObject()) - XCTAssertEqual(state.wrappedValue.count, 2) + $state.insert(SwiftBoolObject()) + XCTAssertEqual(state.count, 2) } - func testManagedMutableSetInsertUnmanagedObservedObject() throws { + @MainActor func testManagedMutableSetInsertUnmanagedObservedObject() throws { let object = SwiftUIObject() - var state = StateRealmObject(wrappedValue: object.set) - XCTAssertEqual(state.wrappedValue.count, 0) + @StateRealmObject var state = object.set + XCTAssertEqual(state.count, 0) let realm = inMemoryRealm(inMemoryIdentifier) try realm.write { realm.add(object) } - state.update() - state.projectedValue.insert(SwiftBoolObject()) - XCTAssertEqual(state.wrappedValue.count, 1) + _state.update() + $state.insert(SwiftBoolObject()) + XCTAssertEqual(state.count, 1) } - func testManagedMutableSetInsertFrozenObject() throws { + @MainActor func testManagedMutableSetInsertFrozenObject() throws { let object = SwiftUIObject() - var state = StateRealmObject(wrappedValue: object.set) - XCTAssertEqual(state.wrappedValue.count, 0) + @StateRealmObject var state = object.set + XCTAssertEqual(state.count, 0) let realm = inMemoryRealm(inMemoryIdentifier) let obj = SwiftBoolObject() @@ -219,59 +224,59 @@ class SwiftUITests: TestCase, @unchecked Sendable { realm.add(obj) } let frozen = obj.freeze() - state.update() - state.projectedValue.insert(frozen) - XCTAssertEqual(state.wrappedValue.count, 1) + _state.update() + $state.insert(frozen) + XCTAssertEqual(state.count, 1) } - func testMutableSetRemovePrimitive() throws { + @MainActor func testMutableSetRemovePrimitive() throws { let object = SwiftUIObject() - let state = StateRealmObject(wrappedValue: object.primitiveSet) - XCTAssertEqual(state.wrappedValue.count, 0) - state.projectedValue.insert(1) - XCTAssertEqual(state.wrappedValue.count, 1) + @StateRealmObject var state = object.primitiveSet + XCTAssertEqual(state.count, 0) + $state.insert(1) + XCTAssertEqual(state.count, 1) let realm = inMemoryRealm(inMemoryIdentifier) try realm.write { realm.add(object) } - state.projectedValue.insert(2) - XCTAssertEqual(state.wrappedValue.count, 2) + $state.insert(2) + XCTAssertEqual(state.count, 2) - state.projectedValue.remove(1) - XCTAssertEqual(state.wrappedValue.count, 1) + $state.remove(1) + XCTAssertEqual(state.count, 1) } - func testUnmanagedMutableSetRemoveUnmanagedObject() throws { + @MainActor func testUnmanagedMutableSetRemoveUnmanagedObject() throws { let object = SwiftUIObject() - let state = StateRealmObject(wrappedValue: object.set) - XCTAssertEqual(state.wrappedValue.count, 0) + @StateRealmObject var state = object.set + XCTAssertEqual(state.count, 0) let obj = SwiftBoolObject() - state.projectedValue.insert(obj) - XCTAssertEqual(state.wrappedValue.count, 1) - state.projectedValue.remove(obj) - XCTAssertEqual(state.wrappedValue.count, 0) + $state.insert(obj) + XCTAssertEqual(state.count, 1) + $state.remove(obj) + XCTAssertEqual(state.count, 0) } - func testManagedMutableSetRemoveUnmanagedObject() throws { + @MainActor func testManagedMutableSetRemoveUnmanagedObject() throws { let object = SwiftUIObject() let realm = inMemoryRealm(inMemoryIdentifier) try realm.write { realm.add(object) } - let state = StateRealmObject(wrappedValue: object.set) - XCTAssertEqual(state.wrappedValue.count, 0) + @StateRealmObject var state = object.set + XCTAssertEqual(state.count, 0) let obj = SwiftBoolObject() - state.projectedValue.insert(obj) - XCTAssertEqual(state.wrappedValue.count, 1) + $state.insert(obj) + XCTAssertEqual(state.count, 1) XCTAssertNotNil(obj.realm) - state.projectedValue.remove(obj) - XCTAssertEqual(state.wrappedValue.count, 0) + $state.remove(obj) + XCTAssertEqual(state.count, 0) } - func testManagedMutableSetRemoveObservedObject() throws { + @MainActor func testManagedMutableSetRemoveObservedObject() throws { let object = SwiftUIObject() - var state = StateRealmObject(wrappedValue: object.set) - XCTAssertEqual(state.wrappedValue.count, 0) + @StateRealmObject var state = object.set + XCTAssertEqual(state.count, 0) let realm = inMemoryRealm(inMemoryIdentifier) try realm.write { realm.add(object) } - state.update() + _state.update() let obj = SwiftBoolObject() let objState = StateRealmObject(wrappedValue: obj) @@ -284,105 +289,105 @@ class SwiftUITests: TestCase, @unchecked Sendable { } objState.wrappedValue.boolCol = true XCTAssertEqual(hit, 1) - state.projectedValue.insert(objState.wrappedValue) - XCTAssertEqual(state.wrappedValue.count, 1) - state.projectedValue.remove(objState.wrappedValue) - XCTAssertEqual(state.wrappedValue.count, 0) + $state.insert(objState.wrappedValue) + XCTAssertEqual(state.count, 1) + $state.remove(objState.wrappedValue) + XCTAssertEqual(state.count, 0) cancellable.cancel() } // MARK: - Map Operations - func testManagedUnmanagedMapAppendPrimitive() throws { + @MainActor func testManagedUnmanagedMapAppendPrimitive() throws { let object = SwiftUIObject() - let state = StateRealmObject(wrappedValue: object.primitiveMap) - XCTAssertEqual(state.wrappedValue.count, 0) - state.projectedValue.set(object: 1, for: "one") - XCTAssertEqual(state.wrappedValue.count, 1) - XCTAssertEqual(state.projectedValue["one"], 1) + @StateRealmObject var state = object.primitiveMap + XCTAssertEqual(state.count, 0) + $state.set(object: 1, for: "one") + XCTAssertEqual(state.count, 1) + XCTAssertEqual($state["one"], 1) let realm = inMemoryRealm(inMemoryIdentifier) try realm.write { realm.add(object) } - state.projectedValue.set(object: 2, for: "two") - state.projectedValue.set(object: 3, for: "two") - XCTAssertEqual(state.wrappedValue.count, 2) - XCTAssertEqual(state.projectedValue["two"], 3) + $state.set(object: 2, for: "two") + $state.set(object: 3, for: "two") + XCTAssertEqual(state.count, 2) + XCTAssertEqual($state["two"], 3) } - func testManagedUnmanagedMapAppendUnmanagedObject() throws { + @MainActor func testManagedUnmanagedMapAppendUnmanagedObject() throws { let object = SwiftUIObject() - let state = StateRealmObject(wrappedValue: object.map) - XCTAssertEqual(state.wrappedValue.count, 0) - state.projectedValue.set(object: SwiftBoolObject(), for: "one") - XCTAssertEqual(state.wrappedValue.count, 1) + @StateRealmObject var state = object.map + XCTAssertEqual(state.count, 0) + $state.set(object: SwiftBoolObject(), for: "one") + XCTAssertEqual(state.count, 1) let realm = inMemoryRealm(inMemoryIdentifier) try realm.write { realm.add(object) } - state.projectedValue.set(object: SwiftBoolObject(), for: "two") - XCTAssertEqual(state.wrappedValue.count, 2) + $state.set(object: SwiftBoolObject(), for: "two") + XCTAssertEqual(state.count, 2) } - func testManagedMapAppendUnmanagedObservedObject() throws { + @MainActor func testManagedMapAppendUnmanagedObservedObject() throws { let object = SwiftUIObject() - var state = StateRealmObject(wrappedValue: object.map) - XCTAssertEqual(state.wrappedValue.count, 0) + @StateRealmObject var state = object.map + XCTAssertEqual(state.count, 0) let realm = inMemoryRealm(inMemoryIdentifier) try realm.write { realm.add(object) } - state.update() - state.projectedValue.set(object: SwiftBoolObject(), for: "one") - XCTAssertEqual(state.wrappedValue.count, 1) + _state.update() + $state.set(object: SwiftBoolObject(), for: "one") + XCTAssertEqual(state.count, 1) } - func testManagedUnmanagedMapRemovePrimitive() throws { + @MainActor func testManagedUnmanagedMapRemovePrimitive() throws { let object = SwiftUIObject() - let state = StateRealmObject(wrappedValue: object.primitiveMap) - XCTAssertEqual(state.wrappedValue.count, 0) - state.projectedValue.set(object: 1, for: "one") - XCTAssertEqual(state.wrappedValue.count, 1) + @StateRealmObject var state = object.primitiveMap + XCTAssertEqual(state.count, 0) + $state.set(object: 1, for: "one") + XCTAssertEqual(state.count, 1) let realm = inMemoryRealm(inMemoryIdentifier) try realm.write { realm.add(object) } - state.projectedValue.set(object: 2, for: "two") - XCTAssertEqual(state.wrappedValue.count, 2) + $state.set(object: 2, for: "two") + XCTAssertEqual(state.count, 2) - state.projectedValue.set(object: nil, for: "one") - XCTAssertEqual(state.wrappedValue.count, 1) - XCTAssertEqual(state.wrappedValue.keys, ["two"]) + $state.set(object: nil, for: "one") + XCTAssertEqual(state.count, 1) + XCTAssertEqual(state.keys, ["two"]) } - func testManagedUnmanagedMapRemoveUnmanagedObject() throws { + @MainActor func testManagedUnmanagedMapRemoveUnmanagedObject() throws { let object = SwiftUIObject() - let state = StateRealmObject(wrappedValue: object.map) - XCTAssertEqual(state.wrappedValue.count, 0) - state.projectedValue.set(object: SwiftBoolObject(), for: "one") - XCTAssertEqual(state.wrappedValue.count, 1) - state.projectedValue.set(object: nil, for: "one") - XCTAssertEqual(state.wrappedValue.count, 0) + @StateRealmObject var state = object.map + XCTAssertEqual(state.count, 0) + $state.set(object: SwiftBoolObject(), for: "one") + XCTAssertEqual(state.count, 1) + $state.set(object: nil, for: "one") + XCTAssertEqual(state.count, 0) } - func testManagedMapAppendRemoveObservedObject() throws { + @MainActor func testManagedMapAppendRemoveObservedObject() throws { let object = SwiftUIObject() - var state = StateRealmObject(wrappedValue: object.map) - XCTAssertEqual(state.wrappedValue.count, 0) + @StateRealmObject var state = object.map + XCTAssertEqual(state.count, 0) let realm = inMemoryRealm(inMemoryIdentifier) try realm.write { realm.add(object) } - state.update() - state.projectedValue.set(object: SwiftBoolObject(), for: "one") - XCTAssertEqual(state.wrappedValue.count, 1) + _state.update() + $state.set(object: SwiftBoolObject(), for: "one") + XCTAssertEqual(state.count, 1) - state.projectedValue.set(object: nil, for: "one") - XCTAssertEqual(state.wrappedValue.count, 0) + $state.set(object: nil, for: "one") + XCTAssertEqual(state.count, 0) } // MARK: - ObservedResults Operations - func testResultsAppendUnmanagedObject() throws { + @MainActor func testResultsAppendUnmanagedObject() throws { let object = SwiftUIObject() let fullResults = ObservedResults(SwiftUIObject.self, configuration: inMemoryRealm(inMemoryIdentifier).configuration) @@ -416,38 +421,35 @@ class SwiftUITests: TestCase, @unchecked Sendable { XCTAssertEqual(sortedResults.wrappedValue[0].int, 1) XCTAssertEqual(sortedResults.wrappedValue[1].int, 0) } - func testResultsAppendManagedObject() throws { - let state = ObservedResults(SwiftUIObject.self, configuration: inMemoryRealm(inMemoryIdentifier).configuration) + @MainActor func testResultsAppendManagedObject() throws { + @ObservedResults(SwiftUIObject.self, configuration: inMemoryRealm(inMemoryIdentifier).configuration) var state let object = SwiftUIObject() - XCTAssertEqual(state.wrappedValue.count, 0) - state.projectedValue.append(object) - XCTAssertEqual(state.wrappedValue.count, 1) - state.projectedValue.append(object) - XCTAssertEqual(state.wrappedValue.count, 1) - } - func testResultsRemoveUnmanagedObject() throws { - let state = ObservedResults(SwiftUIObject.self, - configuration: inMemoryRealm(inMemoryIdentifier).configuration) + XCTAssertEqual(state.count, 0) + $state.append(object) + XCTAssertEqual(state.count, 1) + $state.append(object) + XCTAssertEqual(state.count, 1) + } + @MainActor func testResultsRemoveUnmanagedObject() throws { + @ObservedResults(SwiftUIObject.self, configuration: inMemoryRealm(inMemoryIdentifier).configuration) var state let object = SwiftUIObject() - XCTAssertEqual(state.wrappedValue.count, 0) - assertThrows(state.projectedValue.remove(object)) - XCTAssertEqual(state.wrappedValue.count, 0) + XCTAssertEqual(state.count, 0) + assertThrows($state.remove(object)) + XCTAssertEqual(state.count, 0) } - func testResultsRemoveManagedObject() throws { - let state = ObservedResults(SwiftUIObject.self, - configuration: inMemoryRealm(inMemoryIdentifier).configuration) + @MainActor func testResultsRemoveManagedObject() throws { + @ObservedResults(SwiftUIObject.self, configuration: inMemoryRealm(inMemoryIdentifier).configuration) var state let object = SwiftUIObject() - XCTAssertEqual(state.wrappedValue.count, 0) - state.projectedValue.append(object) - XCTAssertEqual(state.wrappedValue.count, 1) - state.projectedValue.remove(object) - XCTAssertEqual(state.wrappedValue.count, 0) - } - func testResultsMoveUnmanagedObject() throws { - let state = ObservedResults(SwiftUIObject.self, - configuration: inMemoryRealm(inMemoryIdentifier).configuration) + XCTAssertEqual(state.count, 0) + $state.append(object) + XCTAssertEqual(state.count, 1) + $state.remove(object) + XCTAssertEqual(state.count, 0) + } + @MainActor func testResultsMoveUnmanagedObject() throws { + @ObservedResults(SwiftUIObject.self, configuration: inMemoryRealm(inMemoryIdentifier).configuration) var state let object = SwiftUIObject() - XCTAssertEqual(state.wrappedValue.count, 0) + XCTAssertEqual(state.count, 0) object.stringList.append(SwiftStringObject(stringCol: "Tom")) object.stringList.append(SwiftStringObject(stringCol: "Sam")) @@ -478,20 +480,19 @@ class SwiftUITests: TestCase, @unchecked Sendable { XCTAssertEqual(object.stringList[2].stringCol, "Dan") XCTAssertEqual(object.stringList.last!.stringCol, "Paul") - XCTAssertEqual(state.wrappedValue.count, 0) + XCTAssertEqual(state.count, 0) } - func testResultsMoveManagedObject() throws { - let state = ObservedResults(SwiftUIObject.self, - configuration: inMemoryRealm(inMemoryIdentifier).configuration) + @MainActor func testResultsMoveManagedObject() throws { + @ObservedResults(SwiftUIObject.self, configuration: inMemoryRealm(inMemoryIdentifier).configuration) var state let object = SwiftUIObject() - XCTAssertEqual(state.wrappedValue.count, 0) + XCTAssertEqual(state.count, 0) object.stringList.append(SwiftStringObject(stringCol: "Tom")) object.stringList.append(SwiftStringObject(stringCol: "Sam")) object.stringList.append(SwiftStringObject(stringCol: "Dan")) object.stringList.append(SwiftStringObject(stringCol: "Paul")) - state.projectedValue.append(object) + $state.append(object) let binding = object.bind(\.stringList) XCTAssertEqual(object.stringList.first!.stringCol, "Tom") @@ -517,9 +518,9 @@ class SwiftUITests: TestCase, @unchecked Sendable { XCTAssertEqual(object.stringList[2].stringCol, "Dan") XCTAssertEqual(object.stringList.last!.stringCol, "Paul") - XCTAssertEqual(state.wrappedValue.count, 1) + XCTAssertEqual(state.count, 1) } - func testSwiftQuerySyntax() throws { + @MainActor func testSwiftQuerySyntax() throws { let realm = inMemoryRealm(inMemoryIdentifier) try realm.write { realm.add(SwiftUIObject(value: ["str": "apple"])) @@ -534,7 +535,7 @@ class SwiftUITests: TestCase, @unchecked Sendable { XCTAssertEqual(filteredResults.wrappedValue.count, 2) XCTAssertEqual(filteredResults.wrappedValue[0].str, "antenna") } - func testResultsAppendFrozenObject() throws { + @MainActor func testResultsAppendFrozenObject() throws { let state1 = ObservedResults(SwiftUIObject.self, configuration: inMemoryRealm(inMemoryIdentifier).configuration) let object1 = SwiftUIObject() XCTAssertEqual(state1.wrappedValue.count, 0) @@ -559,34 +560,34 @@ class SwiftUITests: TestCase, @unchecked Sendable { XCTAssertEqual(state2.wrappedValue.count, 2) } // MARK: Object Operations - func testUnmanagedObjectModification() throws { - let state = StateRealmObject(wrappedValue: SwiftUIObject()) - state.wrappedValue.str = "bar" - XCTAssertEqual(state.wrappedValue.str, "bar") - XCTAssertEqual(state.projectedValue.wrappedValue.str, "bar") - } - func testManagedObjectModification() throws { - let state = StateRealmObject(wrappedValue: SwiftUIObject()) + @MainActor func testUnmanagedObjectModification() throws { + @StateRealmObject var state = SwiftUIObject() + state.str = "bar" + XCTAssertEqual(state.str, "bar") + XCTAssertEqual($state.wrappedValue.str, "bar") + } + @MainActor func testManagedObjectModification() throws { + @StateRealmObject var state = SwiftUIObject() ObservedResults(SwiftUIObject.self, configuration: inMemoryRealm(inMemoryIdentifier).configuration) - .projectedValue.append(state.wrappedValue) - assertThrows(state.wrappedValue.str = "bar") - state.projectedValue.str.wrappedValue = "bar" - XCTAssertEqual(state.projectedValue.wrappedValue.str, "bar") + .projectedValue.append(state) + assertThrows(state.str = "bar") + $state.str.wrappedValue = "bar" + XCTAssertEqual($state.wrappedValue.str, "bar") } - func testManagedObjectDelete() throws { + @MainActor func testManagedObjectDelete() throws { let results = ObservedResults(SwiftUIObject.self, configuration: inMemoryRealm(inMemoryIdentifier).configuration) - let state = StateRealmObject(wrappedValue: SwiftUIObject()) + @StateRealmObject var state = SwiftUIObject() XCTAssertEqual(results.wrappedValue.count, 0) - state.projectedValue.delete() + $state.delete() XCTAssertEqual(results.wrappedValue.count, 0) - results.projectedValue.append(state.wrappedValue) + results.projectedValue.append(state) XCTAssertEqual(results.wrappedValue.count, 1) - state.projectedValue.delete() + $state.delete() } // MARK: Bind - func testUnmanagedManagedObjectBind() { + @MainActor func testUnmanagedManagedObjectBind() { let object = SwiftUIObject() let binding = object.bind(\.str) XCTAssertEqual(object.str, "foo") @@ -605,7 +606,7 @@ class SwiftUITests: TestCase, @unchecked Sendable { XCTAssertEqual(binding.wrappedValue, "baz") } - func testStateRealmObjectKVO() throws { + @MainActor func testStateRealmObjectKVO() throws { @StateRealmObject var object = SwiftUIObject() var hit = 0 @@ -633,33 +634,31 @@ class SwiftUITests: TestCase, @unchecked Sendable { } // MARK: - Projection ObservedResults Operations - func testResultsAppendProjection() throws { + @MainActor func testResultsAppendProjection() throws { let realm = inMemoryRealm(inMemoryIdentifier) - let state = ObservedResults(UIElementsProjection.self, - configuration: inMemoryRealm(inMemoryIdentifier).configuration) - XCTAssertEqual(state.wrappedValue.count, 0) + @ObservedResults(UIElementsProjection.self, configuration: inMemoryRealm(inMemoryIdentifier).configuration) var state + XCTAssertEqual(state.count, 0) try! realm.write { realm.create(SwiftUIObject.self) } - XCTAssertEqual(state.wrappedValue.count, 1) + XCTAssertEqual(state.count, 1) } - func testResultsRemoveProjection() throws { + @MainActor func testResultsRemoveProjection() throws { let realm = inMemoryRealm(inMemoryIdentifier) - let state = ObservedResults(UIElementsProjection.self, - configuration: inMemoryRealm(inMemoryIdentifier).configuration) + @ObservedResults(UIElementsProjection.self, configuration: inMemoryRealm(inMemoryIdentifier).configuration) var state var object: SwiftUIObject! try! realm.write { object = realm.create(SwiftUIObject.self) } - XCTAssertEqual(state.wrappedValue.count, 1) + XCTAssertEqual(state.count, 1) try! realm.write { realm.delete(object) } - XCTAssertEqual(state.wrappedValue.count, 0) + XCTAssertEqual(state.count, 0) } - func testProjectionStateRealmObjectKVO() throws { + @MainActor func testProjectionStateRealmObjectKVO() throws { @StateRealmObject var projection = UIElementsProjection(projecting: SwiftUIObject()) var hit = 0 @@ -686,22 +685,22 @@ class SwiftUITests: TestCase, @unchecked Sendable { XCTAssertEqual(hit, 2) } - func testProjectionDelete() throws { + @MainActor func testProjectionDelete() throws { let results = ObservedResults(UIElementsProjection.self, configuration: inMemoryRealm(inMemoryIdentifier).configuration) let projection = UIElementsProjection(projecting: SwiftUIObject()) - let state = StateRealmObject(wrappedValue: projection) + @StateRealmObject var state = projection XCTAssertEqual(results.wrappedValue.count, 0) - state.projectedValue.delete() + $state.delete() XCTAssertEqual(results.wrappedValue.count, 0) - results.projectedValue.append(state.wrappedValue) + results.projectedValue.append(state) XCTAssertEqual(results.wrappedValue.count, 1) - state.projectedValue.delete() + $state.delete() } // MARK: - Projection Bind - func testProjectionBind() { + @MainActor func testProjectionBind() { let projection = UIElementsProjection(projecting: SwiftUIObject()) let binding = projection.bind(\.label) XCTAssertEqual(projection.label, "foo") @@ -722,7 +721,7 @@ class SwiftUITests: TestCase, @unchecked Sendable { // MARK: - ObservedSectionedResults - func testObservedSectionedResults() throws { + @MainActor func testObservedSectionedResults() throws { let fullResults = ObservedSectionedResults(SwiftUIObject.self, sectionKeyPath: \.str, configuration: inMemoryRealm(inMemoryIdentifier).configuration) @@ -768,7 +767,7 @@ class SwiftUITests: TestCase, @unchecked Sendable { XCTAssertEqual(fullResults.wrappedValue[0].key, "abc") } - func testObservedSectionedResultsWithProjection() throws { + @MainActor func testObservedSectionedResultsWithProjection() throws { let fullResults = ObservedSectionedResults(UIElementsProjection.self, sectionKeyPath: \.label, configuration: inMemoryRealm(inMemoryIdentifier).configuration) @@ -801,7 +800,7 @@ class SwiftUITests: TestCase, @unchecked Sendable { XCTAssertEqual(filteredResults.wrappedValue[0].key, "def") } - func testAllObservedSectionedResultsConstructors() throws { + @MainActor func testAllObservedSectionedResultsConstructors() throws { let realm = inMemoryRealm(inMemoryIdentifier) let object1 = SwiftUIObject() let object2 = SwiftUIObject() diff --git a/RealmSwift/Tests/TestUtils.swift b/RealmSwift/Tests/TestUtils.swift index 64e42ba9b8..6d42c5b3e7 100644 --- a/RealmSwift/Tests/TestUtils.swift +++ b/RealmSwift/Tests/TestUtils.swift @@ -90,7 +90,7 @@ public struct Unchecked: @unchecked Sendable { public extension XCTestCase { /// Check whether two test objects are equal (refer to the same row in the same Realm), even if their models /// don't define a primary key. - func assertEqual(_ o1: O?, _ o2: O?, fileName: StaticString = #file, lineNumber: UInt = #line) { + func assertEqual(_ o1: O?, _ o2: O?, fileName: StaticString = #filePath, lineNumber: UInt = #line) { if o1 == nil && o2 == nil { return } @@ -102,7 +102,7 @@ public extension XCTestCase { } /// Check whether two collections containing Realm objects are equal. - func assertEqual(_ c1: C, _ c2: C, fileName: StaticString = #file, lineNumber: UInt = #line) + func assertEqual(_ c1: C, _ c2: C, fileName: StaticString = #filePath, lineNumber: UInt = #line) where C.Iterator.Element: Object { XCTAssertEqual(c1.count, c2.count, "Collection counts were incorrect", file: (fileName), line: lineNumber) for (o1, o2) in zip(c1, c2) { @@ -125,7 +125,7 @@ public extension XCTestCase { } } - func assertSucceeds(message: String? = nil, fileName: StaticString = #file, + func assertSucceeds(message: String? = nil, fileName: StaticString = #filePath, lineNumber: UInt = #line, block: () throws -> Void) { do { try block() @@ -136,7 +136,7 @@ public extension XCTestCase { } func assertFails(_ expectedError: Realm.Error.Code, _ message: String? = nil, - fileName: StaticString = #file, lineNumber: UInt = #line, + fileName: StaticString = #filePath, lineNumber: UInt = #line, block: () throws -> T) { do { _ = try autoreleasepool(invoking: block) @@ -153,7 +153,7 @@ public extension XCTestCase { } func assertFails(_ expectedError: Realm.Error.Code, _ file: URL, _ message: String, - fileName: StaticString = #file, lineNumber: UInt = #line, + fileName: StaticString = #filePath, lineNumber: UInt = #line, block: () throws -> T) { do { _ = try autoreleasepool(invoking: block) @@ -169,7 +169,7 @@ public extension XCTestCase { } func assertFails(_ expectedError: Error, _ message: String? = nil, - fileName: StaticString = #file, lineNumber: UInt = #line, + fileName: StaticString = #filePath, lineNumber: UInt = #line, block: () throws -> T) { do { _ = try autoreleasepool(invoking: block) @@ -184,7 +184,7 @@ public extension XCTestCase { } func assertNil(block: @autoclosure() -> T?, _ message: String? = nil, - fileName: StaticString = #file, lineNumber: UInt = #line) { + fileName: StaticString = #filePath, lineNumber: UInt = #line) { XCTAssert(block() == nil, message ?? "", file: (fileName), line: lineNumber) }