利用Swift的反射机制遍历对象属性

阅读原文

前段时间写了一个macOS图床小应用cuImage,里面有些技术点一直没抽出时间总结和分享。临近毕业事情也比较多,只能挤时间了。本文将利用Swift的反射机制遍历对象属性,从而简化代码,提高代码复用率。

cuImage中每个图床都有相应的配置信息,如七牛云的图床配置信息QiniuHostInfo。我希望将其通过UserDefaults保存起来。(图床中有些敏感信息需要加密,但为了简化描述,本文就不涉及加密相关的点了,其实加密方面我也是小白菜。至于为什么不用Keychain加密就是另一回事了,这里略过。)

由于UserDefaults不支持自定义对象,如果强行直接保存,会在运行时抛出异常:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object 'Abc' for key 'Xyz'.

为了让自定义对象也能够通过UserDefaults进行持久化存储,需要通过NSKeyedArchiver将自定义对象编码成NSData格式。这就需要遵循NSCoding协议,实现该协议声明的两个方法(init(_:)encode(_:))。一般的实现方式如下:

final class QiniuHostInfo: NSObject, NSCoding {
    var name = ""
    var accessKey = ""
    var secretKey = ""
    
    init(name: String = "", accessKey: String = "", secretKey: String = "") {
        self.name = name
        self.accessKey = accessKey
        self.secretKey = secretKey
        
        super.init()
    }
    
    init?(coder aDecoder: NSCoder) {
        name = aDecoder.decodeObject(forKey: #keyPath(name)) as! String
        accessKey = aDecoder.decodeObject(forKey: #keyPath(accessKey)) as! String
        secretKey = aDecoder.decodeObject(forKey: #keyPath(secretKey)) as! String
    }
    
    func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: #keyPath(name))
        aCoder.encode(accessKey, forKey: #keyPath(accessKey))
        aCoder.encode(secretKey, forKey: #keyPath(secretKey))
    }
}

final class ImgurHostInfo: NSObject, NSCoding {
    var name = ""
    var userName = ""
    var password = ""
    
    init(name: String = "", userName: String = "", password: String = "") {
        self.name = name
        self.userName = userName
        self.password = password
        
        super.init()
    }
    
    init?(coder aDecoder: NSCoder) {
        name = aDecoder.decodeObject(forKey: #keyPath(name)) as! String
        userName = aDecoder.decodeObject(forKey: #keyPath(userName)) as! String
        password = aDecoder.decodeObject(forKey: #keyPath(password)) as! String
    }
    
    func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: #keyPath(name))
        aCoder.encode(userName, forKey: #keyPath(userName))
        aCoder.encode(password, forKey: #keyPath(password))
    }
}

这种方式的缺点在于,如果QiniuHostInfo增加新的属性或者重构代码时修改属性,需要在NSCoding声明的两个方法中都添加或修改对应的代码,还可能有疏漏。而且,当需要新增一个图床(如ImgurHostInfo)时,又得按相同的的步骤撸一遍代码。

下面是我最终采用的方式。首先,我为各种图床信息类写了个公共基类HostInfo,将实现NSCoding协议方法的任务交给了基类HostInfo,一次性搞定。以后每增加一个图床,只需要在相应的图床信息子类(如QiniuHostInfo, ImgurHostInfo)定义好自身的属性就行了,这样子类的代码就清爽多了。

class HostInfo: NSObject, NSCoding {
    var name = ""

    override init() {
        super.init()
    }
    
    convenience required init?(coder aDecoder: NSCoder) {
        self.init()
        
        forEachChildOfMirror(reflecting: self) { key in
            setValue(aDecoder.decodeObject(forKey: key), forKey: key)
        }
    }
    
    func encode(with aCoder: NSCoder) {
        forEachChildOfMirror(reflecting: self) { key in
            aCoder.encode(value(forKey: key), forKey: key)
        }
    }
    
    func forEachChildOfMirror(reflecting subject: Any, handler: (String) -> Void) {
        var mirror: Mirror? = Mirror(reflecting: subject)
        while mirror != nil {
            for child in mirror!.children {
                if let key = child.label {
                    handler(key)
                }
            }
            
            // Get super class's properties.
            mirror = mirror!.superclassMirror
        }
    }
}

final class QiniuHostInfo: HostInfo {
    var accessKey = ""
    var secretKey = ""
}

final class ImgurHostInfo: HostInfo {
    var userName = ""
    var password = ""
}

上面的代码中,init(_:)encode(_:)都需要用到反射,所以我将公共部分抽出来,并把每访问到一个属性时要做的操作以闭包的形式传给了forEachChildOfMirror(_:_:)函数。其中用到了superclassMirror是因为在父类中可能也有一些公共属性需要被遍历到,如HostInfo中的name属性。于是遍历完当前类的属性后,就可以通过superclassMirror顺着继承链往上爬,访问到继承连上的类的所有属性了。

如果读者们有其他不错的实现方式,期待你们的分享。

你可能感兴趣的:(利用Swift的反射机制遍历对象属性)