前言
MVC是一個做iOS開發(fā)都知道的設(shè)計模式嘉汰,也是Apple官方推薦的設(shè)計模式。實際上唱逢,Cocoa Touch就是按照MVC來設(shè)計的。
這里屋休,我們先不講MVC是什么坞古,我們先來談?wù)勡浖O(shè)計的一些原則或者說理念。在開發(fā)App的時候劫樟,我們的基本目標(biāo)有以下幾點:
可靠性 - App的功能能夠正常使用
健壯性 - 在用戶非正常使用的時候痪枫,app也能夠正常反應(yīng),不要崩潰
效率性 - 啟動時間毅哗,耗電听怕,流量,界面反應(yīng)速度在用戶容忍的范圍以內(nèi)
上文三點是表象層的東西虑绵,是大多數(shù)開發(fā)者或者團隊會著重注意的尿瞭。除了這三點,還有一些目標(biāo)是工程方面的也是開發(fā)者要注意的:
可修改性/可擴展性 - 軟件需要迭代翅睛,功能不斷完善
容易理解 - 代碼能夠容易理解
可測試性 - 代碼能夠方便的編寫單元測試和集成測試
可復(fù)用性 - 不用一次又一次造輪子
于是声搁,軟件設(shè)計領(lǐng)域有了幾大通用設(shè)計原則來幫助我們實現(xiàn)這些目標(biāo):
1單一功能原則黑竞,最少知識原則,聚合復(fù)用原則疏旨,接口隔離原則很魂,依賴倒置原則,里氏代換原則檐涝,開-閉原則
這里的每一個原則都可以寫單獨的一篇文章遏匆,本文篇幅有限,不多講解谁榜。
基于這些設(shè)計目標(biāo)和理念幅聘,軟件設(shè)計領(lǐng)域又有了設(shè)計模式。MVC/MVVM都是就是設(shè)計模式的一種窃植。
MVC
歷史
二十世紀(jì)世紀(jì)八十年代帝蒿,Trygve Reenskaug在訪問Palo Alto(施樂帕克)實驗室的時候,第一次提出了MVC巷怜,并且在Smalltalk-76進行了實踐葛超,大名鼎鼎的施樂帕克實驗室有很多劃時代的研發(fā)成果:個人電腦,以太網(wǎng)延塑,圖形用戶界面等绣张。
在接下來的一段時間內(nèi),MVC不斷的進化关带,基于MVC又提出了諸如MVP(model–view–presenter)胖替,MVVM(model–view–viewmodel)等設(shè)計模式。
組件
MVC設(shè)計模式按照職責(zé)將應(yīng)用中的對象分成了三部分:Model豫缨,View独令,Controller。MVC除了將應(yīng)用劃分成了三個模塊好芭,還定義了模塊之間的通信方式燃箭。
Model
Model定義了你的應(yīng)用是什么(What)。Model通常是純粹的NSObject子類(Swift中可以是Struct/Class)舍败,僅僅用來表示數(shù)據(jù)模型招狸。
Controller
Controller定義了Model如何顯示給用戶(How),并且View接收到的事件反饋到最后Model的變化邻薯。Controller層作為MVC的樞紐裙戏,往往要承擔(dān)許多Model與View同步的工作。
View
View是Model的最終呈現(xiàn)厕诡,也就是用戶看到的界面累榜。
優(yōu)點
MVC設(shè)計模式是是一個成熟的設(shè)計模式,也是Apple推薦的的設(shè)計模式,即使是剛?cè)胄械膇os開發(fā)者也多少了解這個設(shè)計模式壹罚,所以對于新人來說上手迅速葛作,并且有大量的文檔和范例來供我們參考。
在MVC模式中猖凛,View層是比較容易復(fù)用的赂蠢,對應(yīng)Cocoa中的UIView及其子類。所以辨泳,github的iOS開源項目中虱岂,View層也是最多的。
Model層涉及到了應(yīng)用是什么菠红,這一層非常獨立量瓜,但是往往和具體業(yè)務(wù)相關(guān),所以很難跨App服用途乃。
既然只有Model-View-Controller三個組件,那么剩余的邏輯層代碼就比較清楚了扔傅,全部堆積到Controller耍共。
通信
MVC不僅定義了三類組件,還定義了組件之間通信的方式猎塞。
MVC三個組件之間的通信方式如圖
Controller作為樞紐试读,它指向view和Model的線都是綠色的,意味著Controller可以直接訪問(以引用的方式持有)Model和View荠耽。
View指向Controller的是虛線钩骇,虛線表示View到Controller的通信是盲通信的,原因也很簡單:View是純粹的展示部分铝量,它不應(yīng)該知道Controller是什么倘屹,它的工作就是拿到數(shù)據(jù)渲染出來。
那么慢叨,何為盲通信呢纽匙?簡單來說當(dāng)消息的發(fā)送者不知道接受者詳細信息的時候,這樣的通信就是盲通信拍谐。Cocoa Touch為我們提供了諸如delegate(dataSource)烛缔,block,target/action這些盲通信方式轩拨。
Model指向Controller的同樣也是虛線践瓷。原因也差不多,Model層代表的數(shù)據(jù)層應(yīng)該與Controller無關(guān)亡蓉。當(dāng)Model改變的時候晕翠,通過KVO或者Notification的方式來通知Controller應(yīng)當(dāng)更新View。
這里有一點要提一下:UIViewController往往用來作為MVC中的Controller砍濒,MVC中的Controller也可以由其他類來實現(xiàn)崖面。
問題
通過上文的講解元咙,我們可以看到在純粹的MVC設(shè)計模式中,Controller不得不承擔(dān)大量的工作:
網(wǎng)絡(luò)API請求
數(shù)據(jù)讀寫
日志統(tǒng)計
數(shù)據(jù)的處理(JSON<=>Object巫员,數(shù)據(jù)計算)
對View進行布局庶香,動畫
處理Controller之間的跳轉(zhuǎn)(push/modal/custom)
處理View層傳來的事件,返回到Model層
監(jiān)聽Model層简识,反饋給View層
于是赶掖,大量的代碼堆積在Controller層中,MVC最后成了Massive View Controller(重量級視圖控制器)七扰。
為了解決這種問題奢赂,我們通常會為Controller瘦身,也就是把Controller中代碼抽出到不同的類中颈走,引入MVVM就是為Controller瘦身的一個很好的實踐膳灶。
MVVM
在MVVM設(shè)計模式中,組件變成了Model-View-ViewModel立由。
MVVM有兩個規(guī)則
View持有ViewModel的引用轧钓,反之沒有
ViewModel持有Model的引用,反之沒有
圖中锐膜,我們?nèi)匀灰?b>實線表示持有毕箍,虛線表示盲通信。
在iOS開發(fā)中道盏,UIViewController是一個相當(dāng)重要的角色而柑,它是一個個界面的容器,負(fù)責(zé)接收各類系統(tǒng)的事件荷逞,能夠?qū)崿F(xiàn)界面專場的各種效果媒咳,配合NavigationController等能夠輕易的實現(xiàn)各類界面切換。
在實踐中种远,我們發(fā)現(xiàn)UIViewController和View往往是綁定在一起的伟葫,比如UIViewController的一個屬性就是view。在MVVM中院促,Controller可以當(dāng)作一個重量級的View(負(fù)責(zé)界面切換和處理各類系統(tǒng)事件)筏养。
不難看出,MVVM是對MVC的擴展常拓,所以MVVM可以完美的兼容MVC渐溶。
對于一個界面來說,有時候View和ViewModel往往不止一個弄抬,MVVM也可以組合使用:
Controller解耦
MVC是一個優(yōu)秀的設(shè)計模式茎辐,本文講解MVVM也不是說想要用MVVM來替代MVC。對于軟件設(shè)計來說,設(shè)計模式僅僅是一些參考工具拖陆,并沒有固定的范式弛槐,使用起來是很靈活的。MVVM的很多理念對于Controller解耦是很有幫助的依啰。
SubView
把相關(guān)的View放到一個Container View里乎串,這樣把對應(yīng)View的創(chuàng)建,Layout等代碼抽離出來速警,并且由Container統(tǒng)一處理用戶交互叹誉,回調(diào)給外部。(這個比較好理解闷旧,就不舉例子了)
TableView
關(guān)于TableView的Delegate/DataSource解耦长豁,我單獨寫了一篇博客:
優(yōu)雅的開發(fā)TableView
并且,提供了一個swift開源庫忙灼,來進行解耦:
MDTable
Layout
在iOS中匠襟,視圖的Layout一直是代碼很亂的一塊。通常Layout有兩種
手動的計算Frame - 簡單粗暴该园,但是修改起來困難酸舍,易讀性也不好
通過約束AutoLayout - 有學(xué)習(xí)成本,并且不好debug爬范,但是修改起來方便,也容易閱讀弱匪。
通常使用Autolayout青瀑,我們都會用一些DSL的三方庫:Masonry(OC),SnapKit(Swift)。
以一個常見的Layout為例萧诫,以下兩圖是在一個App中很常見的兩種TableViewCell Layout:
兩行列表
左邊圖斥难,右邊detail
這里,我們只關(guān)心左側(cè)的圖帘饶,在常規(guī)的Layout情況下Cell中的代碼:
//Swift代碼哑诊,使用SnapKit
leftImageView?=?UIImageView(frame:?CGRect.zero)
contentView.addSubview(rightLabel)
//Layout
leftImageView.snp.makeConstraints?{?(maker)?in
????maker.leading.equalTo(contentView).offset(8.0)
????maker.width.height.equalTo(80)
????maker.centerY.equalTo(contentView)
}
于是,兩種cell類中及刻,我們把上述代碼進行Copy Paste镀裤。
那么有沒有一種更好的方式進行Layout復(fù)用呢?
其實有兩種方式進行Layout復(fù)用:
繼承(由基類提供Layout) 個人不喜歡繼承缴饭,繼承帶來的額外的耦合會造成后期維護牽一發(fā)而動全身暑劝。
Layout獨立抽離出來,以協(xié)議的方式進行依賴颗搂。
這里以第二種方式為例:
首先定義一個協(xié)議:來定義可以用來布局
protocol?Layoutable?{
????func?layoutMaker()?->(ConstraintMaker)?->?Void
}
然后担猛,對UIView進行擴展,增加布局方法,同時對于client端隱藏snapKit
extension?UIView{
????func?makeLayout(_?layouter:Layoutable)?{
????????snp.makeConstraints(layouter.layoutMaker())
????}
}
然后傅联,我們定義一個結(jié)構(gòu)體先改,來表示左側(cè)的正方形布局
struct?LeftSquareLayout?:?Layoutable?{
????func?layoutMaker()?->?(ConstraintMaker)?->?Void?{
????????return{?maker?in
????????????maker.leading.equalTo(self.superView).offset(8.0)
????????????maker.width.height.equalTo(self.length)
????????????maker.centerY.equalTo(self.superView)
????????}
????}
????varlength?:CGFloat
????varsuperView?:?UIView
????init(length:?CGFloat,?superView:UIView)?{
????????self.length?=?length
????????self.superView?=?superView
????}
}
于是,左側(cè)圖片的Layout代碼變成了如下:
1leftImageView.makeLayout(LeftSquareLayout(length:?80,?superView:?contentView))
工廠
工廠是一個很好的設(shè)計模式蒸走,你是否不斷的在代碼里重寫類似的代碼:
let?titleLabel?=?UILabel(frame:?CGRect.zero)
titleLabel.font?=?UIFont.systemFont(ofSize:?14)
titleLabel.textColor?=?UIColor(colorLiteralRed:?0.3,?green:?0.3,?blue:?0.3,?alpha:?1.0)
titleLabel.text?=?"Inital?Text"
contentView.addSubview(titleLabel)
一般App的字體的大小和顏色都是幾種之一仇奶,這時候我們用工廠的方式生產(chǎn)實例,能更好的實現(xiàn)代碼復(fù)用:
定義Label類型:
enum?LabelStyle?{
????casetitle
????casesubTitle
}
定義工廠方法:
extension?UILabel{
????static?func?with(style?initalStyle:LabelStyle)?->?UILabel{
????????switchinitalStyle?{
????????case.title:
????????????let?titleLabel?=?UILabel(frame:?CGRect.zero)
????????????titleLabel.font?=?UIFont.systemFont(ofSize:?14)
????????????titleLabel.textColor?=?UIColor(colorLiteralRed:?0.3,?green:?0.3,?blue:?0.3,?alpha:?1.0)
????????????returntitleLabel
????????default:
????????????returnUILabel()
????????}
????}
}
我們還可以提供兩個方法载碌,能夠讓我們鏈?zhǔn)降奶砑拥絪uperView和config
extension?UILabel{
????@discardableResult
????func?added(into?superView:UIView)?->?UILabel{
????????superView.addSubview(self)
????????returnself
????}
????@discardableResult
????func?then(config:(UILabel)?->?Void)?->UILabel{
????????config(self)
????????returnself
????}
}
于是猜嘱,代碼變成了這樣子
UILabel.with(style:?.title).added(into:?contentView).then?{?$0.text?=?"Inital?Text"}
在結(jié)合上文的Layout,我們甚至可以用一個鏈?zhǔn)降恼{(diào)用完成初始化和Layout
UILabel.with(style:?.title)
????.added(into:?contentView)
????.then?{?$0.text?=?"Inital?Text"}
????.makeLayout(yourLayout)
1?Note:?僅僅舉例嫁艇,實際應(yīng)用中朗伶,你可以需要更好的去設(shè)計語法
鏈?zhǔn)秸{(diào)用的延伸閱讀:PromiseKit
ViewModel
在MVC的Controller解耦中,引入ViewModel是一種很常見的方式步咪。把Controller中對應(yīng)與View相關(guān)的邏輯層出來论皆,這樣Controller需要做的就是
從DB/網(wǎng)絡(luò)中獲取數(shù)據(jù),轉(zhuǎn)換成ViewModel
把ViewModel裝載給View
View的屬性與ViewModel值綁定在一起(單向)
在Swift中猾漫,實現(xiàn)單向綁定是很容易的:
定義一個可綁定類型:
class?Obserable{
????typealias?ObserableType?=?(T)?->?Void
????varvalue:T{
????????didSet{
????????????observer?(value)
????????}
????}
????varobserver:(ObserableType)?
????func?bind(to?observer:@escaping?ObserableType){
????????self.observer?=?observer
????????observer(value)
????}
????init(value:T){
????????self.value?=?value
????}
}
然后点晴,我們擴展UILabel,讓其text能夠綁定到某一個Obserable值上
extension?UILabel{
????varob_text:Obserable.ObserableType?{
????????return{?value?in
????????????self.text?=?value
????????}
????}
}
接著悯周,建立一個ViewModel
class?MyViewModel{
????varlabelText:Obserable????init(text:?String)?{
????????self.labelText?=?Obserable(value:?text)
????}
}
然后粒督,就可以這么用單向綁定了
let?label?=?UILabel()
let?viewModel?=?MyViewModel(text:?"Inital?Text")
viewModel.labelText.bind(to:?label.ob_text)
//修改viewModel會自動同步到Label
viewMoel.labelText.value?=?"New?Text"
當(dāng)然,實際使用MVVM的時候禽翼,手動實現(xiàn)綁定和View事件回調(diào)也可以屠橄。
延伸閱讀:
RxSwift
ReactiveCocoa
猿題庫 iOS 客戶端架構(gòu)設(shè)計
網(wǎng)絡(luò)
網(wǎng)絡(luò)請求的代碼往往也是放到UIController的生命周期里(比如viewDidLoad)或者某些用戶的UI操縱。假設(shè)你基于以下三個開源庫框架進行網(wǎng)絡(luò)請求和JSON解析
Alamofire
ObjectMapper
AlamofireObjectMapper
我們來模擬一個登錄的網(wǎng)絡(luò)請求闰挡,首先定義一個數(shù)據(jù)結(jié)構(gòu)表示登錄的結(jié)果
struct?LoginResult:?Mappable{
????vartoken:?String
????varname:?String
????init?(map:?Map)?{/*?*/}
????mutating?func?mapping(map:?Map)?{
????????name?<-?map["name"]
????????token?<-?map["token"]
????}
}
然后锐墙,在button點擊事件中,進行l(wèi)ogin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func?handleLogin(sender:UIButton){
????let?userName?=?"userName"
????let?passWord?=?"password"
????let?url?=?"https://api.example.com/user/login"
????let?params?=?["username":userName,"password":passWord]
????Alamofire.request(url,?method:?.post,?parameters:?params,?encoding:?JSONEncoding()).responseObject?{?(response:DataResponse)?in
????????guard?let?result?=?response.value?else{
????????????print(response.error????"Unknown?Error")
????????????return
????????}
????????print(result.name)
????????print(result.token)
????}
}
這是一個很常規(guī)的做法:
在Controller中獲取網(wǎng)絡(luò)請求需要的數(shù)據(jù)
把請求數(shù)據(jù)給網(wǎng)絡(luò)模塊长酗,網(wǎng)絡(luò)模塊負(fù)責(zé)請求網(wǎng)絡(luò)數(shù)據(jù)溪北,并且解析成對象,然后異步回調(diào)給Controller
在Controller中處理網(wǎng)絡(luò)模塊回調(diào)的結(jié)果
這么做有兩個問題
1.host夺脾,paramter encoding等相關(guān)信息對Controller應(yīng)當(dāng)透明
2.Controller不應(yīng)該知道網(wǎng)絡(luò)層是基于Alamofire的
于是之拨,這里我們把網(wǎng)絡(luò)層抽離:
首先,定義一個協(xié)議咧叭,表示能夠解析成一個網(wǎng)絡(luò)請求的類型:
1
2
3
4
5
6
7
protocol?NetworkAPIConvertable?{
????varhost:String?{get}
????varpath:String?{get}
????varmethod:RequestMethod{get}
????varrequestEncoding:RequestEncoding{get}
????varrequestParams:[String:Any]{get}
}
其中敦锌,RequestMethod和RequestEncoding是對Alamofire的簡單封裝
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
enum?RequestEncoding{
????casejson,?propertyList,?url
}
enum?RequestMethod{
????caseget,?post,?delete,?put
}
private?extension?RequestMethod{
????func?toAlamofireMethod()->HTTPMethod{
????????switchself?{
????????case.get:
????????????return.get
????????case.post:
????????????return.post
????????case.delete:
????????????return.delete
????????case.put:
????????????return.put
????????}
????}
}
private?extension?RequestEncoding{
????func?toAlamofireEncoding()->ParameterEncoding{
????????switchself?{
????????case.json:
????????????returnJSONEncoding()
????????case.propertyList:
????????????returnPropertyListEncoding()
????????case.url:
????????????returnURLEncoding()
????????}
????}
}
接著,定義請求的接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct?APIRouter{
???static?func?request(api:NetworkAPIConvertable,completionHandler:@escaping?(ResponseResult)?->?Void){
????????let?requestPath?=?api.host?+?"/"+?api.path
????????_?=?Alamofire.request(requestPath,
??????????????????????????method:?api.method.toAlamofireMethod(),
??????????????????????????parameters:?api.requestParams,
??????????????????????????encoding:?api.requestEncoding.toAlamofireEncoding())
????????????.responseObject?{?(response:DataResponse)?in
????????????????????????????iflet?value?=?response.value{
????????????????????????????????completionHandler(ResponseResult.succeed(value:?value))
????????????????????????????}else{
????????????????????????????????completionHandler(ResponseResult.error(error:?response.error????NSError(domain:?"com.error.unknown",?code:-1,?userInfo:?nil)))
????????????????????????????}
????}
????}
}
enum?ResponseResult{
????casesucceed(value:Value)
????caseerror(error:Error)
}
于是佳簸,我們的網(wǎng)絡(luò)層封裝基本完成了乙墙。然后颖变,我們來定義我們的login API
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
enum?NetworkService{
????caselogin(userName:String,password:String)
????//Add?what?you?need
}
extension?NetworkService:?NetworkAPIConvertable{
????varhost:?String?{
????????return"https://api.example.com"
????}
????varrequestEncoding:?RequestEncoding?{
????????switchself?{
????????case.login(_,_):
????????????return.json
????????}
????}
????varrequestParams:?[String?:?Any]?{
????????switchself?{
????????case.login(let?userName,?let?password):
????????????return["username":userName,"password":password]
????????}
????}
????varpath:?String?{
????????switchself?{
????????case.login(_,_):
????????????return"user/login"
????????}
????}
????varmethod:?RequestMethod?{
????????switchself?{
????????case.login(_,_):
????????????return.post
????????}
????}
}
接著,網(wǎng)絡(luò)請求變成了
1
2
3
4
5
6
7
8
9
10
11
let?userName?=?"userName"
let?passWord?=?"password"
let?login?=?NetworkService.login(userName:?userName,?password:?passWord)
APIRouter.request(api:?login)?{?(response:ResponseResult)?in
????switchresponse{
????case.succeed(let?value):
????????????print(value.token)
????case.error(let?error):
????????????print(error)
????}
}
延伸閱讀:Moya
日志
大部分App都會做日志分析听想,于是你的代碼中不得不進行埋點:
1
2
3
4
5
func?tableView(_?tableView:?UITableView,?didSelectRowAt?indexPath:?IndexPath)?{
????tableView.deselectRow(at:?indexPath,?animated:?true)
????//發(fā)送日志
????Logger.collectWithContent(....)
}
當(dāng)你看這樣的代碼的時候腥刹,日志代碼也在看著你:
是不是很痛苦呢?
在抽離日志之前汉买,我們想想什么樣的日志模塊是我們想要的衔峰?
盡量不要侵入業(yè)務(wù)代碼
支持由后臺動態(tài)下發(fā)日志統(tǒng)計內(nèi)容
AOP是一種常見的日志統(tǒng)計解決:
1通過AOP的方式hook所有需要統(tǒng)計的UIView事件回調(diào),然后通過KVC的方式來獲取日志需要的數(shù)據(jù)蛙粘,是常見的無埋點日志解決方案垫卤。
比如很常見的友盟統(tǒng)計需要在viewWillAppear/viewWillDisappear中加入代碼:
1
2
3
4
5
6
7
8
9
10
-?(void)viewWillAppear:(BOOL)animated
{
????[superviewWillAppear:animated];
????[MobClick?beginLogPageView:@"Page1"];
}
-?(void)viewWillDisappear:(BOOL)animated
{
????[superviewWillDisappear:animated];
????[MobClick?endLogPageView:@"Page1"];
}
使用AOP的方式,代碼變成如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void?swizzle(Class?cls,SEL?originalSEL,SEL?swizzledSEL){
????Method?originalMethod?=?class_getInstanceMethod(cls,?originalSEL);
????Method?swizzledMethod?=?class_getInstanceMethod(cls,?swizzledSEL);
????method_exchangeImplementations(originalMethod,?swizzledMethod);
}
@implementation?UIViewController?(QTSwizzle)
+?(void)load{
????static?dispatch_once_t?onceToken;
????dispatch_once(&onceToken,?^{
????????swizzle(self.class,?@selector(viewWillAppear:),?@selector(sw_viewWillAppear:)));
????????swizzle(self.class,?@selector(viewWillDisappear:),?@selector(sw_viewWillDisappear:)));
????});
}
-?(void)qt_viewWillAppear:(BOOL)animated{
????[self?qt_viewWillAppear:animated];
????//?Log代碼
}
-?(void)qt_viewWillDisappear:(BOOL)animated{
????[self?qt_viewWillDisappear:animated];
????//?Log代碼
}
@end
可以看到出牧,我們通過AOP穴肘,在原有的viewWillAppear后動態(tài)插入的日志代碼,其他點擊事件也可以類似處理舔痕。另外评抚,Objective C有一個很方便的用來做AOP的開源框架:Aspects
細心的同學(xué)可能看到了,這塊的代碼我是以O(shè)bjective C作為例子的伯复,因為OC的Runtime特性慨代,可以很方便的做AOP。對于NSObject及其子類啸如,Swift也支持AOP侍匙,但是考慮到Swift的語言特性,關(guān)于Swift的無侵入日志叮雳,也許還可以方案:
一套支持日志統(tǒng)計的框架想暗。這個看起來工作量很大,但其實需要做大量日志統(tǒng)計的公司往往都有自己的一套XXUIKit债鸡,在基類里加入日志統(tǒng)計的基礎(chǔ)邏輯也未嘗不可
編譯期AOP江滨。這個僅局限于理論铛纬,就是
延伸閱讀:
iOS無埋點數(shù)據(jù)SDK實踐之路
消息轉(zhuǎn)發(fā)機制與Aspects源碼解析
數(shù)據(jù)存儲
iOS常用的本地數(shù)據(jù)存儲方案有幾種:
UserDefaults 用戶配置信息
File/Plist 少量的無須結(jié)構(gòu)化查詢的數(shù)據(jù)
KeyChain 密碼/證書等用戶認(rèn)證數(shù)據(jù)
數(shù)據(jù)庫 需要結(jié)構(gòu)化查詢的信息
iCloud
而數(shù)據(jù)庫往往是App的數(shù)據(jù)核心厌均。在iOS中:可以選擇數(shù)據(jù)庫技術(shù)有
CoreData - 對應(yīng)開源庫MagicalRecord
Sqlite直接封裝 - 對應(yīng)開源庫 FMDB
Realm
CoreData的坑比較多,想要用好需要比較高的學(xué)習(xí)成本告唆。Relam和Sqlite都是建立結(jié)構(gòu)化查詢數(shù)據(jù)庫的比較好的選擇棺弊。
使用FMDB,你的代碼類似這樣子的擒悬。
1
2
3
4
5
6
7
8
9
let?queue?=?FMDatabaseQueue(url:?fileURL)
queue.inTransaction?{?db,?rollback?in
????do{
????????trydb.executeUpdate("INSERT?INTO?foo?(bar)?VALUES?(?)",?values:?[1])
????????trydb.executeUpdate("INSERT?INTO?foo?(bar)?VALUES?(?)",?values:?[2])
????}?catch{
????????rollback.pointee?=?true
????}
}
可以看到模她,F(xiàn)MDB是把sqlite從C的API封裝成了Objective/Swfit等上層API。但是還是缺少了兩項比較核心的
ORM(Object Relational Mapping)從數(shù)據(jù)庫的表映射到Structs/Class
查詢語言懂牧。在代碼里進行SQL字符串的編寫是繁瑣的也容易出問題
于是侈净,通常你需要在FMDB(Sqlite)上在進行一層封裝尊勿,這一層封裝提供ORM和查詢語言。從而更有好的提供上層接口畜侦。類似的框架有:
WCDB 微信最近開源的數(shù)據(jù)庫
GYDataCenter
延伸閱讀:
微信移動端數(shù)據(jù)庫組件WCDB系列(一)-iOS基礎(chǔ)篇
微信移動端數(shù)據(jù)庫組件WCDB系列(二) — 數(shù)據(jù)庫修復(fù)三板斧
微信iOS SQLite源碼優(yōu)化實踐.md
路由
在iOS開發(fā)中元扔,UIViewController之間的跳轉(zhuǎn)是無法避免的一個問題。比如旋膳,一個ViewControllerA想要跳轉(zhuǎn)到ViewControllerB
1
2
3
4
#import?"ViewControllerB.h"
//...
ViewControllerB?*?vcb?=?[[ViewControllerB?alloc]?init];
[self.navigationController?pushViewController:vcb?animated:YES];
當(dāng)在一個類中import另一個類的時候澎语,這兩個類就形成了強耦合。
另外验懊,很多App都有一個用戶中心的界面擅羞,這個界面有一些特點就是會跳轉(zhuǎn)到很多界面。于是义图,日積月累减俏,這個類中,你會發(fā)現(xiàn)代碼編程了這個樣子:
1
2
3
4
5
6
7
8
ifindexPath.secion?==?0{
????ifindexPath.row?==?0{
????}elseif....
}elseifindexPath.section?==?1{
}
....
大量的if/else造成代碼難以閱讀歌溉,并且難以修改垄懂。
一個典型的解Controller與Controller解耦方案就是加一個中間層:路由,并且建立Module(模塊)來管理一組Controller痛垛。
類似這種的路由架構(gòu)草慧,在App啟動的時候,通過注入的方式把各個Module
一個典型的跳轉(zhuǎn)請求如下:
ControllerA發(fā)起跳轉(zhuǎn)請求Request
Router解析Request匙头,輪詢問各個Module漫谷,看看各個Module是否支持對應(yīng)的Requst。
如果有則把requst轉(zhuǎn)發(fā)給對應(yīng)的Module蹂析;
?如果沒有舔示,根據(jù)Request的內(nèi)容可選請求遠端服務(wù)器,服務(wù)器可能返回H5地址
Router根據(jù)遠端服務(wù)器电抚,或者Module的Response惕稻,合成跳轉(zhuǎn)的command,發(fā)送給導(dǎo)航模塊
導(dǎo)航模塊根據(jù)command進行跳轉(zhuǎn),并且返回feedBack給Router
Router返回feedback給ControllerA
總結(jié)
iOS App是一個麻雀雖小蝙叛,五臟俱全的軟件俺祠。良好的架構(gòu)和設(shè)計能夠讓代碼容易理解和維護,并且不易出錯借帘。關(guān)于App的設(shè)計一個仁者見仁蜘渣,智者見智的問題,并沒有什么固定的范式肺然。本文也只是提出了筆者的一些經(jīng)驗蔫缸,僅供參考,