单身模式限制了一个类只能有一个实例。对该类的每一个引用都是指同一个底层实例。这种模式在iOS应用开发中极为常见,因为苹果公司广泛使用了这种模式。
"singleton plus"模式也很常见,它提供了一个共享的单例实例,允许其他实例也被创建。
当一个类有一个以上的实例会导致问题时,或者当它不符合逻辑时,就使用单例模式。
如果一个共享的实例在大多数情况下是有用的,但你也想让自定义的实例被创建,那么就使用单例加模式。这方面的一个例子是
FileManager,它处理所有与文件系统访问有关的事情。有一个 "默认 "实例,它是一个单例,或者你可以创建你自己的。如果你在后台线程上使用它,你通常会创建自己的。
打开Starter目录下的FundamentalDesignPatterns.xcworkspace,然后打开Overview页面。
你会看到Singleton被列在Creational Patterns下面。这是因为Singleton是关于创建一个共享实例的。
点击Singleton链接来打开该页面。
Singleton和singleton plus在整个苹果框架中都很常见。例如,UIApplication是一个真正的单例。
在代码示例后面添加以下内容。
import UIKit
// MARK: - Singleton
let app = UIApplication.shared
// let app2 = UIApplication()
如果你试图取消对let app2这一行的注释,你会得到一个编译器错误! UIApplication不允许创建一个以上的实例。这就证明了它是一个单例! 你也可以创建你自己的单例类。在前面的代码后面添加以下内容。
public class MySingleton {
// 1
static let shared = MySingleton()
// 2
private init() { }
}
// 3
let mySingleton = MySingleton.shared
// 4
// let mySingleton2 = MySingleton()
以下是你的做法。
你首先声明一个名为shared的公共静态属性,它是单例的实例。
你把init标记为私有,以防止创建额外的实例。
3.你通过调用MySingleton.shared获得单例实例。
4.如果你试图创建额外的MySingleton实例,你会得到一个编译器错误。
接下来,在你的MySingleton例子下面添加下面的singleton plus例子。
// MARK: - Singleton Plus
let defaultFileManager = FileManager.default
let customFileManager = FileManager()
FileManager提供了一个默认的实例,这是它的单例属性。你也被允许创建FileManager的新实例。这证明了它使用了单例Plus模式!
创建你自己的单例加类也很容易。在FileManager的例子下面添加以下内容。
public class MySingletonPlus {
// 1
static let shared = MySingletonPlus()
// 2
public init() { }
}
// 3
let singletonPlus = MySingletonPlus.shared
// 4
let singletonPlus2 = MySingletonPlus()
这与真正的单例非常相似。
你声明一个共享的静态属性,就像单例一样。这有时被称为default,而不是,但这只是一个偏好,无论你喜欢哪个名字。
与真正的单例不同,你将init声明为public,以允许创建额外的实例。
你通过调用MySingletonPlus.shared来获得单体实例。
你也可以创建新的实例。
单例模式很容易被过度使用。
如果你遇到了想使用单例的情况,首先要考虑用其他方式来完成你的任务。
例如,如果你只是想把信息从一个视图控制器传递给另一个,那么单例就不合适。相反,可以考虑通过一个初始化器或属性来传递模型。
如果你确定你确实需要一个单例,考虑一个单例加是否更有意义。
拥有一个以上的实例会不会造成问题?拥有自定义实例是否会有用?你的答案将决定你是使用真正的单例还是单例加的更好。
单例有问题的一个最常见的原因是测试。如果你的状态被存储在一个像单例一样的全局对象中,那么测试的顺序就会很重要,而且模拟它们会很痛苦。这两个原因使测试变得很痛苦。
最后,要注意 "代码气味",它表明你的用例根本不适合作为单例。例如,如果你经常需要许多自定义实例,你的用例作为一个普通对象可能会更好。
你将继续构建上一章的Rabble Wabble。
如果你跳过了上一章,或者你想重新开始,打开Finder,导航到你下载本章资源的地方,在Xcode中打开starter/RabbleWabble/RabbleWabble.xcodeproj。
在上一章中,你硬编码了用于显示问题的策略:随机的或连续的。这意味着用户不可能改变这一点。你的任务是让用户选择他们想要的问题显示方式。
你需要做的第一件事就是要有地方来存储应用程序的设置。你将为此创建一个单例。
在文件层次结构中右键点击Models,选择New File....。在iOS标签下,选择Swift文件并按下一步。输入AppSettings.swift作为名字,然后点击创建。
将AppSettings.swift的内容替换为以下内容。
import Foundation
public class AppSettings {
// MARK: - Static Properties
public static let shared = AppSettings()
// MARK: - Object Lifecycle
private init() { }
}
在这里,你创建了一个名为AppSettings的新类,它是一个单例。
你最终会用它来管理整个应用程序的设置。对于Rabble Wabble的目的来说,有多个应用范围内的设置是没有意义的,所以你让它成为一个真正的单例,而不是一个单例plus。
接下来,在文件末尾添加以下代码,在AppSettings的最后结尾括号之后。
// MARK: - QuestionStrategyType
public enum QuestionStrategyType: Int, CaseIterable {
case random
case sequential
// MARK: - Instance Methods
public func title() -> String {
switch self {
case .random:
return "Random"
case .sequential:
return "Sequential"
}
}
public func questionStrategy(
for questionGroup: QuestionGroup) -> QuestionStrategy {
switch self {
case .random:
return RandomQuestionStrategy(questionGroup: questionGroup)
case .sequential:
return SequentialQuestionStrategy(questionGroup: questionGroup)
}
}
}
在这里,你声明了一个名为 QuestionStrategyType 的新枚举,它拥有应用程序中每种可能的 QuestionStrategy 类型的案例。
由于你使用了自Swift 4.2以来可用的CaseIterable协议,你还得到了一个由编译器自动生成的免费静态属性,名为allCases,以便以后用于显示所有可能的策略列表。这样做时,你会用title()来表示策略的标题文本。
你将使用questionStrategy(for:)从选定的QuestionStrategyType中创建一个QuestionStrategy。
然而,你实际上还没有解决当前的主要问题:让用户设置所需的策略类型。
在AppSettings中添加以下代码,就在开头的类大括号之后。
// MARK: - Keys
private struct Keys {
static let questionStrategy = "questionStrategy"
}
你将使用字符串作为键来存储UserDefaults中的设置。与其到处硬编码字符串 "questionStrategy",不如声明一个名为Keys的新结构,以提供一种命名和类型化的方式来引用此类字符串。
接下来,在共享属性后添加以下内容。
// MARK: - Instance Properties
public var questionStrategyType: QuestionStrategyType {
get {
let rawValue = userDefaults.integer(
forKey: Keys.questionStrategy)
return QuestionStrategyType(rawValue: rawValue)!
} set {
userDefaults.set(newValue.rawValue, forKey: Keys.questionStrategy)
}
}
private let userDefaults = UserDefaults.standard
你将使用questionStrategyType来保持用户所需的策略。与其说是一个简单的属性,不如说是使用userDefaults覆盖getter和setter来获取和设置整数值,因为只要用户终止了应用程序,这个属性就会丢失。
userDefaults被设置为UserDefaults.standard,这是另一个单例plus。
提供的。你用它来存储键值对,这些键值对可以在不同的应用程序启动时持续存在。
最后,在AppSettings中添加以下内容,在init之后。
// MARK: - Instance Methods
public func questionStrategy(
for questionGroup: QuestionGroup) -> QuestionStrategy {
return questionStrategyType.questionStrategy(
for: questionGroup)
}
这是一个方便的方法,从选定的问题策略类型中获取问题策略。
干得好! 这就完成了AppSettings。
你接下来需要创建一个新的视图控制器,以便用户可以选择他们想要的问题策略。
在文件层次结构中右击控制器,并选择新建file.... 在iOS标签下,选择Swift文件并按下一步。输入AppSettingsViewController.swift作为名称,然后按创建。
用以下内容替换AppSettingsViewController.swift的内容。
import UIKit
// 1
public class AppSettingsViewController: UITableViewController {
// 2
// MARK: - Properties
public let appSettings = AppSettings.shared
private let cellIdentifier = "basicCell"
// MARK: - View Life Cycle
public override func viewDidLoad() {
super.viewDidLoad()
// 3
tableView.tableFooterView = UIView()
// 4
tableView.register(UITableViewCell.self,
forCellReuseIdentifier: cellIdentifier)
}
}
下面是你在上面做的事情。
首先,你声明AppSettingsTableViewController是UITableViewController的一个子类。
你为appSettings创建一个属性,你将用它来获取和设置questionStrategyType。
你将tableFooterView设置为一个新的UIView。这样,你就不会在表格视图的底部出现额外的空白单元格。
你还为cellIdentifier的cellReuseIdentifier注册了UITableViewCell.self。这可以确保每当你调用tableView.dequeueReusableCell(withIdentifier:for:)时,你都能得到一个UITableViewCell实例。
接下来,在文件的结尾处,在类的结尾大括号后添加以下代码。
// MARK: - UITableViewDataSource
extension AppSettingsViewController {
public override func tableView(
_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
// 1
return QuestionStrategyType.allCases.count
}
public override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: cellIdentifier, for: indexPath)
// 2
let questionStrategyType =
QuestionStrategyType.allCases[indexPath.row]
// 3
cell.textLabel?.text = questionStrategyType.title()
// 4
if appSettings.questionStrategyType ==
questionStrategyType {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
return cell
}
}
以下是你要做的事情。
首先,你覆盖tableView(_:numberOfRowsInSection:)以返回QuestionStrategyType.allCases.count,这是你拥有的策略的数量。
接下来,你覆盖tableView(_:cellForRowAt:),再次使用QuestionStrategyType.allCases来获取给定indexPath.row的questionStrategyType。
将标签设置为该策略的名称。
最后,如果appSettings.questionStrategyType等于给定的questionStrategyType,它就是当前选择的策略,你用一个复选标记来表示。
接下来,在文件的结尾处,在最后一个大括号的后面添加最后一个扩展。
// MARK: - UITableViewDelegate
extension AppSettingsViewController {
public override func tableView(
_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let questionStrategyType =
QuestionStrategyType.allCases[indexPath.row]
appSettings.questionStrategyType = questionStrategyType
tableView.reloadData()
}
}
每当一个单元格被选中,你就会得到给定单元格的indexPath.row的问题策略类型,将其设置为appSettings.questionStrategyType并重新加载表视图。
干得好! 这就搞定了让用户选择问题策略的代码。
现在你需要一种方法让用户进入这个视图控制器。
在文件层次结构中,打开视图▸Main.storyboard。接下来,按下对象库按钮,然后选择显示图像库标签。
将ic_settings图像拖放到选择问题组场景的左侧导航栏项目上。
接下来,选择对象库按钮,选择显示对象库标签,在搜索栏中输入UITableViewController,然后在选择问题组场景下拖放一个新的表视图控制器。
选择新表视图场景的黄色类对象,打开身份检查器,将类设置为AppSettingsViewController。
接下来,打开属性检查器,将标题设置为 "应用程序设置"。
这不是严格的要求,但你不打算使用它,可以通过删除它来摆脱编译器的警告。
构建并运行应用程序,点击设置按钮,你会看到你全新的AppSettingsViewController!
试着选择一个选项并在这个屏幕上导航。你会看到你的选择持续存在!
然而,如果你从 "选择问题组 "列表中点击一个单元格,它可能不会真正反映出你的选择。这是怎么回事呢?
还记得你是如何硬编码上一章中使用的问题策略的吗?是的,你也需要更新这段代码,用你的新AppSettings来代替
打开SelectQuestionGroupViewController.swift,在//MARK后面添加以下属性: - 属性。
private let appSettings = AppSettings.shared
接下来,向下滚动到prepare(for:)并替换。
viewController.questionStrategy =
SequentialQuestionStrategy( questionGroup: selectedQuestionGroup)
......用以下内容。
viewController.questionStrategy =
appSettings.questionStrategy(for: selectedQuestionGroup)
构建并运行该应用程序,现在它将始终使用你选择的QuestionStrategy。
你在本章中了解了单例模式。下面是它的关键点。
单例模式将一个类限制为只有一个实例。
单例加模式提供了一个 "默认 "的共享实例,但也允许创建其他实例。
要小心过度使用这种模式 在你创建一个单例之前,请考虑用其他方法来解决这个问题。如果单例真的是最好的,宁愿使用单例加而不是单例。
RabbleWabble真的在进步!但是,它仍然缺少一个新的模式。然而,它仍然缺少一个关键功能:记住你的分数的能力。
继续看下一章,了解备忘录设计模式,并将这一功能添加到应用程序中。
上一章 | 目录 | 下一章 |
---|