第一篇:《打造強大的BaseModel(1):讓Model自我描述》
這篇文章將講述Model一項更高階也最常用的功能,讓Model實現自動對映–將字典轉化成Model(所有程式碼全由Swift實現)
將JSON轉化為Model的意義
在iOS開發中,基於Model的資料流起到了至關重要的作用。從網路獲取的資料需要進一步處理轉到成View可用的Model,再通過ViewController傳送給View展示出來,從View中反饋的資料也可以轉為為Model,再將Model轉化成JSON傳送給伺服器。通常開發過程中需要最頻繁處理的還是將JOSN轉化成對應的Model,目前市面上許多非常好用的JSON-Model庫,比如MJ大神的MJExtension,還有Matle及JSONModel。
目前我還從未在專案裡用到這些庫,有興趣的讀者可以自行去試試這些開源庫,也可以去看看它們的原始碼。這篇文章主要是討論如何用簡單的程式碼寫一個基類Model,讓子類可以自動實現從字典獲取資料再轉化為自身。功能並不算強大,但是還是比較實用的。
將字典轉化為Model這個過程中,最簡單便捷的情況就是字典的Key與Model的屬性名是一一對應的,這樣只要使用簡單的KVC即可完成JOSN-Model轉換。但是實際開發過程中很少出現這情況,也許你的命名風格和伺服器開發的同事的命名風格不一樣,也許同一個屬性名在不同的介面有不一樣的名字,總之很難達到這種理想的情況。我們最後還是要乖乖按伺服器的同事給的Key名來轉換。
先前我都是使用簡單的Dict[Key]的取值方式來實現JOSN-Model轉換,這種情況在Objc還是比較好用的,寫起來很方便,但是在Swift裡就完全行不通了,各種強制轉換寫起來很囉嗦很不爽。SwiftyJSON就是為了解決這種問題誕生的。用了SwiftyJSON看起來確實好了不少,但是還是要寫很多重複的程式碼來轉換,降低了開發效率。
使用KVC來實現字典-Model轉換
所以最好還是讓Model可以自動實現字典-Model對映。既然有這麼強大的KVC可以用,那為什麼不用呢?按照這個思路,我用Swift寫下了以下程式碼,先定義一個協議,裡面有一個靜態方法,目的是將一個字典轉化成一個Self物件
1 2 3 |
protocol MapAble{ static func mapModel(obj:AnyObject)->Self //將一個字典轉化成自己 } |
需要在GrandModel裡處理一些東西
1 2 3 4 5 6 7 8 9 |
//定義一個Model class GrandModel:NSObject { class var selfMapDescription:[String:String]?{ return nil } required override init() { super.init() } |
在extension裡面實現協議
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
extension GrandModel:MapAble{ static func mapModel(obj:AnyObject)->Self{ let model = self.init() if let mapTable = self.selfMapDescription{ if let dict = obj as? [String:AnyObject] { for item in dict{ if let key = mapTable[item.0]{ print("key 為(item.0)將要被設成(mapTable[item.0),其值是 (item.1)") model.setValue(item.1, forKey: key) } } } } return model } } |
程式碼層次比較深,看起來有點亂,但總體邏輯卻很簡單。selfMapDescription是一個Class靜態屬性,它描述了字典的Key是怎麼和Model的屬性名是怎麼對映的。子類需要重寫這個屬性。
1 |
required override init() |
這是一個構造器,並且標記為required,說明是必需的。作用是保證該類是可以初始化的,只有這樣extension裡的self.init()語句才不會報錯。因為如果子類也不提供構造器的話,那麼該類不能正常用構造器例項化,也就是不能用init()方法了。
1 2 |
let model = self.init() let mapTable = self.selfMapDescription |
這個就比較好解釋了,在靜態方法裡面,self是一個Type型別,表示執行時的型別。它可以呼叫init()直接例項化一個執行時繼承於GrandModel的類。接下來的程式碼就比較簡單了,只要簡單將字典裡的Value用KVC一個一個給Model賦值就行了。賦值完成後再返回該物件
下面我們來看子類如何繼承該類,並且怎麼使用些功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class DemoClass:GrandModel { var name:String? var age:Int? var grade:Int? //需要重寫selfMapDescription override static var selfMapDescription:[String:String]?{ return ["sName":"name","iAge":"age","iGrade":"grade"] } } //下面來測試 let demoDict = ["sName":"Stan","iAge":"12","iGrade":"6"] var demo = DemoClass() demo = DemoClass.mapModel(demoDict) print(demo) //列印結果: key 為sName將要被設成Optional("name"),其值是 Stan key 為iGrade將要被設成Optional("grade"),其值是 6 沒有這個欄位-------grade key 為iAge將要被設成Optional("age"),其值是 12 沒有這個欄位-------age |
這裡有奇怪的地方,明明DemoClass有兩個Int型別的屬性,但是又說找不到?其實在這裡參考第一篇文章就很容易明白,這兩個屬性是Int?的型別的,而Objc裡面是沒有於之對應的型別的,可以在這個屬性前面加上@objc試試
可以看見XCode報錯了。Int?不能表現為Objc型別,所以在Objc執行時裡面是找不到與之對應的型別,自然不能加上@Objc標記了。
那麼怎麼辦?其實很簡單,給它一個初始值就行了,在執行時可以將其轉化成Objc的型別
1 2 3 4 5 6 7 8 9 10 11 12 |
class DemoClass:GrandModel { var name:String? var age:Int = 0 var grade:Int = 0 //後面的省略了 } //下面列印的結果完全符合預期 key 為sName將要被設成Optional("name"),其值是 Stan key 為iGrade將要被設成Optional("grade"),其值是 6 key 為iAge將要被設成Optional("age"),其值是 12 Optional(name) DemoClass:["age": 12, "grade": 6, "name": Stan] |
現在列印出的結果是完全符合我們的預期了。KVC的強大之處在這裡表現得淋漓盡致,我們根本不需要理會這些基本資料型別,KVC可以幫我們搞定。
兩個問題和解決方案
但是目前還有兩個問題:一是如果介面有多種風格的屬性名,比如這個介面將UserName,另一個叫做sUserName,又或者叫userName等,多種不同的屬性名。至少我現在在做的專案至少有三種型別。其實這個非常簡單,直接把所有屬性的名字新增到selfMapDescription裡面就行
1 2 3 4 |
//需要重寫selfMapDescription override static var selfMapDescription:[String:String]?{ return["sName":"name","iAge":"age","iGrade":"grade","UserName":"userName","sUserName":"userName","userName":"userName"]//將這個三屬性名全部對映到userName } |
第二個問題,如果返回的資料裡巢狀了複雜型別,比如Array,Dictionary或者是其他物件等,KVC就不能幫你自動轉了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
class DemoOther: GrandModel { var userName:String? override static var selfMapDescription:[String:String]?{ return [ "userName":"userName"] } } class DemoClass:GrandModel { var name:String? var age:Int = 0 var grade:Int = 0 var userName:String? var otherClass:DemoOther? var otherClasses:[DemoOther]? //需要重寫selfMapDescription override static var selfMapDescription:[String:String]?{ return ["sName":"name", "iAge":"age", "iGrade":"grade", "UserName":"userName", "sUserName":"userName", "userName":"userName", "DemoOther":"otherClass", "DemoOthers":"otherClasses"] } } //下面來測試 let demoDict = ["sName":"Stan","iAge":"12","iGrade":"6","UserName":"userName","DemoOther":["userName":"OtherUserName"], "DemoOthers":[["userName":"OtherUserName1"],["userName":"OtherUserName2"]] ] var demo = DemoClass() demo = DemoClass.mapModel(demoDict) print(demo.otherClass) //列印出來是這東西, //Optional({ userName = OtherUserName;}) |
列印出的資料雖然看起來好像是正確的,但是其實格式是錯誤的。也就是說,KVC不能轉化複雜的或者自定義的物件。只能自己手動寫了。
1 2 3 4 |
demo.otherClass = DemoOther.mapModel(demoDict["DemoOther"]!) print(demo.otherClass) //列印出來是這東西, //**Optional(DemoOther:["userName": OtherUserName])** |
這下列印出的東西就符合預期了,陣列物件也一樣,用個迴圈來轉化即可。
總結
上面的輕量型字典-Model轉換方案雖然在功能上不能和第三方的Json-Model庫相比,但對於大部分專案來說,還是夠用了。使用KVC帶來的效能上的損失我還是可以接受的,我討厭寫重複的程式碼。好了,以上就是第二篇的全部內容。