模型-视图-控制器(MVC)设计模式

模型-视图-控制器(MVC)模式将对象分为三种不同的类型。是的,你猜对了:这三种类型是:模型、视图和控制器!

用下图来解释这些类型之间的关系相当简单。

模型-视图-控制器(MVC)设计模式_第1张图片

  • (Models)模型保存应用数据。它们通常是结构或简单的类。
  • (View)视图在屏幕上显示视觉元素和控件。它们通常是UIView的子类。
  • (Controllers)控制器在模型和视图之间进行协调。它们通常是UIViewController的子类。

MVC在iOS编程中非常常见,因为这是苹果在UIKit中选择采用的设计模式。

允许控制器为他们的模型和视图提供强大的属性,因此他们可以可直接访问。控制器可以有一个以上的模型和/或视图。

相反,模型和视图不应持有对其所属控制器的强引用。这将导致一个保留循环。

相反,模型通过属性观察(您将在后面的章节中深入了解)与控制器通信,而视图通过IBActions与控制器通信。

这让您可以在多个控制器之间重用模型和视图。赢了!

注意:视图可以通过委托对自己的控制器有一个弱引用(见第4章,"委托模式")。例如,一个UITableView可以为它的委托和/或dataSource引用持有对它自己的视图控制器的弱引用。然而,表视图并不知道这些都是设置给它自己的控制器的--它们只是碰巧是这样。

控制器更难重用,因为它们的逻辑通常对它们所做的任何任务都非常特殊。因此,MVC并没有尝试重用它们。

什么时候该用?

将此模式作为创建iOS应用的起点。

几乎在每个应用中,除了MVC之外,你可能还需要更多的模式,但根据你的应用需要引入更多的模式也是可以的。

操场实例

打开Starter目录下的FundamentalDesignPatterns.xcworkspace。这是一个游乐场页面的集合,每个基本设计模式都有一个页面。在本节结束时,你会有一个很好的设计模式参考!

从 "文件 "层次结构打开 "概览 "页面。

模型-视图-控制器(MVC)设计模式_第2张图片

