在 iOS 開發中,寫一個 App 很容易,可是要寫好一個 App,確是要下另外一番功夫。首先,咱們來看一個 App 的開發要求:設計模式
寫一個 App,顯示出 Spotify 上 Lady Gaga 相關的全部音樂專輯,相關信息能夠經過如下網址查到:
https://api.spotify.com/v1/search?q=lady+gaga&type=albumapi
需求分析數組
首先拿到開發要求,最重要的是明確開發細節。這裏面有不少咱們不清楚的地方須要與產品經理和設計師交流:顯示是要用 TableView 仍是 CollectionView?每一個音樂專輯的哪些信息須要顯示?若是專輯數量過多,咱們優先顯示哪些專輯?這個 App 除了顯示信息之外,還須要哪些拓展功能?這個產品的大小是否有要求?須要多少天完成?緩存
通過討論以後,你們的一致意見是作個以下的 App:服務器
因而咱們就清楚了,是要作一個 tableView,每一個 Cell 對應一個專輯信息,左邊是圖片,右邊是專輯名。點擊 Cell,能夠看到相應的專輯大圖。網絡
構建架構多線程
首先這個 App 比較簡單,咱們只要用最基本的 MVC 就可作好。閉包
Model 部分:架構
只須要一個 Model, 爲 Album,對應每個專輯的信息;異步
View 部分:
主體的部分能夠在 Storyboard 裏面完成;
最好單獨新建一個 UITableViewCell 的子類,用來對應設置專輯的UI;
ViewController 部分:
其中一個 ViewController 爲 TableViewController,負責現實全部專輯的信息;
另外一個 ViewController 負責展現 detail info,好比專輯的大圖;
Network 部分:
負責從網絡上 fetch 專輯信息;以及根據專輯的圖片網址,fetch 圖片數據;
細節實現
Network 部分:
fetchAlbums 和 downloadImage 都用Apple 自帶的 URLSession 和 JSONserialization 就能夠實現,或者也能夠用優秀的第三方庫 AlamoFire。由於這個 App 比較簡單,AlamoFire 優點不明顯,且引入第三方庫會增長 App 的體積,故而推薦使用前者。基本上就是實現下面兩個函數:
func fetchAlbums(with url: String, completion : @escaping (_ albums: [Album]?, _ error : NSError?) -> Void)
func downloadImage(_ url: String) -> UIImage?
對於第一個函數 fetchAlbums,由於網絡請求是耗時耗力的工做,咱們通常會將它們用後臺線程而非主線程(UI線程)來處理,這樣能夠保持UI的流暢運行。用閉包則是爲了異步多線程完成後能夠回調,同時 error 是爲了監視網絡請求是否出錯。
對於第二個函數 downloadImage,最簡單的方法是經過 url 拿到對應的 data,而後經過相應的 data 拿到 image。返回爲 optional 的緣由是有可能 URL 有問題或者網絡請求出錯,此時返回 nil。
從API設計的角度來講,以上的downloadImage並非最佳設計。最佳的設計是咱們能知道哪裏出錯了,好比下面這樣:
enum DownloadImageError: Error {
case InvalidURL
case InvalidData
}
func downloadImage(_ url: String) throws -> UIImage {
guard let aUrl = URL(string: url) else {
throw DownloadImageError.InvalidURL
}
do {
let data = try Data(contentsOf: aUrl)
if let image = UIImage(data: data) {
return image
} else {
throw DownloadImageError.InvalidData
}
} catch {
throw DownloadImageError.InvalidURL
}
}
ViewController 部分:
對於 AlbumsController,咱們用到了代理模式(Delegate),即將 tableView 代理到了 AlbumsController 上。咱們只要實現相應的 dataSource 和 delegate 方法便可。其中對於 dataSource 而言,有兩個方法是必須實現的,它們是:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
同時,AlbumsController 裏面,還有兩個數組,一個用來裝專輯([Album]),一個用來裝圖片([UIImage?]),這樣咱們只需下載數據一次,並將其存入相應數組,以後就無需再次進行相關的網絡請求了。也就是說,這兩個數組起到了緩存的做用。
具體的實現是:首先在 viewDidLoad() 中請求服務器取出相應的數據。以後根據專輯數量設定 TableView 的相應行數。在具體的一行當中,咱們能夠根據 indexPath 肯定相應的專輯。根據相應專輯的圖片 URL ,咱們能夠拿到相應的圖片,以後緩存進圖片數組。因爲咱們複用了 TableView 的 Cell,因此若是不緩存圖片而每次去進行網絡請求,會由於延時很嚴重而會形成圖片閃爍的後果。
最後兩個 ViewController 之間的跳轉能夠用 navigationController 來實現。
View 部分:
自定義 AlbumCell 能夠保證 App 的擴展性很好。同時,爲了處理有些專輯名字過長 Label 顯示不了的問題,能夠用 autoshrink 來處理。
優化拓展
上面的設計和實現比較理想化,如今咱們要考慮一個邊界狀況,假如網絡不穩定,怎麼辦?
一個簡單的解決方法就是,當網絡好的時候把數據下載下來,存入 cache 和 storage 中,以後即便網絡中斷、App 崩潰,咱們都能從 storage 中拿到相應數據。
這裏引入外觀模式(Facade),建立一個新的 class 名爲 LibraryAPI,提供兩個接口:
func getAlbums(completion : @escaping (_ albums: [Album]?, _ error : NSError?) -> Void)
func getImage(_ url: String) throws -> UIImage
這裏的方法跟以前 Network 的不一樣之處在於:getAlbums 方法會先嚐試從 storage 中取出相應數據,若是沒有,則去訪問 Network,以後再把從 Network 中拿到的值存入 storage 中。這裏面的實現有點複雜,牽涉到兩大模塊和多線程操做,可是咱們並沒必要關心方法內部的實現,而僅僅關心接口,這就是外觀模式的優勢。同時,LibraryAPI 這個 class 最好用單例模式(singleton),由於它應該被當作是全局 API 被各個 ViewController 來訪問,同時這樣設計也節省資源。
優化後的 App 流程
另一個優化點在於,假如咱們一開始拿到不少數據 —— 例如10000 個專輯,那麼咱們該怎麼操做?
正確的作法是分頁。咱們能夠先只拿20個,顯示在 TableView 上。當用戶快滑到底端的時候,咱們能夠再取下面20個,而後咱們總共有40個在內存中能夠顯示,以此類推。這樣作的好處是,咱們無需下載全部的數據,以最快、最流暢的方式佈局 TableView,同時根據用戶的需求增長相應的專輯數據。
最後一個優化點在於,假如用戶上下滑動很快,咱們如何可以用最快速度加載圖片?
答案是用 operationQueue 來處理,當前 cell 是可見的時候,咱們就 resume 下載圖片的進程,不然就 suspend。這樣保證了咱們用有限的內存和 CPU 去最高效的下載用戶須要、當前要見的圖片。
值得一提的是,你們還能夠借鑑 ASDK 的思路來進一步優化程序。
總結
本文從一個簡單的 tableView App 提及,談論了開發一個 App 的4個步驟:需求分析、構建架構、細節實現、優化拓展。簡單介紹了多線程和幾種設計模式,但願對你們有所幫助。