Skip to content

Commit

Permalink
Fix Observation notification not triggering on nested properties
Browse files Browse the repository at this point in the history
  • Loading branch information
dianaafanador3 committed Jul 14, 2023
1 parent 2df4354 commit 4fddfd4
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 7 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ x.y.z Release notes (yyyy-MM-dd)
* None.

### Fixed
* <How to hit and notice issue? what was the impact?> ([#????](https://github.com/realm/realm-swift/issues/????), since v?.?.?)
* None.
* Fix nested properties observation on a `Projections` not notifying when there is a property change.
([#8276](https://github.com/realm/realm-swift/issues/8276), since v10.34.0).

<!-- ### Breaking Changes - ONLY INCLUDE FOR NEW MAJOR version -->

Expand Down
12 changes: 9 additions & 3 deletions RealmSwift/Projection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -321,15 +321,21 @@ extension ProjectionObservable {

var projectedChanges = [PropertyChange]()
for i in 0..<newValues.count {
for property in schema.filter({ $0.originPropertyKeyPathString == names[i] }) {
for property in schema.filter({ prop in
guard let keyPaths = keyPaths, !keyPaths.isEmpty else {
return prop.originPropertyKeyPathString.components(separatedBy: ".").first == names[i]
}
// This will allow us to notify `PropertyChange`s associated only to the keyPaths passed by the user, instead of any Property which has the same root as the notified one.
return prop.originPropertyKeyPathString.components(separatedBy: ".").first == names[i] && keyPaths.contains(prop.originPropertyKeyPathString) }) {
// If the root is marked as a change this will build a `PropertyChange` for each of the Projection properties with the same original root, even if there is no change on their value
var changeOldValue: Any?
if oldValues != nil {
changeOldValue = unmanagedRoot![keyPath: property.projectedKeyPath]
}
let changeNewValue = object[keyPath: property.projectedKeyPath]
let changedNewValue = object[keyPath: property.projectedKeyPath]
projectedChanges.append(.init(name: property.label,
oldValue: changeOldValue,
newValue: changeNewValue))
newValue: changedNewValue))
}
}

Expand Down
103 changes: 101 additions & 2 deletions RealmSwift/Tests/ProjectionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -197,13 +197,27 @@ class FailedProjection: Projection<ModernAllTypesObject> {
public class AddressSwift: EmbeddedObject {
@Persisted var city: String = ""
@Persisted var country = ""
@Persisted var postalCode: PostalCode?
}

public class PostalCode: EmbeddedObject {
@Persisted var code: String = ""
}

public class PhoneInfo: Object {
@Persisted var mobile: Mobile?
}

public class Mobile: EmbeddedObject {
@Persisted var number: String = ""
}

public class CommonPerson: Object {
@Persisted var firstName: String
@Persisted var lastName = ""
@Persisted var birthday: Date
@Persisted var address: AddressSwift?
@Persisted var phone: PhoneInfo?
@Persisted public var friends: List<CommonPerson>
@Persisted var reviews: List<String>
@Persisted var money: Decimal128
Expand All @@ -214,6 +228,8 @@ public final class PersonProjection: Projection<CommonPerson> {
@Projected(\CommonPerson.lastName.localizedUppercase) var lastNameCaps
@Projected(\CommonPerson.birthday.timeIntervalSince1970) var birthdayAsEpochtime
@Projected(\CommonPerson.address?.city) var homeCity
@Projected(\CommonPerson.address?.postalCode?.code) var postalCode
@Projected(\CommonPerson.phone?.mobile?.number) var mobile
@Projected(\CommonPerson.friends.projectTo.firstName) var firstFriendsName: ProjectedCollection<String>
}

Expand Down Expand Up @@ -416,7 +432,11 @@ class ProjectionTests: TestCase {
let js = realm.create(CommonPerson.self, value: ["firstName": "John",
"lastName": "Snow",
"birthday": Date(timeIntervalSince1970: 10),
"address": ["Winterfell", "Kingdom in the North"],
"address": [
"city": "Winterfell",
"country": "Kingdom in the North",
"postalCode": ["code": "09133"]],
"phone": ["mobile": ["number": "555-555-555"]],
"money": Decimal128("2.22")])
let dt = realm.create(CommonPerson.self, value: ["firstName": "Daenerys",
"lastName": "Targaryen",
Expand Down Expand Up @@ -493,7 +513,7 @@ class ProjectionTests: TestCase {

func testDescription() {
let actual = populatedRealm().objects(PersonProjection.self).filter("lastName == 'Snow'").first!.description
let expected = "PersonProjection<CommonPerson> <0x[0-9a-f]+> \\{\n\t\tfirstName\\(\\\\.firstName\\) = John;\n\tlastNameCaps\\(\\\\.lastName\\) = SNOW;\n\tbirthdayAsEpochtime\\(\\\\.birthday\\) = 10.0;\n\thomeCity\\(\\\\.address.city\\) = Optional\\(\"Winterfell\"\\);\n\tfirstFriendsName\\(\\\\.friends\\) = ProjectedCollection<String> \\{\n\t\\[0\\] Daenerys\n\\};\n\\}"
let expected = "PersonProjection<CommonPerson> <0x[0-9a-f]+> \\{\n\t\tfirstName\\(\\\\.firstName\\) = John;\n\tlastNameCaps\\(\\\\.lastName\\) = SNOW;\n\tbirthdayAsEpochtime\\(\\\\.birthday\\) = 10.0;\n\thomeCity\\(\\\\.address.city\\) = Optional\\(\"Winterfell\"\\);\n\tpostalCode\\(\\\\.address.postalCode.code\\) = Optional\\(\"09133\"\\);\n\tmobile\\(\\\\.phone.mobile.number\\) = Optional\\(\"555-555-555\"\\);\n\tfirstFriendsName\\(\\\\.friends\\) = ProjectedCollection<String> \\{\n\t\\[0\\] Daenerys\n\\};\n\\}"
assertMatches(actual, expected)
}

Expand Down Expand Up @@ -945,6 +965,85 @@ class ProjectionTests: TestCase {
token.invalidate()
}

func testObserveNestedProjection() {
let realm = populatedRealm()
let johnProjection = realm.objects(PersonProjection.self).first!
let johnObject = realm.objects(CommonPerson.self).filter("lastName == 'Snow'").first!

var ex = expectation(description: "testProjectionNotificationNested")
let token = johnObject.observe(keyPaths: [\CommonPerson.phone?.mobile?.number]) { changes in
if case .change(_, let propertyChange) = changes {
XCTAssertEqual(propertyChange[0].name, "phone")
XCTAssertEqual(propertyChange[0].oldValue as? PhoneInfo, PhoneInfo?.none)
XCTAssertEqual((propertyChange[0].newValue as! PhoneInfo).mobile?.number, "529-345-678")
ex.fulfill()
} else {
XCTFail("expected .change, got \(changes)")
}
}
try! realm.write {
let johnObject = realm.objects(CommonPerson.self).filter("lastName == 'Snow'").first!
johnObject.phone?.mobile?.number = "529-345-678"
}
waitForExpectations(timeout: 2.0, handler: nil)
token.invalidate()

ex = expectation(description: "testProjectionNotificationNested")
let token2 = johnProjection.observe { changes in
if case .change(_, let propertyChange) = changes {
XCTAssertEqual(propertyChange[0].name, "mobile")
XCTAssertEqual(propertyChange[0].oldValue as? String, String?.none)
XCTAssertEqual(propertyChange[0].newValue as? String, "529-345-678")
ex.fulfill()
} else {
XCTFail("expected .change, got \(changes)")
}
}
try! realm.write {
let johnObject = realm.objects(CommonPerson.self).filter("lastName == 'Snow'").first!
johnObject.phone?.mobile?.number = "529-345-678"
}
waitForExpectations(timeout: 2.0, handler: nil)
token2.invalidate()

ex = expectation(description: "testProjectionNotificationEmbeddedNested")
let token3 = johnProjection.observe { changes in
if case .change(_, let propertyChange) = changes {
XCTAssertEqual(propertyChange[1].name, "postalCode")
XCTAssertEqual(propertyChange[1].oldValue as? String, String?.none)
XCTAssertEqual(propertyChange[1].newValue as? String, "08033")
ex.fulfill()
} else {
XCTFail("expected .change, got \(changes)")
}
}
try! realm.write {
let johnObject = realm.objects(CommonPerson.self).filter("lastName == 'Snow'").first!
johnObject.address?.postalCode?.code = "08033"
}
waitForExpectations(timeout: 2.0, handler: nil)
token3.invalidate()

ex = expectation(description: "testProjectionNotificationNestedKeyPath")
let token4 = johnProjection.observe(keyPaths: [\PersonProjection.postalCode]) { changes in
if case .change(_, let propertyChange) = changes {
XCTAssertEqual(propertyChange[0].name, "postalCode")
XCTAssertEqual(propertyChange[0].oldValue as? String, String?.none)
XCTAssertEqual(propertyChange[0].newValue as? String, "08022")
ex.fulfill()
} else {
XCTFail("expected .change, got \(changes)")
}
}
try! realm.write {
let johnObject = realm.objects(CommonPerson.self).filter("lastName == 'Snow'").first!
johnObject.address?.city = "Barranquilla"
johnObject.address?.postalCode?.code = "08022"
}
waitForExpectations(timeout: 2.0, handler: nil)
token4.invalidate()
}

var changeDictionary: [NSKeyValueChangeKey: Any]?
override func observeValue(forKeyPath keyPath: String?, of object: Any?,
change: [NSKeyValueChangeKey: Any]?,
Expand Down
10 changes: 10 additions & 0 deletions Test.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 4fddfd4

Please sign in to comment.