概述
View Controller 向來(lái)是 MVC (Model-View-View Controller) 中最讓人頭疼的一環(huán),MVC 架構(gòu)本身并不復(fù)雜递惋,但開(kāi)發(fā)者很容易將大量代碼扔到用于協(xié)調(diào) View 和 Model 的 Controller 中柔滔。你不能說(shuō)這是一種錯(cuò)誤,因?yàn)?View Controller 所承擔(dān)的本來(lái)就是膠水代碼和業(yè)務(wù)邏輯的部分丹墨。但是廊遍,持續(xù)這樣做必定將導(dǎo)致 Model View Controller 變成 Massive View Controller,代碼也就一天天爛下去贩挣,直到?jīng)]人敢碰喉前。
寫(xiě)到后來(lái),幾經(jīng)變換王财,最后你的 Controller 常常就變成了這樣
Controller 中含有大量代碼的一個(gè)很大原因在于卵迂,大多數(shù)人都誤用了 MVC,推薦可以看看喵神的這兩篇文章绒净,深入淺出见咒。
關(guān)于 MVC 的一個(gè)常見(jiàn)的誤用
單向數(shù)據(jù)流動(dòng)的函數(shù)式 View Controller
這篇文章我們先從網(wǎng)絡(luò)層入手,在 iOS 開(kāi)發(fā)中挂疆,網(wǎng)絡(luò)請(qǐng)求與數(shù)據(jù)解析可以說(shuō)是其中占比很高并且不可分割的一部分改览。
身為一名 iOS 開(kāi)發(fā),也許你不知道 NSUrlConnection
缤言、也不知道 NSURLSession
宝当,但你一定知道 AFNetworking / Alamofire。對(duì)他們你肯定也做過(guò)一些自己的封裝胆萧,或者直接采用業(yè)內(nèi)比較知名的第三方封裝庆揩。比如 Objective-C 中的 YTKNetwork ,Swift 中的 Moya 等等。
那么問(wèn)題來(lái)了订晌,無(wú)論是自己封裝也好還是直接采用第三方也好虏辫,在我們熟知的 MVC 模式中,你依舊需要在 Controller 中回調(diào) Block / Delegate 對(duì)其做出處理锈拨,比如對(duì)返回?cái)?shù)據(jù)的校驗(yàn)與解析砌庄,對(duì)指示器的控制,對(duì)刷新控件的控制推励,把 Model 賦值給 View 等等鹤耍。而且在 iOS 中 Controller 本身就包含了一個(gè) View,對(duì)其生命周期的管理和界面布局無(wú)疑又增加了 Controller 的負(fù)擔(dān)验辞。
久而久之稿黄,當(dāng)控制器中再加入一些其他的業(yè)務(wù)邏輯時(shí),整個(gè)控制器里的代碼就會(huì)變得非常臃腫跌造,巨胖無(wú)比杆怕,隨著業(yè)務(wù)的變更,代碼的可讀性會(huì)變得很差壳贪。其實(shí) Controller 中大多數(shù)代碼都可以被抽離出去陵珍,比如說(shuō)我們的網(wǎng)絡(luò)請(qǐng)求。
讓網(wǎng)絡(luò)請(qǐng)求的代碼更優(yōu)雅
本篇文章我們主要是針對(duì) Moya 的再次封裝擴(kuò)展违施。其實(shí) Moya 本身對(duì)網(wǎng)絡(luò)層的封裝已經(jīng)很優(yōu)秀了互纯,自帶了對(duì)于 RxSwift 這類函數(shù)響應(yīng)式庫(kù)的擴(kuò)展,網(wǎng)絡(luò)層非常清晰磕蒲,并且提供了簡(jiǎn)單方便的網(wǎng)絡(luò)單元測(cè)試留潦。但我們依然可以把她變得更好。
封裝 Moya
Moya 的使用我在這里就不貼了辣往,沒(méi)用過(guò)的小伙伴可以去官方文檔學(xué)習(xí)一下兔院。
用過(guò)的小伙伴知道,我們使用 Moya 都要先創(chuàng)建一個(gè) Enum
遵守 TargetType
協(xié)議實(shí)現(xiàn)對(duì)應(yīng)的方法(比如指定請(qǐng)求的 URL 路徑站削,參數(shù)等等)坊萝。
public enum GitHub {
case userProfile(String)
}
extension GitHub: TargetType {
public var baseURL: URL { return URL(string: "https://api.github.com")! }
public var path: String {
switch self {
case .userProfile(let name):
return "/users/\(name.urlEscaped)"
}
}
public var method: Moya.Method {
return .get
}
public var task: Task {
switch self {
default:
return .requestPlain
}
}
}
而實(shí)際的請(qǐng)求是使用 MoyaProvider<Target>
類,傳入一個(gè)遵守 TargetType
協(xié)議的 Enum
许起,創(chuàng)建 MoyaProvider
對(duì)象去請(qǐng)求的十偶。
provider = MoyaProvider<GitHub>()
provider.request(.userProfile("InsectQY")) { result in
// do something with the result
}
可是如果把項(xiàng)目中所有的網(wǎng)絡(luò)請(qǐng)求都寫(xiě)在同一個(gè) Enum
中的話,這個(gè)Enum
里的代碼會(huì)非常多园细,維護(hù)起來(lái)也并不方便扯键。
筆者在使用時(shí)通常都是根據(jù)模塊創(chuàng)建多個(gè) Enum
,比如按首頁(yè)模塊珊肃,新聞模塊這樣劃分。如果這么寫(xiě)的話,我們創(chuàng)建 MoyaProvider
對(duì)象時(shí)就不能再傳入指定類型的 Enum
了伦乔。我們把創(chuàng)建對(duì)象的寫(xiě)法改成 MoyaProvider<MultiTarget>
厉亏,所有傳入的 Enum
得用 MultiTarget
包裝一層。
let provider = MoyaProvider<MultiTarget>
provider.request(MultiTarget(GitHub.userProfile("InsectQY"))) { result in
// do something with the result
}
看了上面的代碼烈和,好像已經(jīng)開(kāi)始變得不那么優(yōu)雅了爱只,我指定一個(gè)請(qǐng)求竟然要寫(xiě)這么多代碼,一大堆括號(hào)看的眼睛都暈招刹。能不能直接使用 Enum
的類型不需要借助 MoyaProvider
對(duì)象去請(qǐng)求呢恬试,類似這樣的效果。
GitHub.userProfile("InsectQY").request
以下封裝我們基于 RxSwift 來(lái)實(shí)現(xiàn)疯暑,當(dāng)然如果你不熟悉 RxSwift 也沒(méi)關(guān)系训柴,這里只是對(duì)封裝思路的介紹,封裝完成以后可以直接使用妇拯,等以后熟悉了 RxSwift 再回頭看也行幻馁。以下文章的思路大多借鑒 RxNetwork 這個(gè)庫(kù)的實(shí)現(xiàn)。
首先我們?yōu)?TargetType
添加自己的 public extension
方便外界調(diào)用越锈。
public extension TargetType {
}
先實(shí)現(xiàn)一個(gè)可以直接使用 Enum
類型調(diào)用請(qǐng)求的方法仗嗦。
let provider = MoyaProvider<MultiTarget>
public extension TargetType {
func request() -> Single<Response> {
return provider.rx.request(.target(self))
}
}
這個(gè)方法返回一個(gè) Single
類型的 Observable
。Single
是 Observable
的另一個(gè)版本甘凭。它不像 Observable
可以發(fā)出多個(gè)元素稀拐,它要么只能發(fā)出一個(gè)元素,要么產(chǎn)生一個(gè) error
事件丹弱,不共享狀態(tài)變化德撬,用來(lái)做請(qǐng)求的返回非常合適。
寫(xiě)完我們就可以直接用 Enum
調(diào)用請(qǐng)求蹈矮,怎么樣是不是非常簡(jiǎn)單呢砰逻。代碼的可讀性也變高了很多。對(duì)請(qǐng)求的結(jié)果只需要調(diào)用 subscribe
去監(jiān)聽(tīng)即可泛鸟。
GitHub.userProfile("InsectQY").request.subscribe...
封裝 JSON 解析
先回顧一下我們以往的 JSON 解析蝠咆,通常都是使用第三方解析庫(kù),直接把代碼放到每次請(qǐng)求的回調(diào)中去處理北滥。
乍一看其實(shí)沒(méi)毛病刚操,那么這么做有什么弊端呢?其實(shí)這種寫(xiě)法侵入性很強(qiáng)再芋,試想一下假如有一天你這個(gè)第三方解析庫(kù)不維護(hù)了菊霜,或者種種原因你需要更換到其他的第三方,或者自己手寫(xiě)解析济赎,那么你需要替換和修改的地方就非常多鉴逞。
你可能會(huì)說(shuō)记某,那我可以在第三方解析的方法上封裝一層,然后調(diào)用我自己的解析方法啊构捡。是的液南,想法很好,但你有沒(méi)有想過(guò)其實(shí)解析的寫(xiě)法可以變得非常優(yōu)雅勾徽。
Moya 自身就提供了基于 Codable
協(xié)議的原生解析方法滑凉。
public func map<D>(_ type: D.Type, atKeyPath keyPath: String? = default, using decoder: JSONDecoder = default, failsOnEmptyData: Bool = default) throws -> D where D : Decodable
支持對(duì) JSON 指定路徑的解析,實(shí)現(xiàn)的原理也非常簡(jiǎn)單喘帚,感興趣的小伙伴可以去源碼中學(xué)習(xí)一下畅姊。具體位置在 Response
這個(gè)類中搜索關(guān)鍵詞即可。
這個(gè)方法我們直接就能使用吹由,轉(zhuǎn)模型的代碼可以寫(xiě)成這樣
GitHub.userProfile("InsectQY").request
.map(UserModel.self)
當(dāng)然最好我們還是在原生方法上再封裝一層若未,減少原生方法對(duì)項(xiàng)目的侵入性。
需要注意的是溉知,在我們平時(shí)使用 Codable
協(xié)議時(shí)陨瘩,通常都要分清解析的是數(shù)組還是字典。如果是數(shù)組類型數(shù)據(jù)的話级乍,必須得調(diào)用指定解析數(shù)組的方法舌劳,否則無(wú)法正確解析。
但 Moya 是可以在外界直接傳入數(shù)組類型的玫荣,具體實(shí)現(xiàn)也非常簡(jiǎn)單甚淡。用一個(gè) Struct
的結(jié)構(gòu)體去包裝每次需要解析的對(duì)象,再把解析對(duì)象指定為包裝好的結(jié)構(gòu)體捅厂。
private struct DecodableWrapper: Decodable {
let value: T
}
這樣就不用關(guān)心外界需要解析的具體類型贯卦,相當(dāng)于每次解析的必然是一個(gè)包裝好的字典類型,最后只要把結(jié)構(gòu)體里的 value
返回就行焙贷。
扯一個(gè)題外話撵割,那這種實(shí)現(xiàn)思路在 Objective-C 中是否可行呢,可以思考如下兩個(gè)問(wèn)題辙芍。
- 在 Objective-C 中我們使用 MJExtension / YYModel 這些庫(kù)去解析 JSON 時(shí)啡彬,都要調(diào)用指定的解析方法(數(shù)組和字典的解析方法是不同的),能否用以上的思路把解析數(shù)組和解析字典的方法整合成一個(gè)方法呢故硅?
- 如果要解析的模型中有個(gè)數(shù)組屬性庶灿,數(shù)組里面又要裝著其他模型。還要寫(xiě)指定數(shù)組內(nèi)部類型的方法吃衅。
// Tell MJExtension what type of model will be contained in statuses and ads.
[StatusResult mj_setupObjectClassInArray:^NSDictionary *{
return @{
@"statuses" : @"Status",
// @"statuses" : [Status class],
@"ads" : @"Ad"
// @"ads" : [Ad class]
};
}];
+ (NSDictionary *)modelContainerPropertyGenericClass {
// value should be Class or Class name.
return @{@"shadows" : [Shadow class],
@"borders" : Border.class,
@"attachments" : @"Attachment" };
}
這么寫(xiě)目的是為了在運(yùn)行時(shí)拿到數(shù)組中元素的具體類型往踢,再用 Runtime
去類中獲取屬性以及 KVC
賦值。如果用泛型指定數(shù)組里元素的具體類型的話徘层,這些方法是否可以省略呢峻呕?
然而很遺憾利职,原生的 Objective-C 是無(wú)法實(shí)現(xiàn)以上想法的。原因在于 Objective-C 的泛型只能算是"偽"泛型山上,僅僅是一個(gè)編譯器特性眼耀,只能在編譯時(shí)為 Xcode 提供具體類型,在運(yùn)行時(shí)是沒(méi)有的佩憾。
封裝網(wǎng)絡(luò)緩存
為了提升用戶體驗(yàn),在實(shí)際開(kāi)發(fā)中干花,有一些內(nèi)容可能會(huì)加載很慢妄帘,我們想先顯示上次的內(nèi)容,等加載成功后池凄,再用最新的內(nèi)容替換上次的內(nèi)容抡驼。也有時(shí)候,由于網(wǎng)絡(luò)處于斷開(kāi)狀態(tài)肿仑,為了更加友好致盟,我們想顯示上次緩存中的內(nèi)容。
網(wǎng)絡(luò)緩存我們基于 Cache 來(lái)實(shí)現(xiàn)锌妻。首先創(chuàng)建一個(gè) CacheManager
統(tǒng)一處理所有的讀取和存儲(chǔ)操作貌矿。我們把讀取模型數(shù)據(jù)和讀取網(wǎng)絡(luò)請(qǐng)求返回的 Response
數(shù)據(jù)分別創(chuàng)建不同的方法(這里只貼了模型的方法)冀值。
// MARK: - 讀取模型緩存
static func object<T: Codable>(ofType type: T.Type, forKey key: String) -> T? {
do {
let storage = try Storage(diskConfig: DiskConfig(name: "NetObjectCache"), memoryConfig: MemoryConfig(), transformer: TransformerFactory.forCodable(ofType: type))
try storage.removeExpiredObjects()
return (try storage.object(forKey: key))
} catch {
return nil
}
}
// MARK: - 緩存模型
static func setObject<T: Codable>(_ object: T, forKey: String) {
do {
let storage = try Storage(diskConfig: DiskConfig(name: "NetCache"), memoryConfig: MemoryConfig(), transformer: TransformerFactory.forCodable(ofType: T.self))
try storage.setObject(object, forKey: forKey)
} catch {
print("error\(error)")
}
}
緩存的方法封裝好以后,我們還需要知道緩存的 key杯道,這里我們采用請(qǐng)求的 URL + 參數(shù)拼接成 key。
extension Task {
public var parameters: String {
switch self {
case .requestParameters(let parameters, _):
return "\(parameters)"
case .requestCompositeData(_, let urlParameters):
return "\(urlParameters)"
case let .requestCompositeParameters(bodyParameters, _, urlParameters):
return "\(bodyParameters)\(urlParameters)"
default:
return ""
}
}
}
public extension TargetType {
var cachedKey: String {
return "\(URL(target: self).absoluteString)?\(task.parameters)"
}
}
萬(wàn)事俱備责蝠,現(xiàn)在為 TargetType
添加一個(gè) cache
屬性党巾,返回一個(gè) Observable
包裝遵守 TargetType
協(xié)議的 Enum
。
var cache: Observable<Self> {
return Observable.just(self)
}
那么我們調(diào)用緩存的代碼就變成了這樣
GitHub.userProfile("InsectQY").cache
但是這個(gè)緩存還沒(méi)有具體的實(shí)現(xiàn)霜医,現(xiàn)在我們?yōu)榫彺嫣砑訉?shí)現(xiàn)齿拂,只有遵守 TargetType
協(xié)議才能調(diào)用。
每次調(diào)用方法都把請(qǐng)求結(jié)果緩存到本地肴敛,返回?cái)?shù)據(jù)時(shí)先從本地獲取署海,本地沒(méi)有值時(shí)只返回網(wǎng)絡(luò)數(shù)據(jù)。這里的 startWith
保證本地?cái)?shù)據(jù)有值時(shí)值朋,本地?cái)?shù)據(jù)每次都優(yōu)先在網(wǎng)絡(luò)數(shù)據(jù)之前返回叹侄。
extension ObservableType where E: TargetType {
public func request() -> Observable<Response> {
return flatMap { target -> Observable<Response> in
let source = target.request().storeCachedResponse(for: target).asObservable()
if let response = target.cachedResponse {
return source.startWith(response)
}
return source
}
}
}
現(xiàn)在我們的緩存已經(jīng)初步完成了,在 onNext 回調(diào)中昨登,第一次返回的是本地?cái)?shù)據(jù)趾代,第二次是網(wǎng)絡(luò)數(shù)據(jù)。我們的請(qǐng)求就變成了這樣
GitHub.userProfile("InsectQY")
.cache
.request()
.map(UserModel.self)
.subscribe ...
這樣的好處是丰辣,每個(gè)方法之間都是獨(dú)立的撒强,我不想要緩存我只要去掉 cache
不想轉(zhuǎn)模型只要去掉 map
禽捆,整段代碼的可讀性變得很強(qiáng)。
由于 RxSwift 的存在飘哨,你也不需要在 Controller 銷毀時(shí)去手動(dòng)管理網(wǎng)絡(luò)請(qǐng)求的取消胚想。你想做一些網(wǎng)絡(luò)的其他高級(jí)操作也變得非常容易,比如說(shuō)鏈?zhǔn)降木W(wǎng)絡(luò)請(qǐng)求芽隆,group 式的網(wǎng)絡(luò)請(qǐng)求浊服,請(qǐng)求失敗自動(dòng)重試,同一個(gè)請(qǐng)求多次請(qǐng)求時(shí)短時(shí)間忽略相同的請(qǐng)求等等都非常簡(jiǎn)單胚吁。
現(xiàn)在回頭看看我們的需求牙躺,優(yōu)先展示本地?cái)?shù)據(jù),網(wǎng)絡(luò)數(shù)據(jù)返回時(shí)自動(dòng)替換本地?cái)?shù)據(jù)腕扶,網(wǎng)絡(luò)請(qǐng)求失敗時(shí)加載本地?cái)?shù)據(jù)孽拷。
但是這種寫(xiě)法應(yīng)用場(chǎng)景相對(duì)比較單一,只能適用于本地?cái)?shù)據(jù)和網(wǎng)絡(luò)數(shù)據(jù)的處理是相同的情況半抱。我們?cè)?onNext
中無(wú)法區(qū)分本地?cái)?shù)據(jù)和網(wǎng)絡(luò)數(shù)據(jù)脓恕,假如想對(duì)本地?cái)?shù)據(jù)做一些特殊處理的話是不行的。
我們?cè)偻晟埔幌麓a窿侈,將本地?cái)?shù)據(jù)的回調(diào)告訴外界炼幔。
func onCache<T: Codable>(_ type: T.Type, atKeyPath keyPath: String? = "", _ onCache: ((T) -> ())?) -> OnCache<Self, T> {
if let object = cachedObject(type) {onCache?(object)}
return OnCache(self)
}
返回的 OnCache
對(duì)象是自定義的一個(gè)結(jié)構(gòu)體
public struct OnCache<Target: TargetType, T: Codable> {
public let target: Target
public let keyPath: String
init(_ target: Target, _ keyPath: String) {
self.target = target
self.keyPath = keyPath
}
public func request() -> Single<T> {
return target.request()
.mapObject(T.self, atKeyPath: keyPath)
.storeCachedObject(for: target)
}
}
現(xiàn)在我們就可以在 onCache
的回調(diào)中拿到本地?cái)?shù)據(jù)了,如果你想對(duì)本地?cái)?shù)據(jù)做一些自己的操作和處理的話棉磨,選擇第二種方案會(huì)更加合適江掩。后續(xù)的 subscribe
監(jiān)聽(tīng)到的是一個(gè) Single
,如之前所說(shuō)乘瓤,只會(huì)返回成功或者失敗环形,這里我們只把網(wǎng)絡(luò)數(shù)據(jù)返回就好。這樣就做到了網(wǎng)絡(luò)數(shù)據(jù)和本地?cái)?shù)據(jù)的區(qū)分衙傀。
GitHub.userProfile("InsectQY")
.onCache(UserModel.self, { (local) in
})
.request()
.subscribe ...
總結(jié)
好了看了以上這么多抬吟,我們只是對(duì)網(wǎng)絡(luò)層做了一些封裝,還沒(méi)有做這種寫(xiě)法實(shí)際在項(xiàng)目中的應(yīng)用统抬,后續(xù)將教大家如何用 RxSwift
減少控制器的代碼火本。
具體的 demo
和用法可以查看我開(kāi)源的這個(gè)項(xiàng)目 GamerSky
或者原作者的 RxNetwork 。