大多数Swift开发人员不时面临的一大挑战是如何处理Massive View Controllers。无论我们是在谈论UIViewControlleriOS和tvOS的子类还是NSViewController在Mac上,这种类型的类都会变得非常大 - 无论是在范围还是代码行数方面。
许多视图控制器实现的问题在于它们只有太多的责任。他们管理视图,执行布局和处理事件 - 还管理网络,图像加载,缓存和许多其他事情。有些人可能认为这个问题是MVC设计模式结构的固有特性- 它鼓励大量的控制器类,因为它们是视图和模型之间的中心点。
虽然除了Apple默认的MVC之外的架构肯定有它们的位置,并且在许多情况下可以成为分解大视图控制器的一个很好的工具,但是还有很多方法可以在不完全切换架构的情况下解决这个问题。本周,我们来看一下这种方式 - 使用逻辑控制器。
本文中的示例将使用UIViewController基于iOS的代码,但也可以在处理NSViewControllermacOS时应用。
同时小编这里有些书籍和面试资料哦()
成分与提取
一般来说,当我们想要将大型分解为多个部分时,我们可以采用两种方法 - 组合和提取。
使用组合我们可以将多种类型组合在一起以形成新功能。我们不是创建具有多种职责的大型类型,而是创建更多模块化构建块,这些构建块可以组合在一起,为我们提供所需的功能。例如,在**我们使用组合来创建可以轻松插入其他视图控制器的小型,可重用的视图控制器。这是来自该帖子的代码示例,我们将其添加LoadingViewController
为子项以显示加载指示符:
class ListViewController: UITableViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) loadItems() } private func loadItems() { let loadingViewController = LoadingViewController() add(loadingViewController) dataLoader.loadItems { [weak self] result in loadingViewController.remove() self?.handle(result) } }}复制代码
虽然创建通用的,可组合的视图控制器非常适合加载视图和可重用列表之类的东西,但它并不是一个灵丹妙药。有时将视图控制器分解为小型的模块化子级并不是很实用 - 并且可以增加很多复杂性以获得极少的收益。在这种情况下,将功能提取到单独的专用类型可能是更好的选择。
使用**提取,**我们可以将大型的部分拉出成一个与原始部分紧密耦合的独立类型。这是几种架构模式所关注的东西 - 包括MVVM (Model-View-ViewModel)和MVP (Model-View-Presenter)之类的东西。对于MVVM,引入了View Model类型来处理大部分Model-> View转换逻辑 - 当使用MVP时,Presenter用于包含所有视图表示逻辑。
虽然我们将在未来的帖子中仔细研究MVVM和MVP(以及其他架构模式),但我们来看看如何在仍然坚持MVC的同时使用提取。
逻辑和观点
使ViewController
类型有点尴尬的一件事是它同时属于视图层和控制器层(我的意思是,它就在名称中; ViewController)。但就像子视图控制器组合如何告诉我们没有什么能阻止我们使用多个视图控制器来形成单个UI一样,实际上并没有什么能阻止我们拥有多个控制器类型。
一种方法是将视图控制器拆分为视图部分和控制器部分。一个控制器将保留其子类UIViewController
并包含所有与视图相关的功能,而另一个控制器可以与UI本身分离,而是专注于处理我们的逻辑。
例如,假设我们正在构建一个ProfileViewController
,我们将用它来在我们的应用程序中显示当前用户的个人资料。这是一个相对复杂的UI,因为它需要执行几个不同的任务:
- 加载用户的配置文件并显示它。
- 允许用户更改其个人资料照片和显示名称。
- 使用户能够退出应用程序。
如果我们将所有上述功能都放入ProfileViewController
类型本身,我们几乎知道它最终会变得非常庞大和复杂。相反,让我们为我们的个人资料屏幕创建两个控制器 - a ProfileViewController
和a ProfileLogicController
。
逻辑控制器
让我们从定义逻辑控制器开始。它的API将包含可以在我们的视图中执行的所有操作,并且对于每个操作,新状态作为完成处理程序的一部分返回。这意味着我们的逻辑控制器可以或多或少变为无状态,这意味着它将更容易测试。以下是我们的ProfileLogicController
最终结果:
class ProfileLogicController { typealias Handler = (ProfileState) -> Void func load(then handler: @escaping Handler) { // Load the state of the view and then run a completion handler } func changeDisplayName(to name: String, then handler: @escaping Handler) { // Change the user's display name and then run a completion handler } func changeProfilePhoto(to photo: UIImage, then handler: @escaping Handler) { // Change the user's profile photo and then run a completion handler } func logout() { // Log the user out, then re-direct to the login screen }}复制代码
如您所见,我们调用了配置文件屏幕的状态类型ProfileState
。这是我们用来告诉我们ProfileViewController
渲染什么的东西。我们将使用**的技术,并为每个状态创建一个具有不同情况的枚举,如下所示:
enum ProfileState { case loading case presenting(User) case failed(Error)}复制代码
每次在UI中发生事件时,我们ProfileViewController
都会调用ProfileLogicController
以处理该事件并返回一个新ProfileState
的视图控制器进行渲染。例如,当配置文件视图即将出现在屏幕上时,我们将调用load()
逻辑控制器以检索视图的状态 - 然后我们将呈现:
class ProfileViewController: UIViewController { private let logicController: ProfileLogicController init(logicController: ProfileLogicController) { self.logicController = logicController super.init(nibName: nil, bundle: nil) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) render(.loading) logicController.load { [weak self] state in self?.render(state) } }}复制代码
我们现在可以将所有与加载视图状态相关的逻辑放在我们的逻辑控制器中,而不是将它与我们的视图设置和布局代码混合在一起。例如,我们可能想要检查是否有一个User
可以简单地返回的缓存模型,或者通过网络加载一个缓存模型 - 如下所示:
class ProfileLogicController { func load(then handler: @escaping Handler) { let cacheKey = "user" if let existingUser: User = cache.object(forKey: cacheKey) { handler(.presenting(existingUser)) return } dataLoader.loadData(from: .currentUser) { [cache] result in switch result { case .success(let user): cache.insert(user, forKey: cacheKey) handler(.presenting(user)) case .failure(let error): handler(.failed(error)) } } }}复制代码
这种方法的优点在于我们的视图控制器不需要知道其状态是如何加载的,它只需要采用逻辑控制器提供的任何状态并进行渲染。通过将我们的逻辑与UI代码分离,测试也变得更加容易。要测试上面的load
方法,我们所要做的就是模拟数据加载器和缓存,并声明在缓存,成功和错误情况下返回正确的状态。
要了解有关编写包含异步代码的单元测试的更多信息,请查看。
只是一个渲染器
每次加载新状态时,我们都会调用视图控制器的render()
方法来渲染它。这使得我们可以将视图控制器或多或少地视为一个简单的渲染器,通过反应处理每个状态,如下所示:
private extension ProfileViewController { func render(_ state: ProfileState) { switch state { case .loading: // Show a loading spinner, for example using a child view controller case .presenting(let user): // Bind the user model to the view controller's views case .failed(let error): // Show an error view, for example using a child view controller } }}复制代码
就像我们load()
在视图控制器即将出现在屏幕上时调用逻辑控制器一样,我们可以在处理UI事件时使用相同的模式 - 例如当用户在文本字段中输入新的显示名称时。在这里,我们将通知逻辑控制器(它可以调用我们的服务器来更新用户的显示名称)并呈现新的更新状态:
extension ProfileViewController: UITextFieldDelegate { func textFieldDidEndEditing(_ textField: UITextField) { guard let newDisplayName = textField.text else { return } logicController.changeDisplayName(to: newDisplayName) { [weak self] state in self?.render(state) } }}复制代码
无论视图控制器处理什么类型的事件,它都执行相同的两个操作:通知逻辑控制器,并呈现结果状态。因此,当将网站分成前端(浏览器)和后端(服务器)组件时,视图控制器最终具有与其逻辑控制器非常相似的关系。每一方都可以专注于他们最擅长的事情。
结论
将视图控制器的核心逻辑提取到匹配的逻辑控制器中可以是避免Massive View Controller问题的好方法,同时仍然坚持MVC模式。当然,用于此技术的几个概念与使用MVVM时应用视图模型的情况类似 - 在以后的文章中,我们将看看这些方法之间的差异和相似之处。
无论我们如何切割视图控制器 - 无论是使用子视图控制器,专用UIView
子类,视图模型,演示者还是逻辑控制器 - 目标都是一样的,让我们UIViewControllers
专注于做他们最擅长的事情 - 控制视图。
哪种方法最适合您的应用程序取决于您的要求,并且总是我建议尝试使用多种技术来找出最适合您需求的方法。同样重要的是要注意并非所有视图控制器都需要分解 - 有些屏幕可能非常简单,因此使用单个视图控制器可以很好地完成工作。
你怎么看?你之前尝试过使用过逻辑控制器吗?或者你会尝试使用它?请通过加我们的交流群 ,来一起交流或者发布您的问题,意见或反馈。
谢谢阅读~点个赞再走呗!?
原文地址