Skip to content

Commit

Permalink
Solve SwiftUI performance issues
Browse files Browse the repository at this point in the history
  • Loading branch information
dianaafanador3 committed Mar 3, 2022
1 parent c6d8ff6 commit da62d08
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions Realm/Tests/SwiftUITestHost/Objects.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Reminder>()
}
61 changes: 61 additions & 0 deletions Realm/Tests/SwiftUITestHost/SwiftUITestHostApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import RealmSwift
import SwiftUI
import Realm.Private

struct ReminderFormView: View {
@ObservedRealmObject var reminder: Reminder
Expand Down Expand Up @@ -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 {
Expand All @@ -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())
}
Expand All @@ -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
}
}
22 changes: 22 additions & 0 deletions Realm/Tests/SwiftUITestHostUITests/SwiftUITestHostUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
8 changes: 5 additions & 3 deletions RealmSwift/Combine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -721,7 +723,7 @@ extension RealmKeyedCollection {

/// Stop emitting values on this subscription.
public func cancel() {
token.invalidate()
token?.invalidate()
}
}

Expand Down
54 changes: 38 additions & 16 deletions RealmSwift/SwiftUI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ private final class ObservableStoragePublisher<ObjectType>: 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
Expand All @@ -222,6 +222,9 @@ private final class ObservableStoragePublisher<ObjectType>: 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())
}
}
}
Expand All @@ -231,10 +234,8 @@ private class ObservableStorage<ObservedType>: 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)
}
}
}
Expand Down Expand Up @@ -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<ResultType>: DynamicProperty, BoundCollection where ResultType: _ObservedResultsValue & RealmFetchable & KeypathSortable & Identifiable {
private class Storage: ObservableStorage<Results<ResultType>> {
var setupHasRun = false
private func didSet() {
if setupHasRun {
setupValue()
}
setupValue()
}

func setupValue() {
Expand Down Expand Up @@ -453,6 +452,28 @@ extension Projection: _ObservedResultsValue { }
}

var searchString: String = ""

init(_ results: Results<ResultType>,
configuration: Realm.Configuration? = nil,
filter: NSPredicate? = nil,
keyPaths: [String]? = nil,
sortDescriptor: SortDescriptor? = nil) where ResultType: Object {
super.init(results, keyPaths)
self.configuration = configuration
self.filter = filter
self.sortDescriptor = sortDescriptor
}

init<ObjectType: ObjectBase>(_ results: Results<ResultType>,
configuration: Realm.Configuration? = nil,
filter: NSPredicate? = nil,
keyPaths: [String]? = nil,
sortDescriptor: SortDescriptor? = nil) where ResultType: Projection<ObjectType>, ObjectType: ThreadConfined {
super.init(results, keyPaths)
self.configuration = configuration
self.filter = filter
self.sortDescriptor = sortDescriptor
}
}

@Environment(\.realmConfiguration) var configuration
Expand Down Expand Up @@ -524,10 +545,10 @@ extension Projection: _ObservedResultsValue { }
keyPaths: [String]? = nil,
sortDescriptor: SortDescriptor? = nil) where ResultType: Projection<ObjectType>, ObjectType: ThreadConfined {
let results = Results<ResultType>(RLMResults<ResultType>.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.
Expand All @@ -547,10 +568,11 @@ extension Projection: _ObservedResultsValue { }
filter: NSPredicate? = nil,
keyPaths: [String]? = nil,
sortDescriptor: SortDescriptor? = nil) where ResultType: Object {
self.storage = Storage(Results(RLMResults<ResultType>.emptyDetached()), keyPaths)
self.storage.configuration = configuration
self.filter = filter
self.sortDescriptor = sortDescriptor
let results = Results<ResultType>(RLMResults<ResultType>.emptyDetached())
self.storage = Storage(results,
configuration: configuration,
filter: filter,
sortDescriptor: sortDescriptor)
}
#if swift(>=5.5)
/**
Expand Down

0 comments on commit da62d08

Please sign in to comment.