-
Notifications
You must be signed in to change notification settings - Fork 10.4k
/
Copy pathKVOKeyPaths.swift
221 lines (180 loc) · 7.18 KB
/
KVOKeyPaths.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
// RUN: %empty-directory(%t)
// RUN: %target-build-swift %s -o %t/a.out
// RUN: %target-codesign %t/a.out
// RUN: %target-run %t/a.out | grep 'check-prefix' > %t/prefix-option
// RUN: %target-run %t/a.out | %FileCheck -check-prefix=CHECK `cat %t/prefix-option` %s
// REQUIRES: executable_test
// REQUIRES: objc_interop
// FIXME: https://github.com/apple/swift/issues/52252
// Disable because it blocks PR testing.
// UNSUPPORTED: CPU=i386
import Foundation
struct Guts {
var internalValue = 42
var value: Int {
get {
return internalValue
}
}
init(value: Int) {
internalValue = value
}
init() {
}
}
class Target : NSObject, NSKeyValueObservingCustomization {
// This dynamic property is observed by KVO
@objc dynamic var objcValue: String
@objc dynamic var objcValue2: String {
willSet {
willChangeValue(for: \.objcValue2)
}
didSet {
didChangeValue(for: \.objcValue2)
}
}
@objc dynamic var objcValue3: String
// This Swift-typed property causes vtable usage on this class.
var swiftValue: Guts
override init() {
self.swiftValue = Guts()
self.objcValue = ""
self.objcValue2 = ""
self.objcValue3 = ""
super.init()
}
static func keyPathsAffectingValue(for key: AnyKeyPath) -> Set<AnyKeyPath> {
if (key == \Target.objcValue) {
return [\Target.objcValue2, \Target.objcValue3]
} else {
return []
}
}
static func automaticallyNotifiesObservers(for key: AnyKeyPath) -> Bool {
if key == \Target.objcValue2 || key == \Target.objcValue3 {
return false
}
return true
}
func print() {
Swift.print("swiftValue \(self.swiftValue.value), objcValue \(objcValue)")
}
}
class ObserverKVO : NSObject {
var target: Target?
var observation: NSKeyValueObservation? = nil
override init() { target = nil; super.init() }
func observeTarget(_ target: Target) {
self.target = target
observation = target.observe(\.objcValue) { (object, change) in
Swift.print("swiftValue \(object.swiftValue.value), objcValue \(object.objcValue)")
}
}
func removeTarget() {
observation!.invalidate()
}
}
var t2 = Target()
var o2 = ObserverKVO()
print("unobserved 2")
t2.objcValue = "one"
t2.objcValue = "two"
print("registering observer 2")
o2.observeTarget(t2)
print("Now witness the firepower of this fully armed and operational panopticon!")
t2.objcValue = "three"
t2.objcValue = "four"
t2.swiftValue = Guts(value: 13)
t2.objcValue2 = "six" //should fire
t2.objcValue3 = "nothing" //should not fire
o2.removeTarget()
t2.objcValue = "five" //make sure that we don't crash or keep posting changes if you deallocate an observation after invalidating it
print("target removed")
// CHECK: registering observer 2
// CHECK-NEXT: Now witness the firepower of this fully armed and operational panopticon!
// CHECK-NEXT: swiftValue 42, objcValue three
// CHECK-NEXT: swiftValue 42, objcValue four
// CHECK-NEXT: swiftValue 13, objcValue four
// The next 2 logs are actually a bug and shouldn't happen
// CHECK-NEXT: swiftValue 13, objcValue four
// CHECK-NEXT: swiftValue 13, objcValue four
// CHECK-NEXT: target removed
//===----------------------------------------------------------------------===//
// Test NSKeyValueObservingCustomization issue with observing from the callbacks
//===----------------------------------------------------------------------===//
// The following tests are only expected to pass when running with the
// Swift 5.1 and later libraries.
if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) {
print("-check-prefix=CHECK-51")
} else {
print("-check-prefix=DONT-CHECK")
// Need at least one check, otherwise FileCheck will complain.
// DONT-CHECK: {{.}}
}
class Target2 : NSObject, NSKeyValueObservingCustomization {
@objc dynamic var name: String?
class Dummy : NSObject {
@objc dynamic var name: String?
}
// In both of the callbacks, observe another property with the same key path.
// We do it in both because we're not sure which callback is invoked first.
// This ensures that using KVO with key paths from one callback doesn't interfere
// with the ability to look up the key path using the other.
static func keyPathsAffectingValue(for key: AnyKeyPath) -> Set<AnyKeyPath> {
print("keyPathsAffectingValue: key == \\.name:", key == \Target2.name)
withExtendedLifetime(Dummy()) { (dummy) in
_ = dummy.observe(\.name) { (_, _) in }
}
return []
}
static func automaticallyNotifiesObservers(for key: AnyKeyPath) -> Bool {
print("automaticallyNotifiesObservers: key == \\.name:", key == \Target2.name)
withExtendedLifetime(Dummy()) { (dummy) in
_ = dummy.observe(\.name) { (_, _) in }
}
return true
}
}
print("registering observer for Target2")
withExtendedLifetime(Target2()) { (target) in
_ = target.observe(\.name) { (_, _) in }
}
print("observer removed")
// CHECK-51-LABEL: registering observer for Target2
// CHECK-51-DAG: keyPathsAffectingValue: key == \.name: true
// CHECK-51-DAG: automaticallyNotifiesObservers: key == \.name: true
// CHECK-51-NEXT: observer removed
//===----------------------------------------------------------------------===//
// Test NSSortDescriptor keyPath support
//===----------------------------------------------------------------------===//
// This one doesn't really match the context of "KVO KeyPaths" but it's close enough
class Sortable1 : NSObject {
@objc var name: String?
}
class Sortable2 : NSObject {
@objc var name: String?
}
print("creating NSSortDescriptor")
let descriptor = NSSortDescriptor(keyPath: \Sortable1.name, ascending: true)
_ = NSSortDescriptor(keyPath: \Sortable2.name, ascending: true)
print("keyPath == \\Sortable1.name:", descriptor.keyPath == \Sortable1.name)
// CHECK-51-LABEL: creating NSSortDescriptor
// CHECK-51-NEXT: keyPath == \Sortable1.name: true
//===----------------------------------------------------------------------===//
// Test keyPath with optional value has correct oldValue/newValue behavior
//===----------------------------------------------------------------------===//
class TestClassForOptionalKeyPath : NSObject {
// Should not use NSObject? as object type
@objc dynamic var optionalObject: String?
}
let testObjectForOptionalKeyPath = TestClassForOptionalKeyPath()
print("observe keyPath with optional value")
let optionalKeyPathObserver = testObjectForOptionalKeyPath.observe(\.optionalObject, options: [.initial, .old, .new]) { (_, change) in
Swift.print("oldValue = \(change.oldValue as String??), newValue = \(change.newValue as String??)")
}
testObjectForOptionalKeyPath.optionalObject = nil
testObjectForOptionalKeyPath.optionalObject = "foo"
// CHECK-51-LABEL: observe keyPath with optional value
// CHECK-51-NEXT: oldValue = {{Optional\(nil\)|nil}}, newValue = {{Optional\(nil\)|nil}}
// CHECK-51-NEXT: oldValue = {{Optional\(nil\)|nil}}, newValue = {{Optional\(nil\)|nil}}
// CHECK-51-NEXT: oldValue = {{Optional\(nil\)|nil}}, newValue = Optional(Optional("foo"))