几年前,当我还是移动咨询公司的一名员工时,我曾为一家大型投资银行开发应用程序。 大公司,特别是银行,通常都有适当的流程来确保其软件的安全性,健壮性和可维护性。
此过程的一部分涉及将我编写的应用程序的代码发送给第三方进行审查。 那并没有打扰我,因为我认为我的代码是无懈可击的,而评论公司也会这么说。
当他们的回应回来时,判决结果与我所想的不同。 尽管他们说代码的质量还不错,但他们指出了这样一个事实,即代码难以维护和测试(当时的单元测试在iOS开发中并不十分流行)。
我驳回了他们的判断,认为我的代码很棒,并且无法对其进行改进。 他们一定不明白!
我有一个典型的开发人员狂妄自大:我们经常认为我们所做的事情很棒,而其他人则没有。
事后看来,我错了。 不久之后,我开始阅读一些最佳实践。 从那时起,我的代码中的问题就开始像拇指酸痛一样伸出来。 我意识到,就像许多iOS开发人员一样,我已经屈服于一些不良编码实践的经典陷阱。
大多数iOS开发人员会出错的地方
在应用的视图控制器之间传递状态时,会出现最常见的iOS开发不良做法之一。 我本人过去曾陷入这个陷阱。
在任何iOS应用中,跨视图控制器进行状态传播都是至关重要的。 当用户浏览应用程序的屏幕并与之交互时,您需要保持全局状态,以跟踪用户对数据所做的所有更改。
这就是大多数iOS开发人员寻求明显但不正确的解决方案的地方:单例模式。
单例模式实现起来非常快,尤其是在Swift中,而且效果很好。 您只需向类添加静态变量以保留该类本身的共享实例,即可完成。
class Singleton {
static let shared = Singleton()
}
这样就可以轻松地从代码中的任何位置访问此共享实例:
let singleton = Singleton.shared
因此,许多开发人员认为他们找到了状态传播问题的最佳解决方案。 但是他们错了。
单例模式实际上被认为是反模式。 开发社区对此进行了许多讨论。 例如,请参阅此堆栈溢出问题 。
简而言之,单例会产生以下问题:
- 它们在您的类中引入了许多依赖关系,使将来很难更改它们。
- 它们使全局状态可访问代码的任何部分。 这会创建难以跟踪的复杂交互,并导致许多意外错误。
- 它们使您的类很难测试,因为您无法轻松地将它们与单例分离。
此时,一些开发人员认为:“啊,我有一个更好的解决方案。 我将改用AppDelegate
”。
问题是通过UIApplication
共享实例访问iOS应用中的AppDelegate
类:
let appDelegate = UIApplication.shared.delegate
但是, UIApplication
的共享实例本身就是一个单例。 所以你什么都还没解决!
该问题的解决方案是依赖注入。 依赖关系注入意味着类不会检索或创建自己的依赖关系,而是从外部接收它们。
要查看如何在iOS应用程序中使用依赖注入以及如何启用状态共享,我们首先需要重新审视iOS应用程序的一种基本架构模式:Model-View-Controller模式。
扩展MVC模式
简而言之,MVC模式指出iOS应用程序的体系结构分为三层:
- 模型层代表应用程序的数据。
- 视图层在屏幕上显示信息并允许交互。
- 控制器层充当其他两层之间的粘合剂,在它们之间移动数据。
MVC模式的通常表示是这样的:
问题在于该图是错误的。
“一个人可以合并一个对象扮演的MVC角色,例如,使一个对象同时担当控制器和视图的角色,在这种情况下,它将被称为视图控制器。 同样,您也可以拥有模型控制器对象。”
许多开发人员认为,视图控制器是iOS应用中存在的唯一控制器。 因此,由于缺少更好的位置,最终在其中编写了许多代码。 这就是使开发人员在需要传播状态时使用单例的原因:这似乎是唯一可能的解决方案。
从上面引用的几行中可以明显看出,我们可以为理解MVC模式添加一个新实体:模型控制器。 模型控制器处理应用程序的模型,履行模型本身不应该履行的角色。 这实际上是上述方案的外观:
模型控制器何时有用的一个完美示例是保持应用程序的状态。 该模型应仅代表您应用程序的数据。 应用程序的状态不应该是它的关注点。
这种状态保持通常在视图控制器内部结束,但是现在我们有了一个新的更好的放置位置:模型控制器。 然后,当依赖项注入到屏幕上时,可以将其传递给视图控制器。
我们已经解决了单例反模式。 让我们以示例的形式看一下我们的解决方案。
使用依赖注入在视图控制器之间传播状态
我们将编写一个简单的应用程序,以查看其工作方式的具体示例。 该应用程序将在一个屏幕上显示您最喜欢的报价,并允许您在第二个屏幕上编辑报价。
这意味着我们的应用程序将需要两个视图控制器,这将需要共享状态。 了解此解决方案的工作原理后,您可以将概念扩展到任何规模和复杂性的应用程序。
首先,我们需要一个模型类型来表示数据,在本例中为报价。 这可以用一个简单的结构来完成:
struct Quote {
let text: String
let author: String
}
模型控制器
然后,我们需要创建一个模型控制器,以保存应用程序的状态。 该模型控制器必须是一类。 这是因为我们将需要一个实例,该实例将传递给所有视图控制器。 像结构这样的值类型在传递时会被复制,因此它们显然不是正确的解决方案。
在我们的示例中,模型控制器所需的全部是可以保留当前报价的属性。 但是,当然,在更大的应用程序中,模型控制器可能比这更复杂:
class ModelController {
var quote = Quote(
text: "Two things are infinite: the universe and human stupidity; and I'm not sure about the universe.",
author: "Albert Einstein"
)
}
我为quote属性分配了默认值,因此在应用启动时,我们已经在屏幕上显示了一些内容。 这不是必需的,如果希望应用程序以空白状态启动,则可以将属性声明为可选的初始化为nil 。
创建用户界面
现在,我们有了模型控制器,它将包含应用程序的状态。 接下来,我们需要用于表示应用程序屏幕的视图控制器。
首先,我们创建他们的用户界面。 这就是两个视图控制器在应用程序的故事板内部的外观。
第二个视图控制器的界面是相同的,但是具有用于编辑引用文本的文本视图和用于编辑作者的文本字段。
这两个视图控制器通过一个模态表示形式连接,该模式源自“ 编辑报价”按钮。
您可以在GitHub存储库中探索界面和视图控制器的约束。
使用依赖注入对视图控制器进行编码
现在,我们需要对视图控制器进行编码。 我们在这里需要牢记的重要一点是,他们需要通过依赖注入从外部接收模型控制器实例。 因此,他们需要为此目的公开一个属性。
var modelController: ModelController!
我们可以调用第一个视图控制器QuoteViewController
。 该视图控制器需要在其界面中的引号和作者的标签上有几个出口。
class QuoteViewController: UIViewController {
@IBOutlet weak var quoteTextLabel: UILabel!
@IBOutlet weak var quoteAuthorLabel: UILabel!
var modelController: ModelController!
}
当该视图控制器出现在屏幕上时,我们将填充其界面以显示当前报价。 我们将执行此操作的代码放在控制器的viewWillAppear(_:)
方法中。
class QuoteViewController: UIViewController {
@IBOutlet weak var quoteTextLabel: UILabel!
@IBOutlet weak var quoteAuthorLabel: UILabel!
var modelController: ModelController!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let quote = modelController.quote
quoteTextLabel.text = quote.text
quoteAuthorLabel.text = quote.author
}
}
我们可以将这段代码放到viewDidLoad()
方法中,这很常见。 但是,问题在于,在创建视图控制器时, viewDidLoad()
仅被调用一次。 在我们的应用程序中,每次屏幕上出现QuoteViewController
时,我们都需要更新其用户界面。 这是因为用户可以在第二个屏幕上编辑报价。
这就是为什么我们使用viewWillAppear(_:)
方法而不是viewDidLoad()
。 这样,每次视图控制器的UI出现在屏幕上时,我们都可以对其进行更新。 如果您想进一步了解视图控制器的生命周期以及所有被调用的方法, 我写了一篇文章,详细介绍了所有这些方法 。
编辑视图控制器
现在,我们需要对第二个视图控制器进行编码。 我们将其称为EditViewController
。
class EditViewController: UIViewController {
@IBOutlet weak var textView: UITextView!
@IBOutlet weak var textField: UITextField!
var modelController: ModelController!
override func viewDidLoad() {
super.viewDidLoad()
let quote = modelController.quote
textView.text = quote.text
textField.text = quote.author
}
}
该视图控制器类似于上一个:
- 它具有用于文本视图和用户将用于编辑报价的文本字段的出口。
- 它具有用于模型控制器实例的依赖项注入的属性。
- 在出现屏幕之前,它会填充其用户界面。
在这种情况下,我使用了viewDidLoad()
方法,因为该视图控制器仅在屏幕上出现一次。
分享国家
现在,我们需要在两个视图控制器之间传递状态,并在用户编辑报价时对其进行更新。
我们在QuoteViewController
的prepare(for:sender:)
方法中传递应用状态。 当用户点击“ 编辑报价”按钮时,此方法由连接的序列触发。
class QuoteViewController: UIViewController {
@IBOutlet weak var quoteTextLabel: UILabel!
@IBOutlet weak var quoteAuthorLabel: UILabel!
var modelController: ModelController!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let quote = modelController.quote
quoteTextLabel.text = quote.text
quoteAuthorLabel.text = quote.author
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let editViewController = segue.destination as? EditViewController {
editViewController.modelController = modelController
}
}
}
在这里,我们转发了ModelController
的实例,该实例保留了应用程序的状态。 这是进行EditViewController
依赖注入的EditViewController
。
在EditViewController
,我们必须将状态更新为新输入的引用,然后再返回到先前的视图控制器。 我们可以通过连接到“ 保存”按钮的操作来做到这一点:
class EditViewController: UIViewController {
@IBOutlet weak var textView: UITextView!
@IBOutlet weak var textField: UITextField!
var modelController: ModelController!
override func viewDidLoad() {
super.viewDidLoad()
let quote = modelController.quote
textView.text = quote.text
textField.text = quote.author
}
@IBAction func save(_ sender: AnyObject) {
let newQuote = Quote(text: textView.text, author: textField.text!)
modelController.quote = newQuote
dismiss(animated: true, completion: nil)
}
}
初始化模型控制器
我们几乎完成,但你可能已经注意到,我们仍然缺少的东西:在QuoteViewController
传递ModelController
到EditViewController
通过依赖注入。 但是谁首先将此实例提供给QuoteViewController
? 请记住,使用依赖项注入时,视图控制器不应创建自己的依赖项。 这些需要来自外部。
但是QuoteViewController
之前没有视图控制器,因为这是我们应用程序的第一个视图控制器。 我们需要一些其他的对象创建ModelController
实例,并把它传递给QuoteViewController
。
该对象是AppDelegate
。 应用程序委托的作用是响应应用程序的生命周期方法并相应地配置应用程序。 这些方法之一是application(_:didFinishLaunchingWithOptions:)
,它在应用程序启动后立即被调用。 这就是我们创建的实例ModelController
并把它传递给QuoteViewController
:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if let quoteViewController = window?.rootViewController as? QuoteViewController {
quoteViewController.modelController = ModelController()
}
return true
}
}
我们的应用程序现已完成。 每个视图控制器都可以访问应用程序的全局状态,但是我们不在代码中的任何地方使用单例。
您可以在GitHub repo教程中下载此示例应用程序的Xcode项目 。
结论
在本文中,您已经了解了使用单例在iOS应用程序中传播状态是一种不好的做法。 尽管很容易创建和使用,但单例仍会产生很多问题。
我们通过更仔细地研究MVC模式并了解其中隐藏的可能性来解决该问题。 通过使用模型控制器和依赖项注入,我们能够在所有视图控制器之间传播应用程序的状态,而无需使用单例。
这是一个简单的示例应用程序,但是该概念可以推广到任何复杂性的应用程序。 这是在iOS应用中传播状态的标准最佳实践。 现在,我在为客户编写的每个应用程序中都使用它。
将概念扩展到更大的应用程序时,请记住以下几点: