第6章:单例模式

单身模式限制了一个类只能有一个实例。对该类的每一个引用都是指同一个底层实例。这种模式在iOS应用开发中极为常见,因为苹果公司广泛使用了这种模式。

"singleton plus"模式也很常见,它提供了一个共享的单例实例,允许其他实例也被创建。

你应该在什么时候使用它?

当一个类有一个以上的实例会导致问题时,或者当它不符合逻辑时,就使用单例模式。

如果一个共享的实例在大多数情况下是有用的,但你也想让自定义的实例被创建,那么就使用单例加模式。这方面的一个例子是

FileManager,它处理所有与文件系统访问有关的事情。有一个 "默认 "实例,它是一个单例,或者你可以创建你自己的。如果你在后台线程上使用它,你通常会创建自己的。

Playground实例

打开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()

以下是你的做法。

  1. 你首先声明一个名为shared的公共静态属性,它是单例的实例。

  2. 你把init标记为私有,以防止创建额外的实例。

  3. 3.你通过调用MySingleton.shared获得单例实例。

  4. 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()

这与真正的单例非常相似。

  1. 你声明一个共享的静态属性,就像单例一样。这有时被称为default,而不是,但这只是一个偏好,无论你喜欢哪个名字。

  2. 与真正的单例不同,你将init声明为public,以允许创建额外的实例。

  3. 你通过调用MySingletonPlus.shared来获得单体实例。

  4. 你也可以创建新的实例。

你应该注意什么?

单例模式很容易被过度使用。

如果你遇到了想使用单例的情况,首先要考虑用其他方式来完成你的任务。

例如,如果你只是想把信息从一个视图控制器传递给另一个,那么单例就不合适。相反,可以考虑通过一个初始化器或属性来传递模型。

如果你确定你确实需要一个单例,考虑一个单例加是否更有意义。

拥有一个以上的实例会不会造成问题?拥有自定义实例是否会有用?你的答案将决定你是使用真正的单例还是单例加的更好。

单例有问题的一个最常见的原因是测试。如果你的状态被存储在一个像单例一样的全局对象中,那么测试的顺序就会很重要,而且模拟它们会很痛苦。这两个原因使测试变得很痛苦。

最后,要注意 "代码气味",它表明你的用例根本不适合作为单例。例如,如果你经常需要许多自定义实例,你的用例作为一个普通对象可能会更好。

教程项目

你将继续构建上一章的Rabble Wabble。

如果你跳过了上一章,或者你想重新开始,打开Finder,导航到你下载本章资源的地方,在Xcode中打开starter/RabbleWabble/RabbleWabble.xcodeproj。

在上一章中,你硬编码了用于显示问题的策略:随机的或连续的。这意味着用户不可能改变这一点。你的任务是让用户选择他们想要的问题显示方式。

创建AppSettings的单例

你需要做的第一件事就是要有地方来存储应用程序的设置。你将为此创建一个单例。

在文件层次结构中右键点击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)
  }
}

下面是你在上面做的事情。

  1. 首先,你声明AppSettingsTableViewController是UITableViewController的一个子类。

  2. 你为appSettings创建一个属性,你将用它来获取和设置questionStrategyType。

  3. 你将tableFooterView设置为一个新的UIView。这样,你就不会在表格视图的底部出现额外的空白单元格。

  4. 你还为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
  }
}

以下是你要做的事情。

  1. 首先,你覆盖tableView(_:numberOfRowsInSection:)以返回QuestionStrategyType.allCases.count,这是你拥有的策略的数量。

  2. 接下来,你覆盖tableView(_:cellForRowAt:),再次使用QuestionStrategyType.allCases来获取给定indexPath.row的questionStrategyType。

  3. 将标签设置为该策略的名称。

  4. 最后,如果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真的在进步!但是,它仍然缺少一个新的模式。然而,它仍然缺少一个关键功能:记住你的分数的能力。

继续看下一章,了解备忘录设计模式,并将这一功能添加到应用程序中。


上一章 目录 下一章