本页列出了三种类型的设计模式。

  • 结构模式描述了对象如何组成更大的子系统。
  • 行为模式描述了对象之间如何通信。
  • 创建模式为您实例化或 "创建 "对象。

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 (http://bit.ly/ios-duolingo)、WaniKani等。(http://bit.ly/wanikani) 和 Anki (http://bit.ly/ios-anki)

你将从头开始创建项目,所以打开Xcode并选择File ▸ New ▸。项目。然后选择iOS ▸单一视图应用程序,并按下一步。

输入RabbleWabble作为产品名称;选择你的团队,如果你没有设置团队,则保留为 "无"(如果你只使用模拟器,则不需要);将你的组织名称和组织标识符设置为你喜欢的任何内容;确认语言设置为Swift;取消选中使用核心数据、包含单元测试和包含UI测试;然后点击 "下一步 "继续。

选择一个方便的位置来保存项目,然后按Create。你需要做一些组织工作来展示MVC模式。

从File hierarchy中打开ViewController.swift,删除大括号内的所有模板代码。然后右击ViewController,选择Refactor ▸ 重命名....。

模型-视图-控制器(MVC)设计模式_第3张图片

输入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组,选择按名称排序。

你的文件层次结构最终应该是这样的。

模型-视图-控制器(MVC)设计模式_第4张图片

由于您移动了Info.plist,您需要告诉Xcode它的新位置在哪里。要做到这一点,选择蓝色的RabbleWabble项目文件夹;选择RabbleWabble目标,选择General选项卡,然后点击Choose Info.plist File....。

模型-视图-控制器(MVC)设计模式_第5张图片

在出现的新窗口中,从文件列表中点击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组中。

模型-视图-控制器(MVC)设计模式_第6张图片

提示时,如果需要,请选中复制项目的选项,然后按完成添加文件。

既然你已经打开了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键防止窗口关闭,然后拖放三个标签到场景上,不要重叠。

模型-视图-控制器(MVC)设计模式_第7张图片

按对象库窗口上的红色X,之后关闭窗口。

双击最上面的标签,将其文字设置为 "提示"。将中间的标签设置为 "提示",将最下面的标签设置为 "答案"。

选择 "提示 "标签,打开 "实用工具 "窗格,选择 "属性 "检查器标签。将标签的字体设置为System 50.0,对齐方式设置为居中,行数设置为0。

模型-视图-控制器(MVC)设计模式_第8张图片

将 "提示 "标签的字体设置为系统24.0,对齐方式为居中,行数为0;将 "答案 "标签的字体设置为系统48.0,对齐方式为居中,行数为0。

如果需要,调整标签的大小以防止剪裁,并重新排列它们,使其保持相同的顺序而不重叠。

接下来,选择 "提示 "标签,选择 "添加新约束 "图标,然后进行以下操作。

  • 设置顶部约束为60
  • 将前导约束设置为0
  • 将尾部约束设置为0
  • 检查对边距的限制
  • 按 "添加3个约束 "键

模型-视图-控制器(MVC)设计模式_第9张图片

选择 "提示 "标签,选择 "添加新约束 "图标,然后进行以下操作。

  • 设置顶部约束为8
  • 将前导约束设置为0
  • 将尾部约束设置为0
  • 检查对边距的限制
  • 按 "添加3个约束 "键

模型-视图-控制器(MVC)设计模式_第10张图片

选择 "答案 "标签,选择 "添加新约束 "图标,然后进行以下操作。

  • 将顶部约束设置为50
  • 将前导约束条件设为0。
  • 将尾部约束设置为0。
  • 勾选约束为页边距。
  • 按 "添加3条约束"。

模型-视图-控制器(MVC)设计模式_第11张图片

现在的场景应该是这样的。

模型-视图-控制器(MVC)设计模式_第12张图片

接下来,按对象库按钮,在搜索栏中输入UIButton,然后拖动一个新按钮到视图的左下角。

打开 "属性检查器",将按钮的图片设置为ic_circle_x,并删除按钮的默认标题。

模型-视图-控制器(MVC)设计模式_第13张图片

将另一个按钮拖入视图的右下角。将其图像设置为ic_circle_check,并删除Button的默认标题。

拖动一个新的标签到场景中。打开 "属性检查器",将 "颜色 "设置为与红圈相匹配。将字体设置为System 32.0,并将Alignment设置为居中。根据需要调整此标签的大小,以防止剪切。

模型-视图-控制器(MVC)设计模式_第14张图片

拖动另一个标签到场景中,将其放置在绿色复选按钮的下方,并将其文字设置为0。 打开 "属性检查器",将颜色设置为与绿色圆圈相匹配。将字体设置为System 32.0,并将对齐方式设置为居中。根据需要调整这个标签的大小,以防止剪切。

接下来需要对按钮和标签进行约束设置。

选择红色圆圈按钮,选择 "添加新约束 "图标,然后进行以下操作。

  • 将前导约束设置为32
  • 将底部约束条件设置为8。
  • 勾选约束到页边距。
  • 按 "添加2个约束"。

模型-视图-控制器(MVC)设计模式_第15张图片

选择红色的标签,选择添加新约束的图标,然后进行以下操作。

  • 将底部约束设置为24
  • 勾选约束为页边距。
  • 按 "添加1条约束"。

模型-视图-控制器(MVC)设计模式_第16张图片

同时选择红圈图像视图和红颜色标签,选择对齐的图标,并进行以下操作。

  • 选中 "水平中心 "的方框。
  • 按添加1个约束。

模型-视图-控制器(MVC)设计模式_第17张图片

选择绿色圆圈图像视图,选择添加新约束的图标,然后进行以下操作。

  • 设置尾部约束为32。
  • 将底部约束设置为8。
  • 勾选约束为页边距。
  • 按 "添加2个约束 "键

模型-视图-控制器(MVC)设计模式_第18张图片

选择绿色的标签,选择添加新约束的图标,然后进行以下操作。

  • 将底部约束设置为24
  • 勾选约束为页边距。
  • 按添加1约束。

模型-视图-控制器(MVC)设计模式_第19张图片

同时选择绿色圆圈图像视图和绿色颜色标签,选择对齐的图标,并进行以下操作。

  • 选中 "水平中心 "的方框
  • 按 "添加1个约束"。

模型-视图-控制器(MVC)设计模式_第20张图片

现在的场景应该是这样的。

模型-视图-控制器(MVC)设计模式_第21张图片

要完成QuestionView的设置,需要在场景上设置视图的类,并连接属性。

点击场景上的视图,注意不要选择任何子视图代替,打开身份检查器。设置类为QuestionView。

模型-视图-控制器(MVC)设计模式_第22张图片

打开 "连接检查器",并从每个 "出入口 "拖动到适当的子视图,如图所示。

模型-视图-控制器(MVC)设计模式_第23张图片

建好后跑去看看风景。厉害!

创建控制器

你终于准备好创建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:。

模型-视图-控制器(MVC)设计模式_第24张图片

构建并运行,并尝试点击视图来显示/隐藏答案和提示标签。接下来,你需要处理每当按钮被按下时的情况。

打开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:。

模型-视图-控制器(MVC)设计模式_第25张图片

同样,选择绿色圆圈按钮,然后Control-drag到QuestionViewController对象上,选择handleCorrect:。

再一次,这些都是视图通知控制器需要处理的例子。构建并运行并尝试按下每个按钮。

关键点

在本章中,你学习了模型-视图-控制器(MVC)模式。下面是它的关键点。

  • MVC将对象分为三类:模型、视图和控制器。
  • MVC提倡控制器之间重用模型和视图。由于控制器的逻辑通常是非常具体的,所以MVC通常不会重用控制器。
  • 控制器负责协调模型和视图之间的关系:它将模型的值设置到视图上,并处理来自视图的IBAction调用。
  • MVC是一个很好的起点,但它也有局限性。并非每个对象都能整齐地归入模型、视图或控制器的范畴。你应该在使用MVC的同时,根据需要使用其他模式。

你已经让Rabble Wabble有了一个很好的开端!不过,你还需要添加很多功能:让用户选择问题组、处理剩余问题时的情况等等。然而,你还有很多功能需要添加:让用户选择问题组,处理没有剩余问题时的情况,以及更多的功能

继续进入下一章,了解授权设计模式,继续构建Rabble Wabble。

你可能感兴趣的