打造強大的BaseModel(1):讓Model自我描述
前言
從事iOS開發已經兩年了,從一無所知到現在能獨立帶領團隊完成一系列APP的開發,網路上的大神給了我太多的幫助。他們無私地貢獻自己的心得和經驗,寫出了一篇篇精美的文章。現在我也開始為大家貢獻自己的心得,把它寫成一系列iOS開發技巧系列文章。
這一系列文章都乾貨十足,希望各位讀者可以積極留言,和我溝通。
何為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,就會打印出來物件的類名。
123456789 | classdemoClass |
顯然這串的數字或者是類名對我們來說毫無用處,正常情況下,我們需要看的是這個物件所有屬性的資料。在Objc裡面,直接在自定義類裡重寫description方法就行,當你列印物件時,執行時會自動呼叫物件的description方法。
1234 | -(NSString)descprition{return你對自定義類的各個變數的描述,從而可以打印出來} |
但是在Swift語言,情況變得不一樣了。Swift並不存在descprition方法,那麼Swift是怎麼實現的呢?
Swift中有一系列協議.其中以Custom開頭的協議(目前共有5個):
12345 | CustomReflectableCustomLeafReflectableCustomPlaygroundQuickLookableCustomStringConvertibleCustomDebugStringConvertible |
這些協議表示自定義一個方法(其實並不是方法,後面會說到這是一個屬性),這個方法是用來將物件轉化為可以打印出來的字串或者視覺化的圖形等。
其中協議CustomStringConvertible和CustomDebugStringConvertible就是相當於Objc的實現descprition方法的協議(其他協議可以看官方文件),讓自定義的類繼承這兩個協議後就需要重寫description屬性和debugDescription屬性(所以前面有提到這不是一個方法)
12345678910111213141516171819202122 | classdemoClass{vardemoId:Int=0vardemoName:String?}//實現協議可以在Extension裡進行extension demoClass:CustomStringConvertible{vardescription:String{//重寫description,注意,因為這個類沒有父類,所以不需要加上overridereturn"DemoClass: demoId:(demoId) demoName:(demoName ?? "nil")"}}extension demoClass:CustomDebugStringConvertible{vardebugDescription:String{returnself.description}}let demo1=demoClass()print(demo1)"DemoClass: demoId:0 demoName:niln"//列印的結果,因沒有設定demoName所以為nildemo1.demoName="this is a demo"print(demo1)"DemoClass: demoId:0 demoName:this is a demon"//打印出了自己在裡面寫的屬性 |
細心的同學可能注意到了CustomStringConvertible對應的應該是屬性description,而CustomDebugStringConvertible對應的是debugDescription屬性.那麼debugDescription有什麼用呢? debugDescription是在除錯時你可以用po命令來列印物件,所以在這裡我讓它直接返回description就行了。
這裡有一點需要注意,如果你的類是繼承了NSObject的話,那麼就不需要再繼承CustomStringConvertible了,因為NSObject已經繼承這個協議了,所以只要重寫description屬性就行了。
123456789 | classDemoClassA:NSObject{vardemoId:Int=0vardemoName:String?override vardescription:String{return"DemoClass: demoId:(demoId) demoName:(demoName ?? "nil")"}}let demo2=DemoClassA()print(demo2)//"DemoClass: demoId:0 demoName:niln" |
好了,怎麼實現物件的自我描述很清楚了,但下一個問題又來了.一個專案裡面通常會有十幾個甚至幾十個Model,如果每個Model都這樣重寫description屬性是件極耗精力的事情.這需要重複寫大量相似的程式碼,顯然不這不可取的。那麼有沒有辦法可以直接讓Model自我描述呢?答案是有的。通過反射的方法或者在執行時可以找到Model的所有屬性,再通過KVC給這些屬性賦值就可以打印出來了。再將所有的屬性名的其對應的值儲存到字典裡。再把字典按照某種格式轉化為String就完成了。
當然這裡有一個侷限性,就是單純的Swift類是沒有KVC的,你需要讓它繼承NSObject就有這個功能。因為只有Objctive C才有執行時這一套東西。如果讓Swift中加入Objc執行時,Swift的效率會有降低。這就要看自己的取捨了。
下面直接上程式碼
12345678910111213141516171819 | //先定義ModelclassGrandModel:NSObject{//這裡不定義任何屬性,所有用的屬性都在子類,直接重寫descriptioninternal override vardescription:String{get{vardict=[String:AnyObject]()let count:UnsafeMutablePointer=UnsafeMutablePointer()varproperties=class_copyPropertyList(self.dynamicType,count)whileproperties.memory.debugDescription!="0x0000000000000000"{lett=property_getName(properties.memory)letn=NSString(CString:t,encoding:NSUTF8StringEncoding)letv=self.valueForKey(nas!String)??"nil"dict[nas!String]=vproperties=properties.successor()}return"(self.dynamicType):(dict)"}}} |
接下來寫一個測試Model繼承於GrandModel
12345678 | classTestModel:GrandModel{vari=0vara:String?}let model=TestModel()print(model)//TestModel:["a": nil, "i": 0]nmodel.a="aaa"print(model)//TestModel:["a": aaa, "i": 0]n |
可見,結果完全符合我們需要的效果。所有的欄位都可以成功打印出來,那麼我再深入一下,如果TestModel裡有一個屬性是Enum,或者是其他的非Objc支援的執行時型別,會出現什麼情況呢?
我們先定義一個列舉,並且把i改成Int?的型別,再加一個有初始值的Int型別
1234567891011121314 | enumweek{caseMon,Thu,Wed,Tur,Fri,Sai,Sun}//TestModel加入列舉classTestModel:GrandModel{vari:Int?varj=1vara:String?varweeb:week?}let model=TestModel()print(model)//列印結果//TestModel:["j": 1, "a": nil] |
這個結果有點讓人奇怪? 執行時找不到這兩個屬性?可以分析一下,我們定義的這個列舉是個純粹的Swift列舉,而Int?型別也無法在Objc裡面用正確的型別來表示。那為什麼String?可以被Objc執行時正確地識別呢?所以一個大的問題出來了,Apple是怎麼設定Swift型別到Objc型別的對映關係的?
關於這個問題,我想到了下面的方法
不再使用PlayGround來驗證,新建立一個Command Line專案,預設語言設為Swift,然後再新增一個Objc類,如圖所示
Xcode
注意在Objc的類加入Swift的標頭檔案,其格式是[專案名]-Swift.h,然後進入這個檔案,可以很容易找到定義在Main.swift裡的TestModel類
1234567 | SWIFT_CLASS("_TtC11DemoConsole9TestModel")@interfaceTestModel:GrandModel@property(nonatomic)NSIntegerj;@property(nonatomic,copy)NSString *__nullablea;-(nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;-(nullable instancetype)initWithCoder:(NSCoder *__nonnull)aDecoder OBJC_DESIGNATED_INITIALIZER;@end |
這下就可以看得一清二楚了,被轉成Objc類後,只有兩個屬性,另外兩個全沒了。所以在執行時找不到這兩個屬性,也就無法列印了。
關於更多的Swift型別到Objc型別的對映關係這裡就不多說了,有興趣的同學可以用XCode除錯,相信你會有大收穫的。
另外一個問題就是如果這個類中的屬性是另一個類怎麼辦?或者是個Array,Dict呢,其實很簡單,只要這個屬性也繼承了GrandModel,都可是順利打印出來。
123456789101112131415161718192021222324252627282930313233343536 | structStructDemo{varq=1varw="w"}classClassDemo{varq=1varw="w"}classClassDemoA:GrandModel{varq=1varw="w"}classTestModelA:GrandModel{vari:Int=1varo:String?varstructDemo:StructDemo?varclassDemo:ClassDemo?varclassDemoA:ClassDemoA?varclassDemoAArray:[ClassDemoA]?varclassDemoDict:[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]";}] |
可見所有的屬性都正確地打印出來了。
結語:讓iOS的Model擁有自我描述的功能,可以在除錯DeBug中發揮非常大的作用。也讓我們看到了單純的Swift類和Objc的的一套執行時機制完全不同的。不過目前iOS開發還是脫離不了Objc執行時,所以雖然相比較於單純的Swift類,Objc執行時會有效能損失,但是還是可以完全接受的。
<