iOS开发技巧系列---打造强大的BaseModel(篇一:让Model自我描述)

摘要: 从事iOS开发已经两年了,从一无所知到现在能独立带领团队完成一系列APP的开发,网络上的大神给了我太多的帮助。他们无私地贡献自己的心得和经验,写出了一篇篇精美的文章。现在我也开始为大家贡献自己的心得,把它写成一系列iOS开发技巧系列文章。
这一系列文章都干货十足,希望各位读者可以积极留言,和我沟通。

2018年Swift4已经发布,现在需要更新这些文章了,里面的代码可能都跑不起了。所以我要修正这些代码让其跑起来。我把这些代码都放在iOSDemo项目里
https://github.com/DuckDeck/iOSDemo

何为Model?

Model就是MVC和MVVM最前面的M,显然Model的重要性不言而喻。只有在将网络&数据库获取的数据正确转化成Model后,才能更好地服务ViewController和View。通常--Model
是应用逻辑层的对象,如 Account、Order 等等。这些对象是你开发的应用程序中的一些核心对象,负责应用的逻辑计算和诸多与业务相关的方法和操作。首先Model将未处理的数据转化成Model后,再传给ViewController,再传给ViewController再将处理好的Model数据显示到View上去。相反View产生的数据可也可以转化为Model,通过ViewConroller传到Model层处理后再保存&更新。在iOS开发中,Model还可以分为胖Model和瘦Model。当然,这些东西都不在本文的讨论范围之内。本文讨论的是如何增强Model的一些功能,这些功能并不是业务逻辑上的功能,而是让Model可以自动实现一些代码层面的功能。可以降低我们的代码量,大量减少重复的代码。

让Model实现自我描述

众所周知,利用iOS的NSLog和print功能是可以打印iOS的任意对象的。但是对于自定义对象,打印出来的却是一连串的数字,这串数字就是该对象的内存地址(Objc),如果是用Swift,就会打印出来对象的类名。

class demoClass{                   //定义一个demoClass对象
    var a = 1
    var b = "demo"
}                           
print(demoClass())                    //打印出来 
"ConsoleSwift.demoClass"       //swift打印出了命名空间类名
//那么为什么Swift打印出了类名而不是类的内存地址呢?
//实际上,打印出对象的地址是Objective C对象的一个功能。
//单纯的Swift对象并非由NSObject派生,只能打印出类名


class demoClassFromNSObject:NSObject{ //定义一个demoClassFromNSObject对象
    var a = 1
    var b = "demo"
}
print(demoClassFromNSObject())  //打印出来
<ConsoleSwift.demoClassFromNSObject: 0x101846d70>  //对于由NSObject派生的类,打印出了内存地址

显然这串的数字或者是类名对我们来说毫无用处,正常情况下,我们需要看的是这个对象所有属性的数据。在Objc里面,直接在自定义类里重写description方法就行,当你打印对象时,运行时会自动调用对象的description方法。

-(NSString)descprition
{
    return 你对自定义类的各个变量的描述,从而可以打印出来
}

但是在Swift语言,情况变得不一样了。Swift并不存在descprition方法,那么Swift是怎么实现的呢?

Swift中有一系列协议.其中以Custom开头的协议(目前共有5个):

CustomReflectable
CustomLeafReflectable
CustomPlaygroundQuickLookable
CustomStringConvertible
CustomDebugStringConvertible

这些协议表示自定义一个方法(其实并不是方法,后面会说到这是一个属性),这个方法是用来将对象转化为可以打印出来的字符串或者可视化的图形等。

其中协议CustomStringConvertible和CustomDebugStringConvertible就是相当于Objc的实现descprition方法的协议(其他协议可以看官方文档),
让自定义的类继承这两个协议后就需要重写description属性和debugDescription属性(所以前面有提到这不是一个方法)

class demoClass{
    var demoId:Int = 0
    var demoName:String?
}
//实现协议可以在Extension里进行
extension demoClass:CustomStringConvertible{
    var description:String{        //重写description,注意,因为这个类没有父类,所以不需要加上override
        return "DemoClass: demoId:\(demoId) demoName:\(demoName ?? "nil")"
    }
}
extension demoClass:CustomDebugStringConvertible{
    var debugDescription:String{
        return self.description
    }
}

let demo1 = demoClass()
print(demo1)
"DemoClass: demoId:0 demoName:nil\n"//打印的结果,因没有设置demoName所以为nil
demo1.demoName = "this is a demo"
print(demo1)
"DemoClass: demoId:0 demoName:this is a demo\n"//打印出了自己在里面写的属性

