模型-视图-控制器(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。