摘要: 從事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"http://打印的結果,因沒有設置demoName所以為nil
demo1.demoName = "this is a demo"
print(demo1)
"DemoClass: demoId:0 demoName:this is a demo\n"http://打印出了自己在里面寫的屬性
細心的同學可能注意到了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的新標準請參考http://www.lxweimin.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.lxweimin.com/p/7d94e49297b