[譯] 使用 Swift 的 iOS 設計模式(第一部分)

使用 Swift 的 iOS 設計模式(第一部分)

在這個由兩部分組成的教程中,你將瞭解構建 iOS 應用程序的常見設計模式,以及如何在本身的應用程序中應用這些模式。前端

更新說明:本教程已由譯者針對 iOS 12,Xcode 10 和 Swift 4.2 進行了更新。原帖由教程團隊成員 Eli Ganem發佈。android

iOS設計模式 — 你可能已經聽過這個術語,可是你知道這意味着什麼嗎?儘管大多數開發人員可能都認爲設計模式很是重要,關於這個主題的文章並很少,咱們開發人員在編寫代碼時有時不會過多地關注設計模式。ios

設計模式是軟件設計中常見問題的可重用解決方案。它們的模板旨在幫助你編寫易於理解和重用的代碼。它們還能夠幫助你建立低耦合度的代碼,以便你能更改或替換代碼中的組件而避免不少麻煩。git

若是你對設計模式不熟悉,那麼我有個好消息要告訴你!首先,因爲 Cocoa 的架構方式以及它鼓勵你使用的最佳實踐,你已經使用過了大量的 iOS 設計模式。其次,本教程將快速幫助你理解 Cocoa 中經常使用的全部重要(還有不那麼重要)的 iOS 設計模式。github

在這個由兩部分組成的教程中,你將建立一個音樂應用程序,用於顯示你的專輯及其相關信息。swift

在開發此應用程序的過程當中,你將熟悉最多見的 Cocoa 設計模式:後端

  • 建立型:單例。
  • 結構型:MVC、裝飾、適配器和外觀。
  • 行爲型:觀察者和備忘錄。

不要誤覺得這是一篇關於理論的文章,你將在音樂應用中使用大多數這些設計模式。在本教程結束時,你的應用將以下所示:設計模式

How the album app will look when the design patterns tutorial is complete

讓咱們開始吧!api

入門

下載 入門項目,解壓縮 ZIP 文件的內容,並在 Xcode 中打開 RWBlueLibrary.xcodeproj數組

請注意項目中的如下內容:

  1. 在 storyboard 裏,ViewController 有三個 IBOutlet 鏈接了 TableView,還有撤消和刪除按鈕按鈕。
  2. Storyboard 有 3 個組件,爲方便起見咱們設置了約束。頂部組件是用來顯示專輯封面的。專輯封面下方是一個 TableView,其中列出了與專輯封面相關的信息。 最後,工具欄有兩個按鈕,一個用於撤消操做,另外一個用於刪除你選擇的專輯。Storyboard 以下所示:

swiftDesignPatternStoryboard

  1. 有一個沒有實現的初始 HTTP 客戶端類(HTTPClient),供你稍後填寫。

注意:你知道嗎,只要你建立新的 Xcode 項目,就已經充滿了設計模式了嘛?模型-視圖-控制器,代理,協議,單例 — 這些設計模式都是現成的!

MVC – 設計模式之王

mvcking

模型 - 視圖 - 控制器(MVC)是 Cocoa 的構建模塊之一,它無疑是全部設計模式中最經常使用的。它將應用內對象按照各自經常使用角色進行分類,並提倡將代碼基於角色進行解耦。

這三個角色是:

  • 模型(Model):Model 是你的應用中持有並定義如何操做數據的對象。例如,在你的應用程序中,模型是 Album 結構體,你能夠在 Album.swift 中找到它。大多數應用程序將具備多個類型做爲其模型的一部分。
  • 視圖(View):View 是用來展現 model 的數據並管理可與用戶交互的控件的對象,基本上能夠說是全部 UIView 派生的對象。 在你的應用程序中,視圖是 AlbumView,你能夠在 AlbumView.swift 中找到它。
  • 控制器(Controller):控制器是協調全部工做的中介。它訪問模型中的數據並將其與視圖一塊兒顯示,監聽事件並根據須要操做數據。你能猜出哪一個類是你的控制器嗎?沒錯,就是 ViewController

