From 56f498f124435d13cd0cbddb17a101f82edf2127 Mon Sep 17 00:00:00 2001 From: Diana Perez Afanador Date: Thu, 3 Mar 2022 16:04:06 -0500 Subject: [PATCH] Solve SwiftUI performance issues --- .../SwiftUISyncTestHostUITests.swift | 5 ++ Realm/Tests/SwiftUITestHost/Objects.swift | 1 + .../SwiftUITestHost/SwiftUITestHostApp.swift | 61 ++++++++++++++++ .../SwiftUITestHostUITests.swift | 22 ++++++ RealmSwift/Combine.swift | 8 ++- RealmSwift/SwiftUI.swift | 72 +++++++++++++------ 6 files changed, 143 insertions(+), 26 deletions(-) diff --git a/Realm/Tests/SwiftUISyncTestHostUITests/SwiftUISyncTestHostUITests.swift b/Realm/Tests/SwiftUISyncTestHostUITests/SwiftUISyncTestHostUITests.swift index 9ef59f0af6..09de3d0f44 100644 --- a/Realm/Tests/SwiftUISyncTestHostUITests/SwiftUISyncTestHostUITests.swift +++ b/Realm/Tests/SwiftUISyncTestHostUITests/SwiftUISyncTestHostUITests.swift @@ -354,6 +354,11 @@ extension SwiftUISyncTestHostUITests { realm2.add(SwiftPerson(firstName: "Jane2", lastName: "Doe")) } user2.waitForUpload(toFinish: partitionValue) + + user1.waitForDownload(toFinish: partitionValue) + realm.refresh() + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 4) + XCTAssertEqual(table.cells.count, 4) loginUser(.first) diff --git a/Realm/Tests/SwiftUITestHost/Objects.swift b/Realm/Tests/SwiftUITestHost/Objects.swift index d01b4ecc36..fd5d16145b 100644 --- a/Realm/Tests/SwiftUITestHost/Objects.swift +++ b/Realm/Tests/SwiftUITestHost/Objects.swift @@ -44,5 +44,6 @@ class Reminder: EmbeddedObject, ObjectKeyIdentifiable { class ReminderList: Object, ObjectKeyIdentifiable { @Persisted var name = "New List" @Persisted var icon = "list.bullet" + @Persisted var colorNumber: Int = 0 @Persisted var reminders = RealmSwift.List() } diff --git a/Realm/Tests/SwiftUITestHost/SwiftUITestHostApp.swift b/Realm/Tests/SwiftUITestHost/SwiftUITestHostApp.swift index 9e98544628..bbe39564ad 100644 --- a/Realm/Tests/SwiftUITestHost/SwiftUITestHostApp.swift +++ b/Realm/Tests/SwiftUITestHost/SwiftUITestHostApp.swift @@ -18,6 +18,7 @@ import RealmSwift import SwiftUI +import Realm.Private struct ReminderFormView: View { @ObservedRealmObject var reminder: Reminder @@ -306,6 +307,27 @@ struct ObservedResultsSearchableTestView: View { } } +struct ObservedResultsSchemaBumpTestView: View { + @ObservedResults(ReminderList.self) var reminders + @State var searchFilter: String = "" + + var body: some View { + NavigationView { + List { + ForEach(reminders) { reminder in + Text(reminder.name) + } + } + .navigationTitle("Reminders") + .navigationBarItems(trailing: + Button("add") { + let reminder = ReminderList() + $reminders.append(reminder) + }.accessibility(identifier: "addList")) + } + } +} + @main struct App: SwiftUI.App { var body: some Scene { @@ -330,6 +352,10 @@ struct App: SwiftUI.App { } else { return AnyView(EmptyView()) } + case "schema_bump_test": + let newconfiguration = configuration + return AnyView(ObservedResultsSchemaBumpTestView() + .environment(\.realmConfiguration, newconfiguration)) default: return AnyView(ContentView()) } @@ -338,4 +364,39 @@ struct App: SwiftUI.App { view } } + + // we are retrieving different configurations for different schema version to been able to test schema migrations on SwiftUI injecting the configuration as an environment value + var configuration: Realm.Configuration { + let schemaVersion = UInt64(ProcessInfo.processInfo.environment["schema_version"]!)! + let rlmConfiguration = RLMRealmConfiguration() + rlmConfiguration.objectClasses = [ReminderList.self, Reminder.self] + switch schemaVersion { + case 2: + let schema = RLMSchema(objectClasses: [ReminderList.classForCoder(), Reminder.classForCoder()]) + let objectSchema = schema.objectSchema[0] + let property = objectSchema.properties[2] + property.name = "colorFloat" + property.type = .float + rlmConfiguration.customSchema = schema + default: break + } + + // Set the default configuration so we can get a swift configuration with the schema change to force a migration block + RLMRealmConfiguration.setDefault(rlmConfiguration) + var configuration = Realm.Configuration.defaultConfiguration + configuration.schemaVersion = schemaVersion + configuration.migrationBlock = { migration, oldSchemaVersion in + if oldSchemaVersion < 2 { + migration.enumerateObjects(ofType: ReminderList.className()) { oldObject, newObject in + let number = oldObject!["colorNumber"] as? Int ?? 0 + newObject!["colorFloat"] = Float(number) + } + } + } + configuration.fileURL = URL(string: ProcessInfo.processInfo.environment["schema_bump_path"]!)! + + // Reset the default configuration, so ObservedResults set a clean RLMRealmConfiguration the first time, before injecting the environment configuration + RLMRealmConfiguration.setDefault(RLMRealmConfiguration.init()) + return configuration + } } diff --git a/Realm/Tests/SwiftUITestHostUITests/SwiftUITestHostUITests.swift b/Realm/Tests/SwiftUITestHostUITests/SwiftUITestHostUITests.swift index cd181132f8..4fd87c0187 100644 --- a/Realm/Tests/SwiftUITestHostUITests/SwiftUITestHostUITests.swift +++ b/Realm/Tests/SwiftUITestHostUITests/SwiftUITestHostUITests.swift @@ -288,4 +288,26 @@ class SwiftUITests: XCTestCase { searchBar.typeText("12") XCTAssertEqual(table.cells.count, 1) } + + // This test allow us to test database migrations on a SwiftUI context + func testObservedResultsSchemaBump() { + let realmPath = URL(string: "\(FileManager.default.temporaryDirectory)\(UUID())")! + app.launchEnvironment["schema_bump_path"] = realmPath.absoluteString + app.launchEnvironment["test_type"] = "schema_bump_test" + app.launchEnvironment["schema_version"] = "1" + app.launch() + + let addButton = app.buttons["addList"] + (1...5).forEach { _ in + addButton.tap() + } + + XCTAssertEqual(app.tables.firstMatch.cells.count, 5) + app.terminate() + + // We bump the schema version and relaunch the app, which should migrate data from the previous version to the current one + app.launchEnvironment["schema_version"] = "2" + app.launch() + XCTAssertEqual(app.tables.firstMatch.cells.count, 5) + } } diff --git a/RealmSwift/Combine.swift b/RealmSwift/Combine.swift index cd4b1f1165..a52bb75401 100644 --- a/RealmSwift/Combine.swift +++ b/RealmSwift/Combine.swift @@ -703,14 +703,16 @@ extension RealmKeyedCollection { /// A subscription which wraps a Realm notification. @available(OSX 10.15, watchOS 6.0, iOS 13.0, iOSApplicationExtension 13.0, OSXApplicationExtension 10.15, tvOS 13.0, *) @frozen public struct ObservationSubscription: Subscription { - private var token: NotificationToken + private var token: NotificationToken? internal init(token: NotificationToken) { self.token = token } + internal init() {} + /// A unique identifier for identifying publisher streams. public var combineIdentifier: CombineIdentifier { - return CombineIdentifier(token) + return token != nil ? CombineIdentifier(token!) : CombineIdentifier(NSNumber(value: 0)) } /// This function is not implemented. @@ -721,7 +723,7 @@ extension RealmKeyedCollection { /// Stop emitting values on this subscription. public func cancel() { - token.invalidate() + token?.invalidate() } } diff --git a/RealmSwift/SwiftUI.swift b/RealmSwift/SwiftUI.swift index 5b8ab2c790..94620eea93 100644 --- a/RealmSwift/SwiftUI.swift +++ b/RealmSwift/SwiftUI.swift @@ -207,7 +207,7 @@ private final class ObservableStoragePublisher: Publisher where Obje if value.realm != nil && !value.isInvalidated, let value = value.thaw() { // This path is for cases where the object is already managed. If an // unmanaged object becomes managed it will continue to use KVO. - let token = value._observe(keyPaths, subscriber) + let token = value._observe(keyPaths, subscriber) subscriber.receive(subscription: ObservationSubscription(token: token)) } else if let value = unwrappedValue, !value.isInvalidated { // else if the value is unmanaged @@ -222,6 +222,9 @@ private final class ObservableStoragePublisher: Publisher where Obje let subscription = SwiftUIKVO.Subscription(observer: kvo, value: value, keyPaths: keyPaths) subscriber.receive(subscription: subscription) SwiftUIKVO.observedObjects[value] = subscription + } else { + // As SwiftUI calls this method before we setup the value, we create an empty subscription which will trigger an UI update when `send` gets called, which will call call again this method and allow us to observe the updated value. + subscriber.receive(subscription: ObservationSubscription()) } } } @@ -231,10 +234,8 @@ private class ObservableStorage: ObservableObject where ObservedTy @Published var value: ObservedType { willSet { if newValue != value { - objectWillChange.subscribers.forEach { - $0.receive(subscription: ObservationSubscription(token: newValue._observe(keyPaths, $0))) - } objectWillChange.send() + self.objectWillChange = ObservableStoragePublisher(newValue, keyPaths) } } } @@ -405,14 +406,12 @@ extension Projection: _ObservedResultsValue { } /// /// Given `@ObservedResults var v` in SwiftUI, `$v` refers to a `BoundCollection`. /// -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +@available(iOS 13.0, macOS 11.0, tvOS 13.0, watchOS 6.0, *) @propertyWrapper public struct ObservedResults: DynamicProperty, BoundCollection where ResultType: _ObservedResultsValue & RealmFetchable & KeypathSortable & Identifiable { private class Storage: ObservableStorage> { var setupHasRun = false private func didSet() { - if setupHasRun { - setupValue() - } + setupValue() } func setupValue() { @@ -453,6 +452,30 @@ extension Projection: _ObservedResultsValue { } } var searchString: String = "" + + init(_ results: Results, + configuration: Realm.Configuration? = nil, + filter: NSPredicate? = nil, + where: ((Query) -> Query)? = nil, + keyPaths: [String]? = nil, + sortDescriptor: SortDescriptor? = nil) where ResultType: Object { + super.init(results, keyPaths) + self.configuration = configuration + self.filter = filter + self.where = `where` + self.sortDescriptor = sortDescriptor + } + + init(_ results: Results, + configuration: Realm.Configuration? = nil, + filter: NSPredicate? = nil, + keyPaths: [String]? = nil, + sortDescriptor: SortDescriptor? = nil) where ResultType: Projection, ObjectType: ThreadConfined { + super.init(results, keyPaths) + self.configuration = configuration + self.filter = filter + self.sortDescriptor = sortDescriptor + } } @Environment(\.realmConfiguration) var configuration @@ -524,10 +547,10 @@ extension Projection: _ObservedResultsValue { } keyPaths: [String]? = nil, sortDescriptor: SortDescriptor? = nil) where ResultType: Projection, ObjectType: ThreadConfined { let results = Results(RLMResults.emptyDetached()) - self.storage = Storage(results, keyPaths) - self.storage.configuration = configuration - self.filter = filter - self.sortDescriptor = sortDescriptor + self.storage = Storage(results, + configuration: configuration, + filter: filter, + sortDescriptor: sortDescriptor) } /** Initialize a `ObservedResults` struct for a given `Object` or `EmbeddedObject` type. @@ -547,10 +570,11 @@ extension Projection: _ObservedResultsValue { } filter: NSPredicate? = nil, keyPaths: [String]? = nil, sortDescriptor: SortDescriptor? = nil) where ResultType: Object { - self.storage = Storage(Results(RLMResults.emptyDetached()), keyPaths) - self.storage.configuration = configuration - self.filter = filter - self.sortDescriptor = sortDescriptor + let results = Results(RLMResults.emptyDetached()) + self.storage = Storage(results, + configuration: configuration, + filter: filter, + sortDescriptor: sortDescriptor) } #if swift(>=5.5) /** @@ -571,10 +595,11 @@ extension Projection: _ObservedResultsValue { } where: ((Query) -> Query)? = nil, keyPaths: [String]? = nil, sortDescriptor: SortDescriptor? = nil) where ResultType: Object { - self.storage = Storage(Results(RLMResults.emptyDetached()), keyPaths) - self.storage.configuration = configuration - self.where = `where` - self.sortDescriptor = sortDescriptor + let results = Results(RLMResults.emptyDetached()) + self.storage = Storage(results, + configuration: configuration, + where: `where`, + sortDescriptor: sortDescriptor) } #endif /// :nodoc: @@ -582,9 +607,10 @@ extension Projection: _ObservedResultsValue { } keyPaths: [String]? = nil, configuration: Realm.Configuration? = nil, sortDescriptor: SortDescriptor? = nil) where ResultType: Object { - self.storage = Storage(Results(RLMResults.emptyDetached()), keyPaths) - self.storage.configuration = configuration - self.sortDescriptor = sortDescriptor + let results = Results(RLMResults.emptyDetached()) + self.storage = Storage(results, + configuration: configuration, + sortDescriptor: sortDescriptor) } public mutating func update() {