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