你的 App 要想規範地使用 MVC 設計模式,就意味着你 App 中每一個對象均可以劃分爲這三個角色其中的某一個。

經過控制器(Controller)能夠最好地描述視圖(View)到模型(Model)之間的通訊,以下圖所示:

mvc0

模型通知控制器任何數據更改,反過來,控制器更新視圖中的數據。而後,視圖能夠向控制器通知用戶執行的操做,控制器將在必要時更新模型或檢索任何請求的數據。

你可能想知道爲何你不能拋棄控制器,並在同一個類中實現視圖和模型,由於這看起來會容易得多。

這一切都將歸結爲代碼分離和可重用性。理想狀況下,視圖應與模型徹底分離。若是視圖不依賴於模型的特定實現,那麼可使用不一樣的模型重用它來呈現其餘一些數據。

例如,若是未來你還想將電影或書籍添加到庫中,你仍然可使用相同的 AlbumView 來顯示電影和書籍對象。此外,若是你想建立一個與專輯有關的新項目,你能夠簡單地重用你的 Album 結構體,由於它不依賴於任何視圖。這就是MVC的力量!

如何使用 MVC 設計模式

首先,你須要確保項目中的每一個類都是Controller、Model 或 View,不要在一個類中組合兩個角色的功能。

其次,爲了確保你符合這種工做方法,你應該建立三個文件夾來保存你的代碼,每一個角色一個。

點擊 **File\New\Group(或者按 Command + Option + N)**並把改組名爲 Model。重複相同的過程以建立 View 和 Controller 組。

如今將 Album.swift 拖拽到 Model 組。將 AlbumView.swift 拖拽到 View 組,最後將 ViewController.swift 拖拽到 Controller 組。

此時項目結構應以下所示:

若是沒有全部這些文件夾,你的項目看起來會好不少。顯然,你能夠擁有其餘組和類,但應用程序的核心將包含在這三個類別中。

如今你的組件已組織完畢,你須要從某個位置獲取相冊數據。你將建立一個 API 類,在整個代碼中使用它來管理數據,這提供了討論下一個設計模式的機會 — 單例(Singleton)。

單例模式

單例設計模式確保給定類只會存在一個實例,而且該實例有一個全局的訪問點。它一般使用延遲加載來在第一次須要時建立單個實例。

注意:Apple 使用了不少這個方法。例如:UserDefaults.standardUIApplication.sharedUIScreen.mainFileManager.default 都返回一個單例對象。

你可能想知道爲何你關心的是一個類有不僅一個實例。代碼和內存不是都很廉價嗎?

在某些狀況下,只有一個實例的類纔有意義。例如,你的應用程序只有一個實例,設備也只有一個主屏幕,所以你只須要一個實例。再者,採用全局配置處理程序類,他更容易實現對單個共享資源(例如配置文件)的線程安全訪問,而不是讓許多類可能同時修改配置文件。

你應該注意什麼?

注意事項:這種模式有被初學者和有經驗的開發着濫用(或誤用)的歷史,所以咱們將 Joshua Greene 的 Design Patterns by Tutorials 一書中的一段簡述摘錄至此,其中解釋了使用這種模式的一些須要注意的事項。

單例模式很容易被濫用。

若是你遇到一種想要使用單例的狀況,請首先考慮是否還有其餘的方法來完成你的任務。

例如,若是你只是嘗試將信息從一個視圖控制器傳遞到另外一個視圖控制器,則不適合使用單例。可是你能夠考慮經過初始化程序或屬性傳遞該模型。

若是你肯定你確實須要一個單例,那麼考慮拓展單例是否會更有意義。

有多個實例會致使問題嗎?自定義實例會有用嗎?你的答案將決定你是否更好地使用真正的單例或其拓展。

用單例時遇到問題的最多見的緣由是測試。若是你將狀態存儲在像單例這樣的全局對象中,則測試順序可能很重要,而且模擬它們會很煩人。這兩個緣由都會使測試成爲一種痛苦。

