摘要: 从事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类,如图所示
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