模型-視圖-控制器(MVC)模式將對象分爲三種不一樣的類型。是的,你猜對了:這三種類型是:模型、視圖和控制器!ios
用下圖來解釋這些類型之間的關係至關簡單。編程
MVC在iOS編程中很是常見,由於這是蘋果在UIKit中選擇採用的設計模式。swift
容許控制器爲他們的模型和視圖提供強大的屬性,所以他們能夠可直接訪問。控制器能夠有一個以上的模型和/或視圖。設計模式
相反,模型和視圖不該持有對其所屬控制器的強引用。這將致使一個保留循環。markdown
相反,模型經過屬性觀察(您將在後面的章節中深刻了解)與控制器通訊,而視圖經過IBActions與控制器通訊。框架
這讓您能夠在多個控制器之間重用模型和視圖。贏了!ide
注意:視圖能夠經過委託對本身的控制器有一個弱引用(見第4章,"委託模式")。例如,一個UITableView能夠爲它的委託和/或dataSource引用持有對它本身的視圖控制器的弱引用。然而,表視圖並不知道這些都是設置給它本身的控制器的--它們只是碰巧是這樣。工具
控制器更難重用,由於它們的邏輯一般對它們所作的任何任務都很是特殊。所以,MVC並無嘗試重用它們。oop
將此模式做爲建立iOS應用的起點。佈局
幾乎在每一個應用中,除了MVC以外,你可能還須要更多的模式,但根據你的應用須要引入更多的模式也是能夠的。
打開Starter目錄下的FundamentalDesignPatterns.xcworkspace。這是一個遊樂場頁面的集合,每一個基本設計模式都有一個頁面。在本節結束時,你會有一個很好的設計模式參考!
從 "文件 "層次結構打開 "概覽 "頁面。
本頁列出了三種類型的設計模式。
MVC是一種結構模式,由於它就是把對象組成模型、視圖或控制器。
接下來,從 "文件 "層次結構中打開 "模型-視圖-控制器 "頁面。在代碼示例中,你將使用MVC建立一個 "地址屏幕"。
你能猜到地址屏的三個部分會是什麼嗎?固然是模型、視圖和控制器! 在Code Example以後添加這段代碼來建立模型。
import UIKit
// MARK: - Address
public struct Address {
public var street: String
public var city: String
public var state: String
public var zipCode: String
}
複製代碼
這樣就建立了一個簡單的結構,表示一個地址。
接下來須要導入UIKit來建立AddressView做爲UIView的子類。
加入這段代碼便可。
// MARK: - AddressView
public final class AddressView: UIView {
@IBOutlet public var streetTextField: UITextField!
@IBOutlet public var cityTextField: UITextField!
@IBOutlet public var stateTextField: UITextField!
@IBOutlet public var zipCodeTextField: UITextField!
}
複製代碼
在實際的iOS應用中,而不是遊樂場,你還會爲這個視圖建立一個xib或故事板,並將IBOUTLET屬性鏈接到它的子視圖。在本章的教程項目中,你會在後面練習這樣作。
最後,你須要建立AddressViewController。接下來添加這段代碼。
// MARK: - AddressViewController
public final class AddressViewController: UIViewController {
// MARK: - Properties
public var address: Address?
public var addressView: AddressView! {
guard isViewLoaded else { return nil }
return (view as! AddressView)
}
}
複製代碼
在這裏,你讓控制器持有對它所擁有的視圖和模型的強引用。
addressView是一個計算屬性,由於它只有一個getter。它首先檢查isViewLoaded,以防止在視圖控制器呈如今屏幕上以前建立視圖。若是isViewLoaded爲真,它就會將視圖投射到一個AddressView上。爲了使警告保持沉默,你用括號包圍這個投射。
在實際的iOS應用中,你還須要在storyboard或xib上指定視圖的類,以確保應用正確地建立一個AddressView而不是默認的UIView。
回顧一下,控制器的責任是協調模型和視圖之間的關係。在這種狀況下,控制器應該使用來自地址的值更新其地址View。
一個很好的地方是每當調用viewDidLoad的時候。在AddressViewController類的末尾添加如下內容。
// MARK: - View Lifecycle
public override func viewDidLoad() {
super.viewDidLoad()
updateViewFromAddress()
}
private func updateViewFromAddress() {
guard let addressView = addressView,
let address = address else { return }
addressView.streetTextField.text = address.street
addressView.cityTextField.text = address.city
addressView.stateTextField.text = address.state
addressView.zipCodeTextField.text = address.zipCode
}
複製代碼
若是在調用viewDidLoad後設置了地址,控制器也應該更新地址View。
將地址屬性替換爲如下內容。
public var address: Address? {
didSet {
updateViewFromAddress()
}
}
複製代碼
這是個例子,說明模型如何告訴控制器有什麼變化,視圖須要更新。
若是你也想讓用戶從視圖中更新地址呢?沒錯--你須要在控制器上建立一個IBAction。
在 updateViewFromAddress()以後添加這個。
// MARK: - Actions
@IBAction public func updateAddressFromView( _ sender: AnyObject) {
guard let street = addressView.streetTextField.text, street.count > 0,
let city = addressView.cityTextField.text, city.count > 0,
let state = addressView.stateTextField.text, state.count > 0,
let zipCode = addressView.zipCodeTextField.text, zipCode.count > 0 else {
// TO-DO: show an error message, handle the error, etc
return
}
address = Address(street: street, city: city,
}
複製代碼
最後,這是一個例子,說明視圖如何告訴控制器有什麼變化,模型須要更新。在實際的iOS應用中,你還須要從AddressView的子視圖中鏈接這個IBAction,好比UITextField上的valueChanged事件或者UIButton上的touchUpInside事件。
總而言之,這給了你一個簡單的例子,讓你瞭解MVC模式是如何工做的。你已經看到了控制器如何擁有模型和視圖,以及每一個模型和視圖如何相互交互,但老是經過控制器。
MVC是一個很好的起點,但它有侷限性。並不是每一個對象都能整齊地納入模型、視圖或控制器的範疇。所以,只使用MVC的應用程序每每會在控制器中加入大量的邏輯。這可能致使視圖控制器變得很是大! 當這種狀況發生時,有一個至關古怪的術語,叫作 "大規模視圖控制器"。(汶:MVC使用會出現的問題)
爲了解決這個問題,你應該根據你的應用須要引入其餘設計模式。
在本節中,你將建立一個名爲Rabble Wabble的教程應用。
這是一款語言學習應用,相似於Duolingo (bit.ly/ios-duoling… 和 Anki (bit.ly/ios-anki)。
你將從頭開始建立項目,因此打開Xcode並選擇File ▸ New ▸。項目。而後選擇iOS ▸單一視圖應用程序,並按下一步。
輸入RabbleWabble做爲產品名稱;選擇你的團隊,若是你沒有設置團隊,則保留爲 "無"(若是你只使用模擬器,則不須要);將你的組織名稱和組織標識符設置爲你喜歡的任何內容;確認語言設置爲Swift;取消選中使用核心數據、包含單元測試和包含UI測試;而後點擊 "下一步 "繼續。
選擇一個方便的位置來保存項目,而後按Create。你須要作一些組織工做來展現MVC模式。
從File hierarchy中打開ViewController.swift,刪除大括號內的全部模板代碼。而後右擊ViewController,選擇Refactor ▸ 重命名....。
輸入QuestionViewController做爲新的名稱,而後按Enter鍵進行修改。而後,在類QuestionViewController前添加關鍵字public,像這樣。
public class QuestionViewController: UIViewController
複製代碼
在本書中,對於那些應該被其餘類公開訪問的類型、屬性和方法,你會使用public;若是某些東西只應該被類型自己訪問,你會使用private;若是它應該被子類或相關類訪問,但不打算供通常使用,你會使用internal。這就是所謂的訪問控制。
這是iOS開發中的 "最佳實踐"。若是你曾經將這些文件移動到一個單獨的模塊中,例如建立一個共享的庫或框架,你會發現若是你遵循這個最佳實踐,你會發現它更容易作到。
接下來,在 "文件 "層次結構中選擇黃色的RabbleWabble組,而後一塊兒按Command + Option + N鍵建立一個新組。
選擇新組,按Enter鍵編輯其名稱。輸入AppDelegate,再按Enter鍵確認。
重複此過程,爲控制器、模型、資源和視圖建立新組。
將 AppDelegate.swift 移入 AppDelegate 組,將 QuestionViewController.swift 移入控制器,將 Assets.xcassets 和 Info.plist 移入資源,將 LaunchScreen.storyboard 和 Main.storyboard 移入視圖。
最後,右鍵單擊黃色的RabbleWabble組,選擇按名稱排序。
你的文件層次結構最終應該是這樣的。
因爲您移動了Info.plist,您須要告訴Xcode它的新位置在哪裏。要作到這一點,選擇藍色的RabbleWabble項目文件夾;選擇RabbleWabble目標,選擇General選項卡,而後點擊Choose Info.plist File....。
在出現的新窗口中,從文件列表中點擊Info.plist,而後按Choose設置。構建並運行以驗證你在Xcode中沒有看到任何錯誤。
這是使用MVC模式的一個好的開始!經過簡單地將你的文件分組,你能夠在Xcode中看到任何錯誤。經過簡單地以這種方式對文件進行分組,您就能夠告訴其餘開發人員您的項目使用了MVC。清晰是好事
接下來你將建立Rabble Wabble的模型。
首先,你須要建立一個問題模型。在 "文件 "層次結構中選擇 "模型 "組,而後按Command + N建立一個新文件。從列表中選擇Swift文件,而後點擊 "下一步"。將文件命名爲Question.swift,而後單擊 "建立"。
用如下內容替換Question.swift的所有內容。
public struct Question {
public let answer: String
public let hint: String?
public let prompt: String
}
複製代碼
您還須要另外一個模型來充當問題組的容器。
在模型組中建立另外一個名爲QuestionGroup.swift的文件,並將其所有內容替換爲如下內容。
public struct QuestionGroup {
public let questions: [Question]
public let title: String
}
複製代碼
接下來,您須要添加問題羣的數據。這可能須要從新打字,因此我提供了一個文件,您能夠簡單地拖放到項目中。
打開Finder並導航到你下載本章項目的地方。在Starter和Final目錄旁邊,你會看到一個Resources目錄,其中包含QuestionGroupData.swift、Assets.xcassets和LaunchScreen.storyboard。
將Finder窗口定位在Xcode上方,而後像這樣將QuestionGroupData.swift拖放到Models組中。
提示時,若是須要,請選中複製項目的選項,而後按完成添加文件。
既然你已經打開了Resources目錄,那麼你應該把其餘文件也複製過來。首先,在應用程序中選擇資源下現有的Assets.xcassets,按Delete鍵刪除。在提示時選擇 "移動到回收站"。而後,將新的Assets.xcassets從Finder拖放到應用程序的資源組中,若是須要,在提示時勾選複製項目。
接下來,選擇應用中現有的LaunchScreen.storyboard在Views下,按Delete鍵將其刪除。一樣,確保在提示時選擇 "移動到垃圾桶"。而後,將新的LaunchScreen.storyboard從Finder拖放到應用程序的資源組中,若是須要,在提示時勾選複製項目。
打開QuestionGroupData.swift,你會發現裏面定義了幾個基本短語、數字等靜態方法。這個數據集是日文的,但若是你喜歡的話,你能夠把它調整爲其餘語言。你很快就會使用這些方法了
打開LaunchScreen.storyboard,你會看到一個漂亮的佈局,每當應用程序啓動時就會顯示出來。
構建並運行,查看可愛的應用程序圖標和啓動屏幕!
如今你須要設置MVC的 "視圖 "部分。選擇 "視圖 "組,並建立一個名爲 "QuestionView.swift "的新文件。
將其內容替換爲如下內容。
import UIKit
public class QuestionView: UIView {
@IBOutlet public var answerLabel: UILabel!
@IBOutlet public var correctCountLabel: UILabel!
@IBOutlet public var incorrectCountLabel: UILabel!
@IBOutlet public var promptLabel: UILabel!
@IBOutlet public var hintLabel: UILabel!
}
複製代碼
接下來,打開Main.storyboard,滾動到現有場景。按對象庫按鈕,在搜索欄中輸入標籤。按住option鍵防止窗口關閉,而後拖放三個標籤到場景上,不要重疊。
按對象庫窗口上的紅色X,以後關閉窗口。
雙擊最上面的標籤,將其文字設置爲 "提示"。將中間的標籤設置爲 "提示",將最下面的標籤設置爲 "答案"。
選擇 "提示 "標籤,打開 "實用工具 "窗格,選擇 "屬性 "檢查器標籤。將標籤的字體設置爲System 50.0,對齊方式設置爲居中,行數設置爲0。
將 "提示 "標籤的字體設置爲系統24.0,對齊方式爲居中,行數爲0;將 "答案 "標籤的字體設置爲系統48.0,對齊方式爲居中,行數爲0。
若是須要,調整標籤的大小以防止剪裁,並從新排列它們,使其保持相同的順序而不重疊。
接下來,選擇 "提示 "標籤,選擇 "添加新約束 "圖標,而後進行如下操做。
選擇 "提示 "標籤,選擇 "添加新約束 "圖標,而後進行如下操做。
選擇 "答案 "標籤,選擇 "添加新約束 "圖標,而後進行如下操做。
如今的場景應該是這樣的。
接下來,按對象庫按鈕,在搜索欄中輸入UIButton,而後拖動一個新按鈕到視圖的左下角。
打開 "屬性檢查器",將按鈕的圖片設置爲ic_circle_x,並刪除按鈕的默認標題。
將另外一個按鈕拖入視圖的右下角。將其圖像設置爲ic_circle_check,並刪除Button的默認標題。
拖動一個新的標籤到場景中。打開 "屬性檢查器",將 "顏色 "設置爲與紅圈相匹配。將字體設置爲System 32.0,並將Alignment設置爲居中。根據須要調整此標籤的大小,以防止剪切。
拖動另外一個標籤到場景中,將其放置在綠色複選按鈕的下方,並將其文字設置爲0。 打開 "屬性檢查器",將顏色設置爲與綠色圓圈相匹配。將字體設置爲System 32.0,並將對齊方式設置爲居中。根據須要調整這個標籤的大小,以防止剪切。
接下來須要對按鈕和標籤進行約束設置。
選擇紅色圓圈按鈕,選擇 "添加新約束 "圖標,而後進行如下操做。
選擇紅色的標籤,選擇添加新約束的圖標,而後進行如下操做。
同時選擇紅圈圖像視圖和紅顏色標籤,選擇對齊的圖標,並進行如下操做。
選擇綠色圓圈圖像視圖,選擇添加新約束的圖標,而後進行如下操做。
選擇綠色的標籤,選擇添加新約束的圖標,而後進行如下操做。
同時選擇綠色圓圈圖像視圖和綠色顏色標籤,選擇對齊的圖標,並進行如下操做。
如今的場景應該是這樣的。
要完成QuestionView的設置,須要在場景上設置視圖的類,並鏈接屬性。
點擊場景上的視圖,注意不要選擇任何子視圖代替,打開身份檢查器。設置類爲QuestionView。
打開 "鏈接檢查器",並從每一個 "出入口 "拖動到適當的子視圖,如圖所示。
建好後跑去看看風景。厲害!
你終於準備好建立MVC的 "控制器 "部分了。
打開QuestionViewController.swift並添加如下屬性。
// MARK: - Instance Properties
public var questionGroup = QuestionGroup.basicPhrases() public var questionIndex = 0
public var correctCount = 0
public var incorrectCount = 0
public var questionView: QuestionView! {
guard isViewLoaded else { return nil }
return (view as! QuestionView)
}
複製代碼
你暫時把questionGroup硬編碼爲基本短語。在將來的一章中,您將擴展應用程序,使用戶可以從列表中選擇問題組。
questionIndex是當前顯示的問題的索引。當用戶瀏覽問題時,你會遞增這個指數。
correctCount是正確回答的次數。用戶經過按下綠色的複選按鈕來表示一個正確的回答。
一樣地,incorrectCount是不正確回答的計數,用戶會經過按下紅色的X按鈕來表示。
問題View是一個計算屬性。這裏你檢查isViewLoaded,這樣你就不會由於訪問這個屬性而致使視圖無心中被加載。若是視圖已經加載了,你就強制將其轉爲QuestionView。
接下來你須要添加代碼來實際顯示一個問題。在剛纔添加的屬性後添加如下內容。
// MARK: - View Lifecycle
public override func viewDidLoad() {
super.viewDidLoad()
showQuestion()
}
private func showQuestion() {
let question = questionGroup.questions[questionIndex]
questionView.answerLabel.text = question.answer questionView.promptLabel.text = question.prompt questionView.hintLabel.text = question.hint
questionView.answerLabel.isHidden = true
questionView.hintLabel.isHidden = true
}
複製代碼
請注意這裏你是如何在控制器中編寫代碼來基於模型中的數據來操做視圖的。MVC FTW!
構建並運行,看看問題在屏幕上看起來如何!
如今,沒有任何方法能夠看到答案。你可能應該修復這個問題。在視圖控制器的末尾添加如下代碼。
// MARK: - Actions
@IBAction func toggleAnswerLabels(_ sender: Any) {
questionView.answerLabel.isHidden =
!questionView.answerLabel.isHidden questionView.hintLabel.isHidden =
!questionView.hintLabel.isHidden
}
複製代碼
這將切換提示和答案標籤是否被隱藏。在showQuestion()中把答案和提示標籤設置爲隱藏,以在每次顯示新問題時重置狀態。
這是一個視圖通知其控制器有關已發生的操做的例子。做爲迴應,控制器會執行代碼來處理這個動做。
你還須要在視圖上掛上這個動做。打開Main.storyboard,按對象庫按鈕。
在搜索字段中輸入tap,而後拖放一個Tap手勢識別器到視圖上。
確保你把它拖到基本視圖上,而不是拖到標籤或按鈕上! 控制-拖拽從 "輕敲手勢識別器 "對象到 "問題視圖"。
場景中的Controller對象,而後選擇toggleAnswerLabels:。
構建並運行,並嘗試點擊視圖來顯示/隱藏答案和提示標籤。接下來,你須要處理每當按鈕被按下時的狀況。
打開QuestionViewController.swift,並在類的末尾添加如下內容。
// 1
@IBAction func handleCorrect(_ sender: Any) {
correctCount += 1
questionView.correctCountLabel.text = "\(correctCount)"
showNextQuestion()
}
// 2
@IBAction func handleIncorrect(_ sender: Any) {
incorrectCount += 1
questionView.incorrectCountLabel.text = "\(incorrectCount)"
showNextQuestion()
}
// 3
private func showNextQuestion() {
questionIndex += 1
guard questionIndex < questionGroup.questions.count else {
// TODO: - Handle this...!
return
}
showQuestion()
}
複製代碼
你剛剛又定義了三個動做。下面是每一個動做的做用。
1.handleCorrect(_:)將在用戶按下綠色圓圈按鈕時被調用,以表示他們獲得了正確的答案。在這裏,你增長correctCount並設置correctCountLabel文本。
2.每當用戶按下紅圈按鈕,表示他們獲得的答案不正確時,就會調用handleIncorrect(_:)。這裏增長incorrectCount並設置incorrectCountLabel文本。
3.調用showNextQuestion()來前進到下一個問題。你根據questionIndex是否小於questionGroup.question.count,來防範是否還有其餘問題,若是有,則顯示下一個問題。
你將在下一章處理沒有問題的狀況。最後,你須要將視圖上的按鈕與這些操做鏈接起來。
打開Main.storyboard,選擇紅色圓圈按鈕,而後Control-drag到QuestionViewController對象上,選擇handleIncorrect:。
一樣,選擇綠色圓圈按鈕,而後Control-drag到QuestionViewController對象上,選擇handleCorrect:。
再一次,這些都是視圖通知控制器須要處理的例子。構建並運行並嘗試按下每一個按鈕。
在本章中,你學習了模型-視圖-控制器(MVC)模式。下面是它的關鍵點。
你已經讓Rabble Wabble有了一個很好的開端!不過,你還須要添加不少功能:讓用戶選擇問題組、處理剩餘問題時的狀況等等。然而,你還有不少功能須要添加:讓用戶選擇問題組,處理沒有剩餘問題時的狀況,以及更多的功能
繼續進入下一章,瞭解受權設計模式,繼續構建Rabble Wabble。