preface
Just learned swift programming language, here directly to do a SWIFT version of KVO, by the way familiar with swift
It describes the basic use of KVO in Swift, imitating the version of KVOController, as well as the Swift attribute packaging trial
In the process of writing, I also encountered some problems in the process of combining SWIFT and OC apis (after all, UIKit still uses OC, and some OC classes can also be used), so I would like to share with you
Case demo
KVO
KVO is basically one of the necessary skills in development, and is common in some content updates (modifying personal information, liking information, etc.).
Because Swift still uses the UIKit framework of Object-C language, there are some problems in adding UI and click events. Moreover, due to programming language limitations, SEL cannot be used as object-C when adding click events. Instead, we set the pointer call using #Selector() if needed
let btn = UIButton(frame: CGRect(x: 0.y: 100.width: self.view.bounds.size.width, height: 40))
btn.setTitle("Click test KVO".for: .normal)
btn.setTitleColor(UIColor.black, for: .normal)
btn.addTarget(self, action: #selector(self.onClickToModify), for: .touchUpInside)
self.view.addSubview(btn)
Copy the code
And since Swift is still using the UIKit framework (written in Object-C), the function needs to be marked @objc to be accessible
@objc func onClickToModify(){ baseModel? .age =200
}
Copy the code
Basic KVO
Before looking at the other KVos, take a look at the basic uses of the basic KVO
The model type to be observed is shown below and needs to be inherited from NSObject, with the @objc Dynamic identifier, or object-c identifier, added to the property
class KVOBaseTestModel: NSObject {
// The @objc dynamic parameter must be added to support listening
// Since there are no optional types in OC, an error will be reported if there are optional types in base datatypes. After all, base datatypes cannot be assigned to nil
@objc dynamic var age: Int = 0
@objc dynamic var name: String?
}
Copy the code
Add the listener method addObsercer and the callback function observeValue
// Add a listening method
baseModel.addObserver(self, forKeyPath: "name".options: [.new, .old], context: nil)
NSKeyValueChangeKey allows access to data in the corresponding dictionary change
override func observeValue(forKeyPath keyPath: String? .ofobject: Any? , change: [NSKeyValueChangeKey : Any]? , context: UnsafeMutableRawPointer?) {
print(change as Any)
}
Copy the code
KVOController
KVOController here is written in imitation of Object-C KVOController, which reduces the use of singletons to deal with monitoring and events in a unified manner. It is not necessary for my personal feeling
When releasing an object, the parent object is first released, and then the child object is released. Therefore, KVOController always exists as a child attribute of the class. When the class holding KVOController is released, the listener will be automatically released
Note: Depending on the release time, it is best to use one KVOController instance variable for one controller page or interaction scenario, and avoid using one for multiple controllers unless necessary, and the object model properties under observation should still have the @objC Dynamic field
KVOController is implemented as follows:
__LLKVOInfo
First, I define a basic __LLKVOInfo data structure that holds listeners, callbacks, and key values for subsequent callbacks and removals
This inherits NSObject and implements hash and isEqual, which are the methods you need to compare values in an NSSet
class __LLKVOInfo: NSObject {
weak var observer: AnyObject?
var block: LLBlock
var keyPath: String
// Block needs to be set to escaping, after all it is not executed directly, it is saved
init(observer: AnyObject, block: @escaping LLBlock, keyPath: String) {
self.observer = observer
self.block = block
self.keyPath = keyPath
}
func hash() -> Int {
return Int(self.keyPath.hash)
}
override func isEqual(_ object: Any?) -> Bool {
if let obj = object as? __LLKVOInfo {
if obj.keyPath == self.keyPath {
return true}}return false}}Copy the code
LLKVOController
LLKVOController is the core class of observation. Our KVOController is an active observation class, which realizes the automatic release of KVO logic by actively observing the observed
Since the observed has multiple key values, and there may be multiple observed, the observed instance is taken as the key value when saving the observer information, and the observed multiple key values are stored in a set, and repeated listening is guaranteed
After consideration, the NSMapTable hash table in Object-C is used. As for Dictionary in Swift and Dictionary in object-C, after all, not every observed Object complies with the hash protocol (for example: Hashable protocol in Swift)
NSMapTable does not need to consider so much, can use object pointer as hash key, so more practical, and value stores __LLKVOInfo type, so can use NSSet, swift Set is a structure, so not suitable. NSSet addresses repeated hashes and isEqual, so it overwrites both methods (you can also use NSDictionary, which is better, but this is just an example).
As shown below, an NSMapTable and a lock are initialized. Not to mention locks. To ensure thread safety, NSMapTable is shown below
// The default strong reference, which refers to the observed, is released and removed only when KVOController is released
// Set the weak reference observer, the weak reference is released by the observer can not remove the listening, if the singleton, may occur multiple listening problem
// Setting weak references applies to views that refresh data frequently to reduce memory overhead
// Weak references are not actually recommended, although it doesn't matter if they are not released, but if the system caches the newly generated listening subclass
// There may be additional performance overhead if the listening attribute is not released
init(_ isWeakObserved: Bool = false) {
infosMap = NSMapTable(keyOptions:
[isWeakObserved ? .weakMemory : .strongMemory, .objectPointerPersonality],
valueOptions: [.strongMemory, .objectPointerPersonality])
semaphore = DispatchSemaphore(value: 1)}Copy the code
The code to observe is shown below
func observer(_ observedObj: AnyObject, _ keyPath: String, _ block: @escaping LLBlock) {
// Create an object of basic type and use it to save data and compare data
let info = __LLKVOInfo(observer: observedObj, block: block, keyPath: keyPath)
semaphore.wait()
// Get the value collection of the specified object
var infoSet = infosMap.object(forKey: observedObj)
if let set = infoSet {
// Add a listener
if set.contains(info) {
// Observations have been added, no more
semaphore.signal()
return}}else {
// Create a set and add it to InfosMap
infoSet = NSMutableSet()
infosMap.setObject(infoSet, forKey: observedObj)
}
// Add a new listenerinfoSet! .add(info) semaphore.signal()// Add observations
observedObj.addObserver(self, forKeyPath: keyPath, options: [.new, .old], context: nil)
}
Copy the code
ObserveValue, the UnsafeMutableRawPointer type is not very useful and can only be used for one lookup (NSSet instead of NSDictionary).
override func observeValue(forKeyPath keyPath: String? .ofobject: Any? , change: [NSKeyValueChangeKey : Any]? , context: UnsafeMutableRawPointer?) {
let obj = object as AnyObject
if let infoSet = self.infosMap.object(forKey: obj) {
for info in infoSet {
if let info = info as? __LLKVOInfo {
if(info.keyPath == keyPath) { info.block(change? [NSKeyValueChangeKey.newKey]as Any, obj)
return
}
}
}
}
}
Copy the code
When our KVOController is released, actively release and remove all listeners in it
deinit {
for observer in self.infosMap.keyEnumerator() {
let observed = observer as AnyObject
let obj = self.infosMap.object(forKey: observed)
for info in obj! {
observed.removeObserver(self, forKeyPath: (info as! __LLKVOInfo).keyPath)
}
}
print("KVOController released.")}Copy the code
Swift attribute wrapped lightweight KVO
A relatively useful KVOController is introduced above, which uses the Swift property wrapper to solve the problem of KVO.
Advantages: Minimal code, high performance, easy to use, and no @objc Dynamic attributes, just the normal use of attribute wrapping
Disadvantages: can not be monitored by multiple objects at the same time, applicable to a key value, a monitoring KVO use, can be improved
The implementation code is as follows:
typealias LLObserverBlock = (Any) -> Void
@propertyWrapper
struct LLObserver<T> {
private var observerValue: T? // Remember to set the initial value, which can also be set by init
private var block: LLObserverBlock?
// Default attribute wrapper name, parameter name fixed
var wrappedValue: T? {
get {observerValue}
set {
observerValue = newValue
if let b = block {
b(newValue as Any)
}
}
}
$number = $obj.$number = $obj.$number
var projectedValue: LLObserverBlock {
get {
block!
}
set {
block = newValue
}
}
init() {
observerValue = nil
block = nil
}
}
Copy the code
It is simpler to use, as shown below, to listen successfully
observerModel = KVOObserverTestModel()
// Set the listenerobserverModel? .$name = { newValuein
print("name", newValue) } observerModel? .$age = { newValuein
print("age", newValue)
}
Copy the code
Be careful to use attribute wrapping
class KVOObserverTestModel: NSObject {
@LLObserver
var age: UInt?
@LLObserver
var name: String?
}
Copy the code
The last
This is a swift function test, and combined with some common classes of Object-C (most of the classes of Object-C can still be used normally), and found that swift basic content function is not enough (there are some version problems at present, only swift new features are not enough), Some basic functions in Object-C are still needed to complete. Therefore, to develop ios well, it is not enough to only know swift programming language, but also to understand object-C, in order to better progress