原文:https://www.jianshu.com/p/4ef3d3737661?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io
作者:故胤道長(zhǎng)
在 iOS 開(kāi)發(fā)中,寫(xiě)一個(gè) App 很容易,但是要寫(xiě)好一個(gè) App,確是要下另一番功夫。首先,我們來(lái)看一個(gè) App 的開(kāi)發(fā)要求:
寫(xiě)一個(gè) App,顯示出 Spotify 上 Lady Gaga 相關(guān)的所有音樂(lè)專(zhuān)輯,相關(guān)信息可以通過(guò)以下網(wǎng)址查到:
https://api.spotify.com/v1/search?q=lady+gaga&type=album
需求分析
首先拿到開(kāi)發(fā)要求,最重要的是明確開(kāi)發(fā)細(xì)節(jié)。這里面有很多我們不清楚的地方需要與產(chǎn)品經(jīng)理和設(shè)計(jì)師交流:顯示是要用 TableView 還是 CollectionView?每個(gè)音樂(lè)專(zhuān)輯的哪些信息需要顯示?如果專(zhuān)輯數(shù)量過(guò)多,我們優(yōu)先顯示哪些專(zhuān)輯?這個(gè) App 除了顯示信息以外,還需要哪些拓展功能?這個(gè)產(chǎn)品的大小是否有要求?需要多少天完成?
經(jīng)過(guò)討論之后,大家的一致意見(jiàn)是做個(gè)如下的 App:
LadyGaga
于是我們就清楚了,是要做一個(gè) tableView,每個(gè) Cell 對(duì)應(yīng)一個(gè)專(zhuān)輯信息,左邊是圖片,右邊是專(zhuān)輯名。點(diǎn)擊 Cell,可以看到相應(yīng)的專(zhuān)輯大圖。
構(gòu)建架構(gòu)
首先這個(gè) App 比較簡(jiǎn)單,我們只要用最基本的 MVC就可做好。
Model 部分:
只需要一個(gè) Model, 為 Album,對(duì)應(yīng)每一個(gè)專(zhuān)輯的信息;
View 部分:
主體的部分可以在 Storyboard 里面完成;
最好單獨(dú)新建一個(gè) UITableViewCell 的子類(lèi),用來(lái)對(duì)應(yīng)設(shè)置專(zhuān)輯的UI;
ViewController 部分:
其中一個(gè) ViewController 為 TableViewController,負(fù)責(zé)現(xiàn)實(shí)所有專(zhuān)輯的信息;
另一個(gè) ViewController 負(fù)責(zé)展示 detail info,比如專(zhuān)輯的大圖;
Network 部分:
負(fù)責(zé)從網(wǎng)絡(luò)上 fetch 專(zhuān)輯信息;以及根據(jù)專(zhuān)輯的圖片網(wǎng)址,fetch 圖片數(shù)據(jù);
基本架構(gòu)
細(xì)節(jié)實(shí)現(xiàn)
Network 部分:
fetchAlbums 和 downloadImage 都用Apple 自帶的 URLSession 和 JSONserialization 就可以實(shí)現(xiàn),或者也可以用優(yōu)秀的第三方庫(kù) AlamoFire。因?yàn)檫@個(gè) App 比較簡(jiǎn)單,AlamoFire 優(yōu)勢(shì)不明顯,且引入第三方庫(kù)會(huì)增加 App 的體積,故而推薦使用前者?;旧暇褪菍?shí)現(xiàn)下面兩個(gè)函數(shù):
funcfetchAlbums(with url: String, completion : @escaping (_ albums: [Album]?, _ error : NSError?) -> Void) funcdownloadImage(_ url: String) -> UIImage?
對(duì)于第一個(gè)函數(shù) fetchAlbums,因?yàn)榫W(wǎng)絡(luò)請(qǐng)求是耗時(shí)耗力的工作,我們一般會(huì)將它們用后臺(tái)線程而非主線程(UI線程)來(lái)處理,這樣可以保持UI的流暢運(yùn)行。用閉包則是為了異步多線程完成后可以回調(diào),同時(shí) error 是為了監(jiān)視網(wǎng)絡(luò)請(qǐng)求是否出錯(cuò)。
對(duì)于第二個(gè)函數(shù) downloadImage,最簡(jiǎn)單的方法是通過(guò) url 拿到對(duì)應(yīng)的 data,然后通過(guò)相應(yīng)的 data 拿到 image。返回為 optional 的原因是有可能 URL 有問(wèn)題或者網(wǎng)絡(luò)請(qǐng)求出錯(cuò),此時(shí)返回 nil。
從API設(shè)計(jì)的角度來(lái)說(shuō),以上的downloadImage并不是最佳設(shè)計(jì)。最佳的設(shè)計(jì)是我們能知道哪里出錯(cuò)了,比如下面這樣:
enumDownloadImageError: Error{ caseInvalidURLcaseInvalidData}funcdownloadImage(_ url: String)throws -> UIImage { guardlet aUrl = URL(string: url) else { throwDownloadImageError.InvalidURL } do { let data = tryData(contentsOf: aUrl) iflet image = UIImage(data: data) { return image } else { throwDownloadImageError.InvalidData } } catch { throwDownloadImageError.InvalidURL } }
ViewController 部分:
對(duì)于 AlbumsController,我們用到了代理模式(Delegate),即將 tableView 代理到了 AlbumsController 上。我們只要實(shí)現(xiàn)相應(yīng)的 dataSource 和 delegate 方法即可。其中對(duì)于 dataSource 而言,有兩個(gè)方法是必須實(shí)現(xiàn)的,它們是:
functableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> IntfunctableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
同時(shí),AlbumsController 里面,還有兩個(gè)數(shù)組,一個(gè)用來(lái)裝專(zhuān)輯([Album]),一個(gè)用來(lái)裝圖片([UIImage?]),這樣我們只需下載數(shù)據(jù)一次,并將其存入相應(yīng)數(shù)組,之后就無(wú)需再次進(jìn)行相關(guān)的網(wǎng)絡(luò)請(qǐng)求了。也就是說(shuō),這兩個(gè)數(shù)組起到了緩存的作用。
具體的實(shí)現(xiàn)是:首先在 viewDidLoad() 中請(qǐng)求服務(wù)器取出相應(yīng)的數(shù)據(jù)。之后根據(jù)專(zhuān)輯數(shù)量設(shè)定 TableView 的相應(yīng)行數(shù)。在具體的一行當(dāng)中,我們可以根據(jù) indexPath 確定相應(yīng)的專(zhuān)輯。根據(jù)相應(yīng)專(zhuān)輯的圖片 URL ,我們可以拿到相應(yīng)的圖片,之后緩存進(jìn)圖片數(shù)組。由于我們復(fù)用了 TableView 的 Cell,所以如果不緩存圖片而每次去進(jìn)行網(wǎng)絡(luò)請(qǐng)求,會(huì)因?yàn)檠訒r(shí)很?chē)?yán)重而會(huì)造成圖片閃爍的后果。
最后兩個(gè) ViewController 之間的跳轉(zhuǎn)可以用 navigationController 來(lái)實(shí)現(xiàn)。
View 部分:
自定義 AlbumCell 可以保證 App 的擴(kuò)展性很好。同時(shí),為了處理有些專(zhuān)輯名字過(guò)長(zhǎng) Label 顯示不了的問(wèn)題,可以用autoshrink 來(lái)處理。
App 運(yùn)行流程
優(yōu)化拓展
上面的設(shè)計(jì)和實(shí)現(xiàn)比較理想化,現(xiàn)在我們要考慮一個(gè)邊界情況,假如網(wǎng)絡(luò)不穩(wěn)定,怎么辦?
一個(gè)簡(jiǎn)單的解決方法就是,當(dāng)網(wǎng)絡(luò)好的時(shí)候把數(shù)據(jù)下載下來(lái),存入 cache 和 storage 中,之后即使網(wǎng)絡(luò)中斷、App 崩潰,我們都能從 storage 中拿到相應(yīng)數(shù)據(jù)。
這里引入外觀模式(Facade),創(chuàng)建一個(gè)新的 class 名為 LibraryAPI,提供兩個(gè)接口:
funcgetAlbums(completion : @escaping (_ albums: [Album]?, _ error : NSError?) -> Void)funcgetImage(_ url: String) throws-> UIImage
這里的方法跟之前 Network 的不同之處在于:getAlbums 方法會(huì)先嘗試從 storage 中取出相應(yīng)數(shù)據(jù),如果沒(méi)有,則去訪問(wèn) Network,之后再把從 Network 中拿到的值存入 storage 中。這里面的實(shí)現(xiàn)有點(diǎn)復(fù)雜,牽涉到兩大模塊和多線程操作,但是我們并不必關(guān)心方法內(nèi)部的實(shí)現(xiàn),而僅僅關(guān)心接口,這就是外觀模式的優(yōu)點(diǎn)。同時(shí),LibraryAPI 這個(gè) class 最好用單例模式(singleton),因?yàn)樗鼞?yīng)該被當(dāng)做是全局 API 被各個(gè) ViewController 來(lái)訪問(wèn),同時(shí)這樣設(shè)計(jì)也節(jié)省資源。
優(yōu)化后的 App 流程
另外一個(gè)優(yōu)化點(diǎn)在于,假如我們一開(kāi)始拿到很多數(shù)據(jù) —— 例如10000 個(gè)專(zhuān)輯,那么我們?cè)撛趺床僮鳎?/p>
正確的做法是分頁(yè)。我們可以先只拿20個(gè),顯示在 TableView 上。當(dāng)用戶快滑到底端的時(shí)候,我們可以再取下面20個(gè),然后我們總共有40個(gè)在內(nèi)存中可以顯示,以此類(lèi)推。這樣做的好處是,我們無(wú)需下載所有的數(shù)據(jù),以最快、最流暢的方式布局 TableView,同時(shí)根據(jù)用戶的需求增加相應(yīng)的專(zhuān)輯數(shù)據(jù)。
最后一個(gè)優(yōu)化點(diǎn)在于,假如用戶上下滑動(dòng)很快,我們?nèi)绾文軌蛴米羁焖俣燃虞d圖片?
答案是用 operationQueue 來(lái)處理,當(dāng)前 cell 是可見(jiàn)的時(shí)候,我們就 resume 下載圖片的進(jìn)程,否則就 suspend。這樣保證了我們用有限的內(nèi)存和 CPU 去最高效的下載用戶需要、當(dāng)前要見(jiàn)的圖片。
值得一提的是,大家還可以借鑒 ASDK 的思路來(lái)進(jìn)一步優(yōu)化程序。
總結(jié)
本文從一個(gè)簡(jiǎn)單的 tableView App 說(shuō)起,談?wù)摿碎_(kāi)發(fā)一個(gè) App 的4個(gè)步驟:需求分析、構(gòu)建架構(gòu)、細(xì)節(jié)實(shí)現(xiàn)、優(yōu)化拓展。簡(jiǎn)單介紹了多線程和幾種設(shè)計(jì)模式,希望對(duì)大家有所幫助。
評(píng)論列表