最後,要注意「代碼異味」,它代表你的用例根本不適合使用單例。例如,若是你常常須要許多自定義實例,那麼你的用例可能會更好地做爲常規對象。

如何使用單例模式

爲了確保你的單例只有一個實例,你必須讓其餘任何人都沒法建立實例。Swift 容許你經過將初始化方法標記爲私有來完成此操做,而後你能夠爲共享實例添加靜態屬性,該屬性在類中初始化。

你將經過建立一個單例來管理全部專輯數據從而實現此模式。

你會注意到項目中有一個名爲 API 的組,這是你將全部將爲你的應用程序提供服務的類的地方。右鍵單擊該組並選擇 New File,在該組中建立一個新文件,選擇 iOS > Swift File。將文件名設置爲 LibraryAPI.swift,而後單擊 Create

如今打開 LibraryAPI.swift 並插入代碼:

final class LibraryAPI {
  // 1
  static let shared = LibraryAPI()
  // 2
  private init() {

  }
}
複製代碼

如下是詳細分析:

  1. 其中 shared 聲明的常量使得其餘對象能夠訪問到單例對象 LibraryAPI
  2. 私有的初始化方法防止從外部建立 LibraryAPI 的新實例。

你如今有一個單例對象做爲管理專輯的入口。接下來建立一個類來持久化庫裏的數據。

如今在 API 組裏建立一個新文件。 選擇 iOS > Swift File。將類名設置爲 PersistencyManager.swift,而後單擊 Create

打開 PersistencyManager.swift 並添加如下代碼:

final class PersistencyManager {

}
複製代碼

在括號裏面添加如下代碼:

private var albums = [Album]()
複製代碼

在這裏,你聲明一個私有屬性來保存專輯數據。該數組將是可變的,所以你能夠輕鬆添加和刪除專輯。

如今將如下初始化方法添加到類中:

init() {
  //Dummy list of albums
  let album1 = Album(title: "Best of Bowie",
                     artist: "David Bowie",
                     genre: "Pop",
                     coverUrl: "https://s3.amazonaws.com/CoverProject/album/album_david_bowie_best_of_bowie.png",
                     year: "1992")
    
  let album2 = Album(title: "It's My Life",
                     artist: "No Doubt",
                     genre: "Pop",
                     coverUrl: "https://s3.amazonaws.com/CoverProject/album/album_no_doubt_its_my_life_bathwater.png",
                     year: "2003")
    
  let album3 = Album(title: "Nothing Like The Sun",
                     artist: "Sting",
                     genre: "Pop",
                     coverUrl: "https://s3.amazonaws.com/CoverProject/album/album_sting_nothing_like_the_sun.png",
                     year: "1999")
    
  let album4 = Album(title: "Staring at the Sun",
                     artist: "U2",
                     genre: "Pop",
                     coverUrl: "https://s3.amazonaws.com/CoverProject/album/album_u2_staring_at_the_sun.png",
                     year: "2000")
    
  let album5 = Album(title: "American Pie",
                     artist: "Madonna",
                     genre: "Pop",
                     coverUrl: "https://s3.amazonaws.com/CoverProject/album/album_madonna_american_pie.png",
                     year: "2000")
    
  albums = [album1, album2, album3, album4, album5]
}
複製代碼

在初始化程序中,你將使用五個示例專輯填充數組。若是上述專輯不符合你的喜愛,能夠隨便使用你喜歡的音樂替換它們。

如今將如下函數添加到類中:

func getAlbums() -> [Album] {
  return albums
}
  
func addAlbum(_ album: Album, at index: Int) {
  if albums.count >= index {
    albums.insert(album, at: index)
  } else {
    albums.append(album)
  }
}
  
func deleteAlbum(at index: Int) {
  albums.remove(at: index)
}
複製代碼

這些方法容許你獲取,添加和刪除專輯。

編譯你的項目,確保全部內容能正確地經過編譯。

