Skip to content

Commit 56f498f

Browse files
Solve SwiftUI performance issues
1 parent c6d8ff6 commit 56f498f

File tree

6 files changed

+143
-26
lines changed

6 files changed

+143
-26
lines changed

Realm/Tests/SwiftUISyncTestHostUITests/SwiftUISyncTestHostUITests.swift

+5
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,11 @@ extension SwiftUISyncTestHostUITests {
354354
realm2.add(SwiftPerson(firstName: "Jane2", lastName: "Doe"))
355355
}
356356
user2.waitForUpload(toFinish: partitionValue)
357+
358+
user1.waitForDownload(toFinish: partitionValue)
359+
realm.refresh()
360+
XCTAssertEqual(realm.objects(SwiftPerson.self).count, 4)
361+
357362
XCTAssertEqual(table.cells.count, 4)
358363

359364
loginUser(.first)

Realm/Tests/SwiftUITestHost/Objects.swift

+1
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,6 @@ class Reminder: EmbeddedObject, ObjectKeyIdentifiable {
4444
class ReminderList: Object, ObjectKeyIdentifiable {
4545
@Persisted var name = "New List"
4646
@Persisted var icon = "list.bullet"
47+
@Persisted var colorNumber: Int = 0
4748
@Persisted var reminders = RealmSwift.List<Reminder>()
4849
}

Realm/Tests/SwiftUITestHost/SwiftUITestHostApp.swift

+61
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import RealmSwift
2020
import SwiftUI
21+
import Realm.Private
2122

2223
struct ReminderFormView: View {
2324
@ObservedRealmObject var reminder: Reminder
@@ -306,6 +307,27 @@ struct ObservedResultsSearchableTestView: View {
306307
}
307308
}
308309

310+
struct ObservedResultsSchemaBumpTestView: View {
311+
@ObservedResults(ReminderList.self) var reminders
312+
@State var searchFilter: String = ""
313+
314+
var body: some View {
315+
NavigationView {
316+
List {
317+
ForEach(reminders) { reminder in
318+
Text(reminder.name)
319+
}
320+
}
321+
.navigationTitle("Reminders")
322+
.navigationBarItems(trailing:
323+
Button("add") {
324+
let reminder = ReminderList()
325+
$reminders.append(reminder)
326+
}.accessibility(identifier: "addList"))
327+
}
328+
}
329+
}
330+
309331
@main
310332
struct App: SwiftUI.App {
311333
var body: some Scene {
@@ -330,6 +352,10 @@ struct App: SwiftUI.App {
330352
} else {
331353
return AnyView(EmptyView())
332354
}
355+
case "schema_bump_test":
356+
let newconfiguration = configuration
357+
return AnyView(ObservedResultsSchemaBumpTestView()
358+
.environment(\.realmConfiguration, newconfiguration))
333359
default:
334360
return AnyView(ContentView())
335361
}
@@ -338,4 +364,39 @@ struct App: SwiftUI.App {
338364
view
339365
}
340366
}
367+
368+
// 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
369+
var configuration: Realm.Configuration {
370+
let schemaVersion = UInt64(ProcessInfo.processInfo.environment["schema_version"]!)!
371+
let rlmConfiguration = RLMRealmConfiguration()
372+
rlmConfiguration.objectClasses = [ReminderList.self, Reminder.self]
373+
switch schemaVersion {
374+
case 2:
375+
let schema = RLMSchema(objectClasses: [ReminderList.classForCoder(), Reminder.classForCoder()])
376+
let objectSchema = schema.objectSchema[0]
377+
let property = objectSchema.properties[2]
378+
property.name = "colorFloat"
379+
property.type = .float
380+
rlmConfiguration.customSchema = schema
381+
default: break
382+
}
383+
384+
// Set the default configuration so we can get a swift configuration with the schema change to force a migration block
385+
RLMRealmConfiguration.setDefault(rlmConfiguration)
386+
var configuration = Realm.Configuration.defaultConfiguration
387+
configuration.schemaVersion = schemaVersion
388+
configuration.migrationBlock = { migration, oldSchemaVersion in
389+
if oldSchemaVersion < 2 {
390+
migration.enumerateObjects(ofType: ReminderList.className()) { oldObject, newObject in
391+
let number = oldObject!["colorNumber"] as? Int ?? 0
392+
newObject!["colorFloat"] = Float(number)
393+
}
394+
}
395+
}
396+
configuration.fileURL = URL(string: ProcessInfo.processInfo.environment["schema_bump_path"]!)!
397+
398+
// Reset the default configuration, so ObservedResults set a clean RLMRealmConfiguration the first time, before injecting the environment configuration
399+
RLMRealmConfiguration.setDefault(RLMRealmConfiguration.init())
400+
return configuration
401+
}
341402
}

