Skip to content

Commit f34ef5d

Browse files
authored
Merge pull request #24356 from zetasq/zetasq-fix-KVO
2 parents 3fdb42f + 4511222 commit f34ef5d

File tree

2 files changed

+61
-2
lines changed

2 files changed

+61
-2
lines changed

stdlib/public/Darwin/Foundation/NSObject.swift

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,17 @@ public class NSKeyValueObservation : NSObject {
233233
}
234234
}
235235

236+
// Used for type-erase Optional type
237+
private protocol _OptionalForKVO {
238+
static func _castForKVO(_ value: Any) -> Any?
239+
}
240+
241+
extension Optional: _OptionalForKVO {
242+
static func _castForKVO(_ value: Any) -> Any? {
243+
return value as? Wrapped
244+
}
245+
}
246+
236247
extension _KeyValueCodingAndObserving {
237248

238249
///when the returned NSKeyValueObservation is deinited or invalidated, it will stop observing
@@ -242,9 +253,30 @@ extension _KeyValueCodingAndObserving {
242253
changeHandler: @escaping (Self, NSKeyValueObservedChange<Value>) -> Void)
243254
-> NSKeyValueObservation {
244255
return NSKeyValueObservation(object: self as! NSObject, keyPath: keyPath, options: options) { (obj, change) in
256+
257+
let converter = { (changeValue: Any?) -> Value? in
258+
if let optionalType = Value.self as? _OptionalForKVO.Type {
259+
// Special logic for keyPath having a optional target value. When the keyPath referencing a nil value, the newValue/oldValue should be in the form .some(nil) instead of .none
260+
// Solve https://bugs.swift.org/browse/SR-6066
261+
262+
// NSNull is used by KVO to signal that the keyPath value is nil.
263+
// If Value == Optional<T>.self, We will get nil instead of .some(nil) when casting Optional(<null>) directly.
264+
// To fix this behavior, we will eliminate NSNull first, then cast the transformed value.
265+
266+
if let unwrapped = changeValue {
267+
// We use _castForKVO to cast first.
268+
// If Value != Optional<NSNull>.self, the NSNull value will be eliminated.
269+
let nullEliminatedValue = optionalType._castForKVO(unwrapped) as Any
270+
let transformedOptional: Any? = nullEliminatedValue
271+
return transformedOptional as? Value
272+
}
273+
}
274+
return changeValue as? Value
275+
}
276+
245277
let notification = NSKeyValueObservedChange(kind: change.kind,
246-
newValue: change.newValue as? Value,
247-
oldValue: change.oldValue as? Value,
278+
newValue: converter(change.newValue),
279+
oldValue: converter(change.oldValue),
248280
indexes: change.indexes,
249281
isPrior: change.isPrior)
250282
changeHandler(obj as! Self, notification)

test/stdlib/KVOKeyPaths.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,30 @@ print("keyPath == \\Sortable1.name:", descriptor.keyPath == \Sortable1.name)
191191

192192
// CHECK-51-LABEL: creating NSSortDescriptor
193193
// CHECK-51-NEXT: keyPath == \Sortable1.name: true
194+
195+
//===----------------------------------------------------------------------===//
196+
// Test keyPath with optional value has correct oldValue/newValue behavior
197+
//===----------------------------------------------------------------------===//
198+
199+
class TestClassForOptionalKeyPath : NSObject {
200+
201+
// Should not use NSObject? as object type
202+
@objc dynamic var optionalObject: String?
203+
204+
}
205+
206+
let testObjectForOptionalKeyPath = TestClassForOptionalKeyPath()
207+
208+
print("observe keyPath with optional value")
209+
210+
let optionalKeyPathObserver = testObjectForOptionalKeyPath.observe(\.optionalObject, options: [.initial, .old, .new]) { (_, change) in
211+
Swift.print("oldValue = \(change.oldValue as String??), newValue = \(change.newValue as String??)")
212+
}
213+
214+
testObjectForOptionalKeyPath.optionalObject = nil
215+
testObjectForOptionalKeyPath.optionalObject = "foo"
216+
217+
// CHECK-LABEL: observe keyPath with optional value
218+
// CHECK-NEXT: oldValue = Optional(nil), newValue = Optional(nil)
219+
// CHECK-NEXT: oldValue = Optional(nil), newValue = Optional(nil)
220+
// CHECK-NEXT: oldValue = Optional(nil), newValue = Optional(Optional("foo"))

0 commit comments

Comments
 (0)