细心的同学可能注意到了CustomStringConvertible对应的应该是属性description,而CustomDebugStringConvertible对应的是debugDescription属性.那么debugDescription有什么用呢? debugDescription是在调试时你可以用po命令来打印对象,所以在这里我让它直接返回description就行了。

这里有一点需要注意,如果你的类是继承了NSObject的话,那么就不需要再继承CustomStringConvertible了,因为NSObject已经继承这个协议了,所以只要重写description属性就行了。

class DemoClassA: NSObject {  //继承了NSObject
    var demoId:Int = 0
    var demoName:String?
    override var description:String{
        return "DemoClassA: demoId:\(demoId) demoName:\(demoName ?? "nil")"
    }
}
let demo2 = DemoClassA()
demo2.demoName = "DemoClassA"
print(demo2)        //“DemoClassA: demoId:0 demoName:DemoClassA"

好了,怎么实现对象的自我描述很清楚了,但下一个问题又来了.一个项目里面通常会有十几个甚至几十个Model,如果每个Model都这样重写description属性是件极耗精力的事情.这需要重复写大量相似的代码,显然不这不可取的。那么有没有办法可以直接让Model自我描述呢? 答案是有的。通过反射的方法或者在运行时可以找到Model的所有属性,再通过KVC给这些属性赋值就可以打印出来了。再将所有的属性名的其对应的值保存到字典里。再把字典按照某种格式转化为String就完成了。

当然这里有一个局限性,就是单纯的Swift类是没有KVC的,你需要让它继承NSObject就有这个功能。因为只有Objctive C才有运行时这一套东西。如果让Swift中加入Objc运行时,Swift的效率会有降低。这就要看自己的取舍了。

下面直接上代码

//先定义Model
class GrandModel:NSObject{
//这里不定义任何属性,所有用的属性都在子类,直接重写description
     internal override var description:String{
        get{
            var dict = [String:AnyObject]()
            var count:UInt32 =  0
            let vars = class_copyIvarList(type(of: self), &count)
            for i in 0..<count {
                let t = ivar_getName((vars?[Int(i)])!)
                if let n = NSString(cString: t!, encoding: String.Encoding.utf8.rawValue) as String?
                {
                    let v = self.value(forKey: n ) ?? "nil"  //在Swift4会出错:this class is not key value coding-compliant for the key
                    //原因是因为在Swift 4中继承 NSObject 的 swift class 不再默认全部 bridge 到 OC。也就是说如果我们想要使用KVC的话我们就需要加上@objcMembers 这么一个关键字
                    dict[n] = v as AnyObject?
                }
            }
            free(vars)
            return "\(type(of: self)):\(dict)"
        }
    }

}

接下来写一个测试Model继承于GrandModel

@objcMembers class TestModel: GrandModel {
  var i = 0
  var a:String?
}
let model = TestModel()

print(model)  //TestModel:["a": nil, "i": 0]\n
model.a = "aaa"
print(model)//TestModel:["a": aaa, "i": 0]\n

注意,在Swift4里面已经不再推荐使用Objective C的KVC了。Swift4的新标准请参考https://www.jianshu.com/p/c4f5db08bcab

可见,结果完全符合我们需要的效果。所有的字段都可以成功打印出来,那么我再深入一下,如果TestModel里有一个属性是Enum,或者是其他的非Objc支持的运行时类型,会出现什么情况呢?

我们先定义一个枚举,并且把i改成Int?的类型,再加一个有初始值的Int类型

enum week{
    case Mon,Thu,Wed,Tur,Fri,Sai,Sun
}
//TestModel加入枚举
class TestModel: GrandModel {
    var i:Int?
    var j = 1
    var a:String?
    var weeb:week?
}
let model = TestModel()
print(model)
//打印结果
//TestModel:["j": 1, "a": nil]
// 在Swift4已经不适用,会报错

以上代码在Swift4已经不适用,会报错

这个结果有点让人奇怪? 运行时找不到这两个属性?可以分析一下,我们定义的这个枚举是个纯粹的Swift枚举,而Int?类型也无法在Objc里面用正确的类型来表示。那为什么String?可以被Objc运行时正确地识别呢?所以一个大的问题出来了,Apple是怎么设定Swift类型到Objc类型的映射关系的?
关于这个问题,我想到了下面的方法

不再使用PlayGround来验证,新建立一个Command Line项目,默认语言设为Swift,然后再添加一个Objc类,如图所示

Xcode

注意在Objc的类加入Swift的头文件,其格式是[项目名]-Swift.h,然后进入这个文件,可以很容易找到定义在Main.swift里的TestModel类

SWIFT_CLASS("_TtC11DemoConsole9TestModel")
@interface TestModel : GrandModel
@property (nonatomic) NSInteger j;
@property (nonatomic, copy) NSString * __nullable a;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder * __nonnull)aDecoder OBJC_DESIGNATED_INITIALIZER;
@end