Realm/Tests/SwiftUITestHostUITests/SwiftUITestHostUITests.swift

+22
Original file line numberDiff line numberDiff line change
@@ -288,4 +288,26 @@ class SwiftUITests: XCTestCase {
288288
searchBar.typeText("12")
289289
XCTAssertEqual(table.cells.count, 1)
290290
}
291+
292+
// This test allow us to test database migrations on a SwiftUI context
293+
func testObservedResultsSchemaBump() {
294+
let realmPath = URL(string: "\(FileManager.default.temporaryDirectory)\(UUID())")!
295+
app.launchEnvironment["schema_bump_path"] = realmPath.absoluteString
296+
app.launchEnvironment["test_type"] = "schema_bump_test"
297+
app.launchEnvironment["schema_version"] = "1"
298+
app.launch()
299+
300+
let addButton = app.buttons["addList"]
301+
(1...5).forEach { _ in
302+
addButton.tap()
303+
}
304+
305+
XCTAssertEqual(app.tables.firstMatch.cells.count, 5)
306+
app.terminate()
307+
308+
// We bump the schema version and relaunch the app, which should migrate data from the previous version to the current one
309+
app.launchEnvironment["schema_version"] = "2"
310+
app.launch()
311+
XCTAssertEqual(app.tables.firstMatch.cells.count, 5)
312+
}
291313
}

RealmSwift/Combine.swift

+5-3
Original file line numberDiff line numberDiff line change
@@ -703,14 +703,16 @@ extension RealmKeyedCollection {
703703
/// A subscription which wraps a Realm notification.
704704
@available(OSX 10.15, watchOS 6.0, iOS 13.0, iOSApplicationExtension 13.0, OSXApplicationExtension 10.15, tvOS 13.0, *)
705705
@frozen public struct ObservationSubscription: Subscription {
706-
private var token: NotificationToken
706+
private var token: NotificationToken?
707707
internal init(token: NotificationToken) {
708708
self.token = token
709709
}
710710

711+
internal init() {}
712+
711713
/// A unique identifier for identifying publisher streams.
712714
public var combineIdentifier: CombineIdentifier {
713-
return CombineIdentifier(token)
715+
return token != nil ? CombineIdentifier(token!) : CombineIdentifier(NSNumber(value: 0))
714716
}
715717

716718
/// This function is not implemented.
@@ -721,7 +723,7 @@ extension RealmKeyedCollection {
721723

722724
/// Stop emitting values on this subscription.
723725
public func cancel() {
724-
token.invalidate()
726+
token?.invalidate()
725727
}
726728
}
727729

RealmSwift/SwiftUI.swift

+49-23
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ private final class ObservableStoragePublisher<ObjectType>: Publisher where Obje
207207
if value.realm != nil && !value.isInvalidated, let value = value.thaw() {
208208
// This path is for cases where the object is already managed. If an
209209
// unmanaged object becomes managed it will continue to use KVO.
210-
let token = value._observe(keyPaths, subscriber)
210+
let token = value._observe(keyPaths, subscriber)
211211
subscriber.receive(subscription: ObservationSubscription(token: token))
212212
} else if let value = unwrappedValue, !value.isInvalidated {
213213
// else if the value is unmanaged
@@ -222,6 +222,9 @@ private final class ObservableStoragePublisher<ObjectType>: Publisher where Obje
222222
let subscription = SwiftUIKVO.Subscription(observer: kvo, value: value, keyPaths: keyPaths)
223223
subscriber.receive(subscription: subscription)
224224
SwiftUIKVO.observedObjects[value] = subscription
225+
} else {
226+
// 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.
227+
subscriber.receive(subscription: ObservationSubscription())
225228
}
226229
}
227230
}
@@ -231,10 +234,8 @@ private class ObservableStorage<ObservedType>: ObservableObject where ObservedTy
231234
@Published var value: ObservedType {
232235
willSet {
233236
if newValue != value {
234-
objectWillChange.subscribers.forEach {
235-
$0.receive(subscription: ObservationSubscription(token: newValue._observe(keyPaths, $0)))
236-
}
237237
objectWillChange.send()
238+
self.objectWillChange = ObservableStoragePublisher(newValue, keyPaths)
238239
}
239240
}
240241
}
@@ -405,14 +406,12 @@ extension Projection: _ObservedResultsValue { }
405406
///
406407
/// Given `@ObservedResults var v` in SwiftUI, `$v` refers to a `BoundCollection`.
407408
///
408-
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
409+
@available(iOS 13.0, macOS 11.0, tvOS 13.0, watchOS 6.0, *)
409410
@propertyWrapper public struct ObservedResults<ResultType>: DynamicProperty, BoundCollection where ResultType: _ObservedResultsValue & RealmFetchable & KeypathSortable & Identifiable {
410411
private class Storage: ObservableStorage<Results<ResultType>> {
411412
var setupHasRun = false
412413
private func didSet() {
413-
if setupHasRun {
414-
setupValue()
415-
}
414+
setupValue()
416415
}
417416

418417
func setupValue() {
@@ -453,6 +452,30 @@ extension Projection: _ObservedResultsValue { }
453452
}
454453

455454
var searchString: String = ""
455+
456+
init(_ results: Results<ResultType>,
457+
configuration: Realm.Configuration? = nil,
458+
filter: NSPredicate? = nil,
459+
where: ((Query<ResultType>) -> Query<Bool>)? = nil,
460+
keyPaths: [String]? = nil,
461+
sortDescriptor: SortDescriptor? = nil) where ResultType: Object {
462+
super.init(results, keyPaths)
463+
self.configuration = configuration
464+
self.filter = filter
465+
self.where = `where`
466+
self.sortDescriptor = sortDescriptor
467+
}
468+
469+
init<ObjectType: ObjectBase>(_ results: Results<ResultType>,
470+
configuration: Realm.Configuration? = nil,
471+
filter: NSPredicate? = nil,
472+
keyPaths: [String]? = nil,
473+
sortDescriptor: SortDescriptor? = nil) where ResultType: Projection<ObjectType>, ObjectType: ThreadConfined {
474+
super.init(results, keyPaths)
475+
self.configuration = configuration
476+
self.filter = filter
477+
self.sortDescriptor = sortDescriptor
478+
}
456479
}
457480

458481
@Environment(\.realmConfiguration) var configuration
@@ -524,10 +547,10 @@ extension Projection: _ObservedResultsValue { }
524547
keyPaths: [String]? = nil,
525548
sortDescriptor: SortDescriptor? = nil) where ResultType: Projection<ObjectType>, ObjectType: ThreadConfined {
526549
let results = Results<ResultType>(RLMResults<ResultType>.emptyDetached())
527-
self.storage = Storage(results, keyPaths)
528-
self.storage.configuration = configuration
529-
self.filter = filter
530-
self.sortDescriptor = sortDescriptor
550+
self.storage = Storage(results,
551+
configuration: configuration,
552+
filter: filter,
553+
sortDescriptor: sortDescriptor)
531554
}
532555
/**
533556
Initialize a `ObservedResults` struct for a given `Object` or `EmbeddedObject` type.
@@ -547,10 +570,11 @@ extension Projection: _ObservedResultsValue { }
547570
filter: NSPredicate? = nil,
548571
keyPaths: [String]? = nil,
549572
sortDescriptor: SortDescriptor? = nil) where ResultType: Object {
550-
self.storage = Storage(Results(RLMResults<ResultType>.emptyDetached()), keyPaths)
551-
self.storage.configuration = configuration
552-
self.filter = filter
553-
self.sortDescriptor = sortDescriptor
573+
let results = Results<ResultType>(RLMResults<ResultType>.emptyDetached())
574+
self.storage = Storage(results,
575+
configuration: configuration,
576+
filter: filter,
577+
sortDescriptor: sortDescriptor)
554578
}
555579
#if swift(>=5.5)
556580
/**
@@ -571,20 +595,22 @@ extension Projection: _ObservedResultsValue { }
571595
where: ((Query<ResultType>) -> Query<Bool>)? = nil,
572596
keyPaths: [String]? = nil,
573597
sortDescriptor: SortDescriptor? = nil) where ResultType: Object {
574-
self.storage = Storage(Results(RLMResults<ResultType>.emptyDetached()), keyPaths)
575-
self.storage.configuration = configuration
576-
self.where = `where`
577-
self.sortDescriptor = sortDescriptor
598+
let results = Results<ResultType>(RLMResults<ResultType>.emptyDetached())
599+
self.storage = Storage(results,
600+
configuration: configuration,
601+
where: `where`,
602+
sortDescriptor: sortDescriptor)
578603
}
579604
#endif
580605
/// :nodoc:
581606
public init(_ type: ResultType.Type,
582607
keyPaths: [String]? = nil,
583608
configuration: Realm.Configuration? = nil,
584609
sortDescriptor: SortDescriptor? = nil) where ResultType: Object {
585-
self.storage = Storage(Results(RLMResults<ResultType>.emptyDetached()), keyPaths)
586-
self.storage.configuration = configuration
587-
self.sortDescriptor = sortDescriptor
610+
let results = Results<ResultType>(RLMResults<ResultType>.emptyDetached())
611+
self.storage = Storage(results,
612+
configuration: configuration,
613+
sortDescriptor: sortDescriptor)
588614
}
589615

590616
public mutating func update() {

0 commit comments

Comments
 (0)