此時,你可能想知道 PersistencyManager 類的位置,由於它不是單例。你將在下一節中看到 LibraryAPIPersistencyManager 之間的關係,你將在其中查看 外觀(Facade) 設計模式。

外觀模式

外觀設計模式爲複雜子系統提供了單一界面。你只需公開一個簡單的統一 API,而不是將用戶暴露給一組類及其 API。

下圖說明了這個概念:

facade2

API 的用戶徹底不知道它其中的複雜性。這種模式在大量使用比較複雜或難理解的類時是比較理想的。

外觀模式將使用系統接口的代碼與你隱藏的類的實現進行解耦,它還減小了外部代碼對子系統內部工做的依賴性。 若是外觀下的類可能會更改,那這仍然頗有用,由於外觀類能夠在幕後發生更改時保留相同的 API。

舉個例子,若是你想要替換後端服務,那麼你沒必要更改使用 API 的代碼,只需更改外觀類中的代碼便可。

如何使用外觀模式

目前,你擁有 PersistencyManager 在本地保存專輯數據,並使用 HTTPClient 來處理遠程通訊。項目中的其餘類不該該涉及這個邏輯,由於它們將隱藏在 LibraryAPI 的外觀後面。

要實現此模式,只有 LibraryAPI 應該包含 PersistencyManagerHTTPClient 的實例。其次,LibraryAPI 將公開一個簡單的 API 來訪問這些服務。

設計以下所示:

facade3

LibraryAPI 將暴露給其餘代碼,但會隱藏應用程序其他部分的 HTTPClientPersistencyManager 複雜性。

打開 LibraryAPI.swift 並將如下常量屬性添加到類中:

private let persistencyManager = PersistencyManager()
private let httpClient = HTTPClient()
private let isOnline = false
複製代碼

isOnline 決定了是否應使用對專輯列表所作的任何更改來更新服務器,例如添加或刪除專輯。實際上 HTTP 客戶端並非與真實服務器工做,僅用於演示外觀模式的用法,所以 isOnline 將始終爲 false

接下來,將如下三個方法添加到 LibraryAPI.swift

func getAlbums() -> [Album] {
  return persistencyManager.getAlbums()    
}
  
func addAlbum(_ album: Album, at index: Int) {
  persistencyManager.addAlbum(album, at: index)
  if isOnline {
    httpClient.postRequest("/api/addAlbum", body: album.description)
  }  
}
  
func deleteAlbum(at index: Int) {
  persistencyManager.deleteAlbum(at: index)
  if isOnline {
    httpClient.postRequest("/api/deleteAlbum", body: "\(index)")
  }   
}
複製代碼

咱們來看看 addAlbum(_:at:)。該類首先在本地更新數據,而後若是網絡有鏈接,則更新遠程服務器。這是外觀模式的核心優點,當你要編寫 Album 以外的某個類添加一個新專輯時,它不知道,也不須要知道類背後的複雜性。

注意:在爲子系統中的類設計外觀時,請記住,除非你正在構建單獨的模塊並使用訪問控制,不然不會阻止客戶端直接訪問這些「隱藏」的類。不要吝嗇訪問控制的代碼,也不要假設全部客戶端都必須使用那些與外觀使用它們方法相同的類。

編譯並運行你的應用程序。你將看到兩個空視圖和一個工具欄。頂部的 View 將用於顯示你的專輯封面,底部 View 將用於顯示與該專輯相關的信息列表。

Album app in starting state with no data displayed

你須要一些東西能在屏幕上顯示專輯的數據,這是你下一個設計模式的完美實踐:裝飾(Decorator)

裝飾模式

裝飾模式動態地向對象添加行爲和職責而無需修改其中代碼。它是子類化的替代方法,經過用另外一個對象包裝它來修改類的行爲。

在 Swift 中,這種模式有兩種很是常見的實現:擴展代理

拓展

添加擴展是一種很是強大的機制,容許你向現有類,結構體或枚舉類型添加新功能,而無需子類化。你能夠擴展你沒法訪問的代碼並加強他們的功能也很是棒。這意味着你能夠將本身的方法添加到 Cocoa 類,如 UIViewUIImage

Swift 擴展與裝飾模式的經典定義略有不一樣,由於擴展不包含它擴展的類的實例。

如何使用拓展

想象一下,你但願在 TableView 中顯示 Album 實例的狀況:

swiftDesignPattern3

專輯的標題來自哪裏?Album 是一個模型,所以它不關心你將如何呈現數據。你須要一些外部代碼才能將此功能添加到 Album 結構體中。

你將建立 Album 結構體的擴展,它將定義一個返回能夠在 UITableView 中容易使用的數據結構的新方法。

打開 Album.swift 並在文件末尾添加如下代碼:

typealias AlbumData = (title: String, value: String)
複製代碼

此類型定義了一個元組,其中包含表視圖顯示一行數據所需的全部信息。如今添加如下擴展名以訪問此信息:

extension Album {
  var tableRepresentation: [AlbumData] {
    return [
      ("Artist", artist),
      ("Album", title),
      ("Genre", genre),
      ("Year", year)
    ]
  }
}
複製代碼

AlbumData 數組將更容易在 TableView 中顯示。

注意:類徹底能夠覆蓋父類的方法,可是對於擴展則不能。擴展中的方法或屬性不能與原始類中的方法或屬性同名。

考慮一下這個模式有多強大:

  • 你能夠直接在 Album 中使用屬性。
  • 你已添加到 Album 結構體而且不用修改它。
  • 這次簡單的操做將容許你返回一個相似 UITableViewAlbum

代理

外觀設計模式的另外一個實現是代理,它是一種讓一個對象表明或協同另一個對象工做的機制。UITableView 很貪婪,它有兩個代理類型屬性,一個叫作數據源,另外一個叫代理。它們作的事情略有不一樣,例如 TableView 將詢問其數據源在特定部分中應該有多少行,但它會詢問其代理在行被點擊時要執行的操做。

你不能期望 UITableView 知道你但願在每一個 section 中有多少行,由於這是特定於應用程序的。所以,計算每一個 section 中的行數的任務會被傳遞到數據源。這容許 UITableView 的類獨立於它顯示的數據。

如下是你建立新 UITableView 時所發生的事情的僞解釋:

Table:我在這兒!我想作的就是顯示 cell。嘿,我有幾個 section 呢? Data source:一個! Table:好的,好的,很簡單!第一個 section 中有多少個 cell 呢? Data source:四個! Table:謝謝!如今,請耐心點,這可能會有點重複。我能夠在第 0 個 section 第 0 行得到 cell 嗎? Data source:能夠,去吧! Table:如今第 0 個 section,第 1 行呢?

未完待續...

UITableView 對象完成顯示錶視圖的工做。可是最終它須要一些它沒有的信息。而後它轉向其代理和數據源,併發送一條消息,要求提供其餘信息。

將一個對象子類化並重寫必要的方法彷佛更容易,但考慮一下你只能基於單個類進行子類化。若是你但願一個對象成爲兩個或更多其餘對象的代理,你就沒法經過子類化實現此目的。

注意:這是一個重要的模式。Apple 在大多數 UIKit 類中使用這種方法: UITableViewUITextViewUITextFieldUIWebViewUICollectionViewUIPickerViewUIGestureRecognizerUIScrollView。 這個清單還將不斷更新。

如何使用代理模式

打開 ViewController.swift 並把這些私有的屬性添加到類:

private var currentAlbumIndex = 0
private var currentAlbumData: [AlbumData]?
private var allAlbums = [Album]()
複製代碼

從 Swift 4 開始,標記爲 private 的變量能夠在類型和所述類型的任何擴展之間共享相同的訪問控制範圍。若是你想瀏覽 Swift 4 引入的新功能,請查看 What’s New in Swift 4

你將使 ViewController 成爲 TableView 的數據源。在類定義的右大括號以後,將此擴展添加到 ViewController.swift 的末尾:

extension ViewController: UITableViewDataSource {

}
複製代碼

編譯器會發出警告,由於 UITableViewDataSource 有一些必需的函數。在擴展中添加如下代碼讓警告消失:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  guard let albumData = currentAlbumData else {
    return 0
  }
  return albumData.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
  if let albumData = currentAlbumData {
    let row = indexPath.row
    cell.textLabel?.text = albumData[row].title
    cell.detailTextLabel?.text = albumData[row].value
  }
  return cell
}
複製代碼

tableView(_:numberOfRowsInSection:) 返回要在 tableView 中顯示的行數,該行數與專輯「裝飾」表示中的項目數相匹配。

tableView(_:cellForRowAtIndexPath:) 建立並返回一個帶有 title 和 value 的 cell。

注意:你實際上能夠將方法添加到主類聲明或擴展中,編譯器並不關心數據源方法實際上存在於 UITableViewDataSource 擴展中。對於閱讀代碼的人來講,這種組織確實有助於提升可讀性。

接下來,使用如下代碼替換 viewDidLoad()

override func viewDidLoad() {
  super.viewDidLoad()
 
  //1
  allAlbums = LibraryAPI.shared.getAlbums()

  //2
  tableView.dataSource = self		
}
複製代碼

如下是上述代碼的解析:

  1. 經過 API 獲取全部專輯的列表。請記住,咱們的計劃是直接使用 LibraryAPI 的外觀而不是直接用 PersistencyManager
  2. 這是你設置 UITableView 的地方。你聲明 ViewController 是 UITableView 數據源,所以,UITableView 所需的全部信息都將由 ViewController 提供。請注意,若是在 storyboard 中建立了 TableView,你實際上能夠在那裏設置代理和數據源。

如今,將如下方法添加到 ViewController 裏:

private func showDataForAlbum(at index: Int) {
    
  // defensive code: make sure the requested index is lower than the amount of albums
  if index < allAlbums.count && index > -1 {
    // fetch the album
    let album = allAlbums[index]
    // save the albums data to present it later in the tableview
    currentAlbumData = album.tableRepresentation
  } else {
    currentAlbumData = nil
  }
  // we have the data we need, let's refresh our tableview
  tableView.reloadData()
}
複製代碼

showDataForAlbum(at:) 從專輯數組中獲取所需的專輯數據。當你想要刷新數據時,你只須要在 UITableView 裏調用 reloadData。這會致使 TableView 再次調用其數據源方法,例如從新加載 TableView 中應顯示的 section 個數,每一個 section 中的行數以及每一個 cell 的外觀等等。

將如下行添加到 viewDidLoad() 的末尾:

showDataForAlbum(at: currentAlbumIndex)
複製代碼

這會在應用啓動時加載當前專輯。因爲 currentAlbumIndex 設置爲 0,所以顯示該集合中的第一張專輯。

編譯並運行你的項目,你的應用啓動後屏幕上應該會顯示以下圖:

Album app showing populated table view

TableView 設置數據源完成!

寫在最後

爲了避免使用硬編碼值(例如字符串 Cell)污染代碼,請查看 ViewController,並在類定義的左大括號以後添加如下內容:

private enum Constants {
  static let CellIdentifier = "Cell"
}
複製代碼

在這裏,你將建立一個枚舉充當常量的容器。

注意:使用不帶 case 的枚舉的優勢是它不會被意外地實例化並只做爲一個純命名空間。

如今只需用 Constants.CellIdentifier 替換 "Cell"

接下來該幹嗎?

到目前爲止,事情看起來進展很順利!你知道了 MVC 模式,還有單例,外觀和裝飾模式。你能夠看到 Apple 在 Cocoa 中如何使用它們以及如何將模式應用於你本身的代碼。

若是你想要查看或比較,那請看 最終項目

庫存裏還有不少:本教程的第二部分還有適配器,觀察者和備忘錄模式。若是這還不夠,咱們會有一個後續教程,在你重構一個簡單的 iOS 遊戲時會涉及更多的設計模式。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索