Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix SwiftUI performance issues #7686

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
72 changes: 49 additions & 23 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,30 @@ extension Projection: _ObservedResultsValue { }
}

var searchString: String = ""

init(_ results: Results<ResultType>,
configuration: Realm.Configuration? = nil,
filter: NSPredicate? = nil,
where: ((Query<ResultType>) -> Query<Bool>)? = 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<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 +547,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 +570,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 All @@ -571,20 +595,22 @@ extension Projection: _ObservedResultsValue { }
where: ((Query<ResultType>) -> Query<Bool>)? = nil,
keyPaths: [String]? = nil,
sortDescriptor: SortDescriptor? = nil) where ResultType: Object {
self.storage = Storage(Results(RLMResults<ResultType>.emptyDetached()), keyPaths)
self.storage.configuration = configuration
self.where = `where`
self.sortDescriptor = sortDescriptor
let results = Results<ResultType>(RLMResults<ResultType>.emptyDetached())
self.storage = Storage(results,
configuration: configuration,
where: `where`,
sortDescriptor: sortDescriptor)
}
#endif
/// :nodoc:
public init(_ type: ResultType.Type,
keyPaths: [String]? = nil,
configuration: Realm.Configuration? = nil,
sortDescriptor: SortDescriptor? = nil) where ResultType: Object {
self.storage = Storage(Results(RLMResults<ResultType>.emptyDetached()), keyPaths)
self.storage.configuration = configuration
self.sortDescriptor = sortDescriptor
let results = Results<ResultType>(RLMResults<ResultType>.emptyDetached())
self.storage = Storage(results,
configuration: configuration,
sortDescriptor: sortDescriptor)
}

public mutating func update() {
Expand Down