这下就可以看得一清二楚了,被转成Objc类后,只有两个属性,另外两个全没了。所以在运行时找不到这两个属性,也就无法打印了。

我给上面的文字都加上删除线,因为这些在Swift4已经不适用了。实事上在Swift4里面已经可以正确地识别出这几非Objc类型了,但是这些类型不能用在KVC上面。其实感觉现在Apple不推荐在Swift4使用这些运行时API了,我以后会再写文章来解决这个问题

关于更多的Swift类型到Objc类型的映射关系这里就不多说了,有兴趣的同学可以用XCode调试,相信你会有大收获的。

另外一个问题就是如果这个类中的属性是另一个类怎么办?或者是个Array,Dict呢,其实很简单,只要这个属性也继承了GrandModel,都可是顺利打印出来

struct StructDemo {
    var q = 1
    var w = "w"
}
class ClassDemo {
    var q = 1
    var w = "w"
}
@objcMembers class ClassDemoA:GrandModel{
    var q = 1
    var w = "w"
}
@objcMembers class TestModelA: GrandModel {
    var i:Int = 1
    var o:String?
    //var structDemo:StructDemo? //this class is not key value coding-compliant for the key structDemo.'
    //var classDemo:ClassDemo?  //this class is not key value coding-compliant for the key classDemo.'
    var classDemoA:ClassDemoA?
    var classDemoAArray:[ClassDemoA]?
    var classDemoDict:[String:ClassDemoA]?
}
let modelA = TestModelA()
modelA.classDemoAArray = [ClassDemoA]()
modelA.classDemoAArray?.append(ClassDemoA())
modelA.classDemoAArray?.append(ClassDemoA())
modelA.classDemoDict = [String:ClassDemoA]()
modelA.classDemoDict!["1"] = ClassDemoA()
modelA.classDemoDict!["2"] = ClassDemoA()
print(modelA)
/*TestModelA:["o": nil, "classDemoA": ClassDemoA:["q": 1, "w": w], "i": 1, "classDemoAArray": (
    "ClassDemoA:[\"q\": 1, \"w\": w]",
    "ClassDemoA:[\"q\": 1, \"w\": w]"
), "classDemoDict": {
    1 = "ClassDemoA:[\"q\": 1, \"w\": w]";
    2 = "ClassDemoA:[\"q\": 1, \"w\": w]";
}]
*/
// 上面的结果已经不能打出

/*
TestModelA:["classDemoA": nil, "o": nil, "i": 1, "classDemoDict": {
    1 = "ClassDemoA:[\"q\": 1, \"w\": w]";
    2 = "ClassDemoA:[\"q\": 1, \"w\": w]";
}, "classDemoAArray": <_TtGCs23_ContiguousArrayStorageC12ConsoleSwift10ClassDemoA_ 0x101a80bc0>(
ClassDemoA:["q": 1, "w": w],
ClassDemoA:["q": 1, "w": w]
)
]*/
//这是Swift4的打印结果

可见所有的属性都正确地打印出来了。

因为Swift4可以获取有的属性,所以ClassDemo和StructDemo的Key可以获取到的,但是对于这两个类(结构),是不能使用KVC的,所以就出错了。

结语:让iOS的Model拥有自我描述的功能,可以在调试DeBug中发挥非常大的作用。也让我们看到了单纯的Swift类和Objc的的一套运行时机制完全不同的。不过目前iOS开发还是脱离不了Objc运行时,所以虽然相比较于单纯的Swift类,Objc运行时会有性能损失,但是还是可以完全接受的
在Swift4的Mirror反射机制差不多成熟了,这个可以参考第四章。所以我们可以使用Swift4的Mirror反射机制来实现这个功能,下面我将写文章来实现这个功能。

下一篇文章是 iOS开发技巧系列---打造强大的BaseModel(篇二:让Model能够自动将字典转化成Model),敬请期待。
最新更新,文章已经发布了,请看:http://www.jianshu.com/p/7d94e49297b

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,019评论 4 62
  • “阿凝,你当真要杀我”,在陌凝的攻势下,冷殇有点体力不支,陌凝招招夺命,而冷殇招招小心,生怕伤她丝毫。滴滴鲜血,衣...
    墨兮晗阅读 293评论 0 0
  • 如前文所述,要想实现热更新的目的,就必须在dex分包完成之后操作字节码文件。比较常用的字节码操作工具有ASM和ja...
    李牙刷儿阅读 911评论 2 0
  • 一、新建一个目录(git_test) [root@git ~]# mkdir git_test 二、进入创建的目录...
    如来自然阅读 237评论 0 0