MVVM與Controller瘦身實踐

前言

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)驗蔫缸,僅供參考,

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末际起,一起剝皮案震驚了整個濱河市拾碌,隨后出現(xiàn)的幾起案子吐葱,更是在濱河造成了極大的恐慌,老刑警劉巖校翔,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件唇撬,死亡現(xiàn)場離奇詭異,居然都是意外死亡展融,警方通過查閱死者的電腦和手機窖认,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來告希,“玉大人扑浸,你說我怎么就攤上這事⊙嗯迹” “怎么了喝噪?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長指么。 經(jīng)常有香客問我酝惧,道長,這世上最難降的妖魔是什么伯诬? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任晚唇,我火速辦了婚禮,結(jié)果婚禮上盗似,老公的妹妹穿的比我還像新娘哩陕。我一直安慰自己,他們只是感情好赫舒,可當(dāng)我...
    茶點故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布悍及。 她就那樣靜靜地躺著,像睡著了一般接癌。 火紅的嫁衣襯著肌膚如雪心赶。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天缺猛,我揣著相機與錄音缨叫,去河邊找鬼。 笑死枯夜,一個胖子當(dāng)著我的面吹牛弯汰,可吹牛的內(nèi)容都是我干的艰山。 我是一名探鬼主播湖雹,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼曙搬!你這毒婦竟也來了摔吏?” 一聲冷哼從身側(cè)響起鸽嫂,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎征讲,沒想到半個月后据某,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡诗箍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年癣籽,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片滤祖。...
    茶點故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡筷狼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出匠童,到底是詐尸還是另有隱情埂材,我是刑警寧澤,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布汤求,位于F島的核電站俏险,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏扬绪。R本人自食惡果不足惜竖独,卻給世界環(huán)境...
    茶點故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望挤牛。 院中可真熱鬧预鬓,春花似錦、人聲如沸赊颠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽竣蹦。三九已至顶猜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間痘括,已是汗流浹背长窄。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留纲菌,地道東北人挠日。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像翰舌,于是被迫代替她去往敵國和親嚣潜。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,884評論 2 354

推薦閱讀更多精彩內(nèi)容