翻譯自 Introducing iOS Design Patterns in Swift – Part 1/2 ,本教程 objc 版本的做者是 Eli Ganem ,由 Vincent Ngo 更新爲 Swift 版本。html
媽蛋這文章太長了,再加上天天加班到10點多基本沒時間,拖了10幾天才完成。。。ios
說到設計模式,相信你們都不陌生,可是又有多少人知道它背後的真正含義?絕大多數程序員都知道設計模式十分重要,不過關於這個話題的文章卻不是不少,開發者們在開發的時候有時也不太在乎設計模式方面的內容。git
設計模式針對軟件設計中的常見問題,提供了一些可複用的解決方案,開發者能夠經過這些模板寫出易於理解且可以複用的代碼。正確的使用設計模式能夠下降代碼之間的耦合度,從而很輕鬆的修改或者替換之前的代碼。程序員
若是你對設計模式還很陌生,那麼告訴你一個好消息!在 iOS 的開發過程當中,其實你不知不覺已經用了不少設計模式。這得益於 Cocoa 提供的框架和一些良好的編程習慣。接下來的這篇教程將會帶你一塊兒飛,去領略設計模式的魅力。github
整個教程分爲兩篇文章,經過整個系列的學習,咱們將會完成一個完整的應用,展現音樂專輯和專輯的相關信息。編程
經過這個應用,咱們會接觸一些 Cocoa 中常見的設計模式:swift
嘿嘿嘿別愁眉苦臉的嘛,這篇文章不是什麼長篇大論的理論知識,你會在開發應用的過程當中慢慢學會這些設計模式。設計模式
先來預覽一下最終的結果:api
看起來仍是不錯的,開始學習接下來的內容吧。勇敢的少年們,快來創造奇蹟!數組
下載初始項目並解壓,在 Xcode 中打開 BlueLibrarySwift.xcodeproj
項目文件。
項目中有三個地方須要注意一下:
ViewController
有兩個 IBOutlet
,分別鏈接到了 UITableView
和 UIToolBar
上。
在 StoryBoard 上有三個組件設置了約束。最上面的是專輯的封面,封面下面是列舉了相關專輯的列表,最下面是有兩個按鈕的工具欄,一個用來撤銷操做,另外一個用來刪除你選中的專輯。 StoryBoard 看起來是這個樣子的:
HTTP
客戶端類 (HTTPClient
) ,裏面尚未什麼內容,須要你去完善。注意:其實當你建立一個新的 Xcode 的項目的時候,你的代碼裏就已經有不少設計模式的影子了: MVC、委託、代理、單例 - 真是衆裏尋他千百度,得來全不費功夫。
在學習第一個設計模式以前,你須要建立兩個類,用來存儲和展現專輯數據。
建立一個新的類,繼承 NSObject
名爲 Album
,記得選擇 Swift 做爲編程語言而後點擊下一步。
打開 Album.swift
而後添加以下定義:
var title : String! var artist : String! var genre : String! var coverUrl : String! var year : String!
這裏建立了五個屬性,分別對應專輯的標題、做者、流派、封面地址和出版年份。
接下來咱們添加一個初始化方法:
init(title: String, artist: String, genre: String, coverUrl: String, year: String) { super.init() self.title = title self.artist = artist self.genre = genre self.coverUrl = coverUrl self.year = year }
這樣咱們就能夠愉快的初始化了。
而後再加上下面這個方法:
func description() -> String { return "title: \(title)" + "artist: \(artist)" + "genre: \(genre)" + "coverUrl: \(coverUrl)" + "year: \(year)" }
這是專輯對象的描述方法,詳細的打印了 Album
的全部屬性值,方便咱們查看變量各個屬性的值。
接下來,再建立一個繼承自 UIView
的視圖類 AlbumView.swift
。
在新建的類中添加兩個屬性:
private let coverImage: UIImageView! private let indicator: UIActivityIndicatorView!
coverImage
表明了封面的圖片,indicator
則是在加載過程當中顯示的等待指示器。
這兩個屬性都是私有屬性,由於除了 AlbumView
以外,其餘類沒有必要知道他倆的存在。在寫一些框架或者類庫的時候,這種規範十分重要,能夠避免一些誤操做。
接下來給這個類添加初始化化方法:
required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } init(frame: CGRect, albumCover: String) { super.init(frame: frame) backgroundColor = UIColor.blackColor() coverImage = UIImageView(frame: CGRectMake(5, 5, frame.size.width - 10, frame.size.height - 10)) addSubview(coverImage) indicator = UIActivityIndicatorView() indicator.center = center indicator.activityIndicatorViewStyle = .WhiteLarge indicator.startAnimating() addSubview(indicator) }
由於 UIView
聽從 NSCoding
協議,因此咱們須要 NSCoder
的初始化方法。不過目前咱們沒有 encode
和 decode
的必要,因此就把它放在那裏就行,調用父類方法初始化便可。
在真正的初始化方法裏,咱們設置了一些初始化的默認值。好比設置背景顏色默認爲黑色,建立 ImageView
並設置了 margin
值,添加了一個加載指示器。
最終咱們再加上以下方法:
func highlightAlbum(#didHighlightView: Bool) { if didHighlightView == true { backgroundColor = UIColor.whiteColor() } else { backgroundColor = UIColor.blackColor() } }
這會切換專輯的背景顏色,若是高亮就是白色,不然就是黑色。
在繼續下面的內容以前, Command + B
試一下確保沒有什麼問題,一切正常?那就開始第一個設計模式的學習啦!:]
Model-View-Controller
(縮寫 MVC ) 是 Cocoa 框架的一部分,而且毋庸置疑是最經常使用的設計模式之一。它能夠幫你把對象根據職責進行劃分和歸類。
做爲劃分依據的三個基本職責是:
Album
類。UIView
這個基類。在咱們的項目中就是 AlbumView
這個類。ViewController
這個類就是。若是你的項目遵循 MVC 的設計模式,那麼各類對象要不是 Model ,要不是 View ,要不就是 Controller。固然在實際的開發中也能夠靈活變化,好比結合具體業務使用 MVVM 結構給 ViewController
瘦瘦身,也是能夠的。
三者之間的關係以下:
模型層通知控制器層任何數據的變化,而後控制器層會刷新視圖層中的數據。視圖層能夠通知控制器層用戶的交互事件,而後控制器會處理各類事件以及刷新數據。
你可能會感受奇怪:爲何要把這三個東西分開來,而不能揉在一個類裏呢?那樣彷佛更簡單一點嘛。
Naive.
之因此這樣作,是爲了將代碼更好的分離和重用。理想狀態下,視圖層應當和模型層徹底分離。若是視圖層不依賴任何模型層的具體實現,那麼就能夠很容易的被其餘模型複用,用來展現不一樣的數據。
舉個例子,好比在將來咱們須要添加電影或者什麼書籍,咱們依舊可使用 AlbumView
這個類做爲展現。更久遠點來講,在之後若是你建立了一個新的項目而且須要用到和專輯相關的內容,你能夠直接複用 Album
類由於它並不依賴於任何視圖模塊。這就是 MVC 的強大之處,三大元素,各司其職,減小依賴。
首先,你須要肯定你的項目中的每一個類都是三大基本類型中的一種:控制器、模型、視圖。不要在一個類裏糅合多個角色。目前咱們建立了 Album
類和 AlbumView
類是符合要求的,作得很好。
而後,爲了確保你遵循這種模式,你最好建立三個項目分組來存放代碼,分別是 Model、View、Controller,保持每一個類型的文件分別獨立。
接下來把 Album.swift
拖到 Model
分組,把 AlbumView.swift
拖到 View
分組,而後把 ViewController.swift
拖到 Controller
分組中。
如今你的項目應該是這個樣子:
如今你的項目已經有點樣子了,再也不是各個文件顛沛流離居無定所了。顯然你還會有其餘分組和類,可是應用的核心就在這三個類裏。
如今你的內容已經組織好了,接下來要作的就是獲取專輯的數據。你將會建立一個 API 類來管理數據 - 這裏咱們會用到下一個設計模式:單例模式。
單例模式確保每一個指定的類只存在一個實例對象,而且能夠全局訪問那個實例。通常狀況下會使用延時加載的策略,只在第一次須要使用的時候初始化。
注意:在 iOS 中單例模式很常見,NSUserDefaults.standardUserDefaults()
、 UIApplication.sharedApplication()
、 UIScreen.mainScreen()
、 NSFileManager.defaultManager()
這些都是單例模式。
你可能會疑惑了:若是多於一個實例又會怎麼樣呢?代碼和內存還沒精貴到這個地步吧?
某些場景下,保持實例對象僅有一份是頗有意義的。舉個例子,你的應用實例 (UIApplication
),應該只有一個吧,顯然是指你的當前應用。還有一個例子:設備的屏幕 (UIScreen
) 實例也是這樣,因此對於這些類的狀況,你只想要一個實例對象。
單例模式的應用還有另外一種狀況:你須要一個全局類來處理配置文件。咱們很容易經過單例模式實現線程安全的實例訪問,而若是有多個類能夠同時訪問配置文件,那可就複雜多了。
能夠看下這個圖:
這是一個日誌類,有一個屬性 (是一個單例對象) 和兩個方法 (sharedInstance()
和 init()
)。
第一次調用 sharedInstance()
的時候,instance
屬性尚未初始化。因此咱們要建立一個新實例而且返回。
下一次你再調用 sharedInstance()
的時候,instance
已經初始化完成,直接返回便可。這個邏輯確保了這個類只存在一個實例對象。
接下來咱們繼續完善單例模式,經過這個類來管理專輯數據。
注意到在咱們前面的截圖裏,分組中有個 API
分組,這裏能夠放那些提供後臺服務的類。在這個分組中建立一個新的文件 LibraryAPI.swift
,繼承自 NSObject
類。
在 LibraryAPI
裏添加下面這段代碼:
//1 class var sharedInstance: LibraryAPI { //2 struct Singleton { //3 static let instance = LibraryAPI() } //4 return Singleton.instance }
在這幾行代碼裏,作了以下工做:
建立一個計算類型的類變量,這個類變量,就像是 objc 中的靜態方法同樣,能夠直接經過類訪問而不用實例對象。具體可參見蘋果官方文檔的 屬性 這一章。
在類變量裏嵌套一個 Singleton
結構體。
Singleton
封裝了一個靜態的常量,經過 static
定義意味着這個屬性只存在一個,注意 Swift 中 static
的變量是延時加載的,意味着 Instance
直到須要的時候纔會被建立。同時再注意一下,由於它是一個常量,因此一旦建立以後不會再建立第二次。這些就是單例模式的核心所在:一旦初始化完成,當前類存在一個實例對象,初始化方法就不會再被調用。
返回計算後的屬性值。
注意:更多的單例模式實例能夠看看 Github
上的這個示例,列舉了單例模式的若干種實現方式。
你如今能夠將這個單例做爲專輯管理類的入口,接下來咱們繼續建立一個處理專輯數據持久化的類。
新建 PersistencyManager.swift
並添加以下代碼:
private var albums = [Album]()
在這裏咱們定義了一個私有屬性,用來存儲專輯數據。這是一個可變數組,因此你能夠很容易的增長或者刪除數據。
而後加上一些初始化的數據:
override init() { //Dummy list of albums let album1 = Album(title: "Best of Bowie", artist: "David Bowie", genre: "Pop", coverUrl: "http://www.coversproject.com/static/thumbs/album/album_david%20bowie_best%20of%20bowie.png", year: "1992") let album2 = Album(title: "It's My Life", artist: "No Doubt", genre: "Pop", coverUrl: "http://www.coversproject.com/static/thumbs/album/album_no%20doubt_its%20my%20life%20%20bathwater.png", year: "2003") let album3 = Album(title: "Nothing Like The Sun", artist: "Sting", genre: "Pop", coverUrl: "http://www.coversproject.com/static/thumbs/album/album_sting_nothing%20like%20the%20sun.png", year: "1999") let album4 = Album(title: "Staring at the Sun", artist: "U2", genre: "Pop", coverUrl: "http://www.coversproject.com/static/thumbs/album/album_u2_staring%20at%20the%20sun.png", year: "2000") let album5 = Album(title: "American Pie", artist: "Madonna", genre: "Pop", coverUrl: "http://www.coversproject.com/static/thumbs/album/album_madonna_american%20pie.png", year: "2000") albums = [album1, album2, album3, album4, album5] }
在這個初始化方法裏,咱們初始化了五張專輯。若是上面的專輯沒有你喜歡的,你能夠隨意替換成你的菜:]
而後添加以下方法:
func getAlbums() -> [Album] { return albums } func addAlbum(album: Album, index: Int) { if (albums.count >= index) { albums.insert(album, atIndex: index) } else { albums.append(album) } } func deleteAlbumAtIndex(index: Int) { albums.removeAtIndex(index) }
這些方法可讓你自由的訪問、添加、刪除專輯數據。
這時你能夠運行一下你的項目,確保編譯經過以便進行下一步操做。
此時你或許會感到好奇: PersistencyManager
好像不是單例啊?是的,它確實不是單例。不過不要緊,在接下來的外觀模式章節,你會看到 LibraryAPI
和 PersistencyManager
之間的聯繫。
外觀模式在複雜的業務系統上提供了簡單的接口。若是直接把業務的全部接口直接暴露給使用者,使用者須要單獨面對這一大堆複雜的接口,學習成本很高,並且存在誤用的隱患。若是使用外觀模式,咱們只要暴露必要的 API 就能夠了。
下圖演示了外觀模式的基本概念:
API 的使用者徹底不知道這內部的業務邏輯有多麼複雜。當咱們有大量的類而且它們使用起來很複雜並且也很難理解的時候,外觀模式是一個十分理想的選擇。
外觀模式把使用和背後的實現邏輯成功解耦,同時也下降了外部代碼對內部工做的依賴程度。若是底層的類發生了改變,外觀的接口並不須要作修改。
舉個例子,若是有一天你想換掉全部的後臺服務,你只須要修改 API 內部的代碼,外部調用 API 的代碼並不會有改動。
如今咱們用 PersistencyManager
來管理專輯數據,用 HTTPClient
來處理網絡請求,項目中的其餘類不該該知道這個邏輯。他們只須要知道 LibraryAPI
這個「外觀」就能夠了。
爲了實現外觀模式,應該只讓 LibraryAPI
持有 PersistencyManager
和 HTTPClient
的實例,而後 LibraryAPI
暴露一個簡單的接口給其餘類來訪問,這樣外部的訪問類不須要知道內部的業務具體是怎樣的,也不用知道你是經過 PersistencyManager
仍是 HTTPClient
獲取到數據的。
大體的設計是這樣的:
LibraryAPI
會暴露給其餘代碼訪問,可是 PersistencyManager
和 HTTPClient
則是不對外開放的。
打開 LibraryAPI.swift
而後添加以下代碼:
private let persistencyManager: PersistencyManager private let httpClient: HTTPClient private let isOnline: Bool
除了兩個實例變量以外,還有個 Bool
值: isOnline
,這個是用來標識當前是否爲聯網狀態的,若是是聯網狀態就會去網絡獲取數據。
咱們須要在 init
裏面初始化這些變量:
override init() { persistencyManager = PersistencyManager() httpClient = HTTPClient() isOnline = false super.init() }
HTTPClient
並不會直接和真實的服務器交互,只是用來演示外觀模式的使用。因此 inOnline
這個值咱們一直設置爲 false
。
接下來在 LibraryAPI.swift
裏添加以下代碼:
func getAlbums() -> [Album] { return persistencyManager.getAlbums() } func addAlbum(album: Album, index: Int) { persistencyManager.addAlbum(album, index: index) if isOnline { httpClient.postRequest("/api/addAlbum", body: album.description()) } } func deleteAlbum(index: Int) { persistencyManager.deleteAlbumAtIndex(index) if isOnline { httpClient.postRequest("/api/deleteAlbum", body: "\(index)") } }
看一下 addAlbum(_:index:)
這個方法,先更新本地緩存,而後若是是聯網狀態還須要向服務器發送網絡請求。這即是外觀模式的強大之處:若是外部文件想要添加一個新的專輯,它不會也不用去了解內部的實現邏輯是怎麼樣的。
注意:當你設計外觀的時候,請務必牢記:使用者隨時可能直接訪問你的隱藏類。永遠不要假設使用者會遵循你當初的設計作事。
運行一下你的應用,你能夠看到兩個空的頁面和一個工具欄:最上面的視圖用來展現專輯封面,下面的視圖展現數據列表。
你須要在屏幕上展現專輯數據,這是就該用下一種設計模式了:裝飾者模式。
裝飾者模式能夠動態的給指定的類添加一些行爲和職責,而不用對原代碼進行任何修改。當你須要使用子類的時候,不妨考慮一下裝飾者模式,能夠在原始類上面封裝一層。
在 Swift 裏,有兩種方式實現裝飾者模式:擴展 (Extension) 和委託 (Delegation)。
擴展是一種十分強大的機制,可讓你在不用繼承的狀況下,給已存在的類、結構體或者枚舉類添加一些新的功能。最重要的一點是,你能夠在你沒有訪問權限的狀況下擴展已有類。這意味着你甚至能夠擴展 Cocoa 的類,好比 UIView
或者 UIImage
。
舉個例子,在編譯時新加的方法能夠像擴展類的正常方法同樣執行。這和裝飾器模式有點不一樣,由於擴展不會持有擴展類的對象。
想象一下這個場景,咱們須要在下面這個列表裏展現數據:
專輯標題從哪裏來? Album
自己是個 Model
對象,因此它不該該負責如何展現數據。你須要一些額外的代碼添加展現數據的邏輯,可是爲了保持 Model
的乾淨,咱們不該該直接修改代碼,由於這樣不符合單一職責原則。 Model
層最好就是負責純粹的數據結構,若是有數據的操做能夠放到擴展中完成。
接下來咱們會建立一個擴展,擴展示有的 Album
類,在擴展裏定義了新的方法,返回更適合 UITableView
展現用的數據結構。
數據的結構大概是這樣:
新建一個 Swift 文件:AlbumExtensions
,在裏面添加以下擴展:
extension Album { func ae_tableRepresentation() -> (titles:[String], values:[String]) { return (["Artist", "Album", "Genre", "Year"], [artist, title, genre, year]) } }
在方法的前面有個 ae_
前綴,是 AlbumExtension
的縮寫,這樣有利於和類的原有方法進行區分,避免使用的時候產生衝突。如今不少還在維護中的第三方庫都已經改爲了這個風格。
注意:類是能夠重寫父類方法的,可是在擴展裏不能夠。擴展裏的方法和屬性不能和原始類裏的方法和屬性衝突。
思考一下這個設計模式的強大之處:
Album
裏的屬性。Album
類添加了內容可是並無繼承它,事實上,使用繼承來擴展業務也能夠實現同樣的功能。Album
的數據展現在 UITableView
裏,並且不用修改源碼。裝飾者模式的另外一種實現方案是委託。在這種機制下,一個對象能夠和另外一個對象相關聯。好比你在用 UITableView
,你必須實現 tableView(_:numberOfRowsInSection:)
這個委託方法。
你不該該期望 UITableView
知道你有多少數據,這是個應用層該解決的問題。因此,數據相關的計算應該經過 UITableView
的委託來解決。這樣可讓 UITableView
和數據層分別獨立。視圖層就負責顯示數據,你遞過來什麼我就顯示什麼。
下面這張圖很好的解釋了 UITableView
的工做過程:
UITableView
的工做僅僅是展現數據,可是最終它須要知道本身要展現那些數據,這時就能夠向它的委託詢問。在 objc 的委託模式裏,一個類能夠經過協議來聲明可選或者必須的方法。
看起來彷佛繼承而後重寫必須的方法來的更簡單一點。可是考慮一下這個問題:繼承的結果一定是一個獨立的類,若是你想讓某個對象成爲多個對象的委託,那麼子類這招就行不通了。
注意:委託模式十分重要,蘋果在 UIKit 中大量使用了該模式,基本上隨處可見。
打開 ViewController.swift
文件,添加以下私有變量:
private var allAlbums = [Album]() private var currentAlbumData : (titles:[String], values:[String])? private var currentAlbumIndex = 0
在 viewDidLoad
裏面加入以下內容:
override func viewDidLoad() { super.viewDidLoad() //1 self.navigationController?.navigationBar.translucent = false currentAlbumIndex = 0 //2 allAlbums = LibraryAPI.sharedInstance.getAlbums() // 3 // the uitableview that presents the album data dataTable.delegate = self dataTable.dataSource = self dataTable.backgroundView = nil view.addSubview(dataTable!) }
對上面三個部分進行拆解:
關閉導航欄的透明效果
經過 API 獲取全部的專輯數據,記住,咱們使用外觀模式以後,應該從 LibraryAPI
獲取數據,而不是 PersistencyManager
。
你能夠在這裏設置你的 UITablweView
,在這裏聲明瞭 UITableView
的 delegate
是當前的 ViewController
。事實上你用了 XIB 或者 StoryBoard ,能夠直接在可視化的頁面裏拖拽完成。
接下來添加一個新的方法用來更方便的獲取數據:
func showDataForAlbum(albumIndex: Int) { // defensive code: make sure the requested index is lower than the amount of albums if (albumIndex < allAlbums.count && albumIndex > -1) { //fetch the album let album = allAlbums[albumIndex] // save the albums data to present it later in the tableview currentAlbumData = album.ae_tableRepresentation() } else { currentAlbumData = nil } // we have the data we need, let's refresh our tableview dataTable!.reloadData() }
showDataForAlbum()
這個方法獲取最新的專輯數據,當你想要展現新數據的時候,你須要調用 reloadData()
這個方法,這樣 UITableView
就會向委託請求數據,好比有多少個 section
有多少個 row
之類的。
在 viewDidLoad
裏面調用上面的方法:
self.showDataForAlbum(currentAlbumIndex)
這樣應用一啓動就會去加載當前的專輯數據。由於 currentAlbumIndex
的默認值是 0 ,因此一開始會默認顯示第一章專輯的信息。
接下來咱們該去完善 DataSource
的協議方法了。你能夠直接把委託方法寫在類裏面,固然若是你想讓你的代碼看起來更整潔一點,則能夠放在擴展裏。
在文件底部添加以下方法,注意必定要放在類定義的大括號外面,由於這兩個傢伙不是類定義的一部分,它們是擴展:
extension ViewController: UITableViewDataSource { } extension ViewController: UITableViewDelegate { }
上面就是實現委託的方法 - 你能夠把協議想象成是與委託之間的約定,只要你實現了約定的方法,就算是實現了委託。在咱們的代碼中, ViewController
須要遵照 UITableViewDataSource
和 UITableViewDelegate
的協議。這樣 UITableView
才能確保必要的委託方法都已經實現了。
在 UITableViewDataSource
對應的那個擴展里加上以下方法:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if let albumData = currentAlbumData { return albumData.titles.count } else { return 0 } } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { var cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell if let albumData = currentAlbumData { cell.textLabel?.text = albumData.titles[indexPath.row] if let detailTextLabel = cell.detailTextLabel { detailTextLabel.text = albumData.values[indexPath.row] } } return cell }
tableView(_:numberOfRowsInSection:)
返回須要展現的行數,和存儲的數據中的 title 的數目相同。
tableView(_:cellForRowAtIndexPath:)
建立而且返回了一個單元格,上面有標題和對應的值。
注意:你能夠把這些方法直接加在類聲明裏面,也能夠放在擴展裏,編譯器不會去管數據源到底在哪裏,只要能找到對應的方法就能夠了。而咱們之因此這樣作,是爲了方便其餘人閱讀。
此時再構建項目,你能夠看到以下內容:
是的,顯示成功啦!目前的項目源碼在這裏:BlueLibrarySwift-Part1,若是遇到什麼問題你能夠下載下來對比一下。
下一張咱們會繼續設計模式的內容,敬請期待!
原文連接: