原文地址:Avoiding Massive View Controller using Containment & Child View Controller

首发于:【译】通过视图控制器容器和子视图控制器避免庞大的视图控制器

通过视图控制器容器和子视图控制器避免庞大的视图控制器

视图控制器容器和子视图控制器图解

View Controller 是一个提供基本构建块的组件,在 iOS 开发中我们以它为基础构建应用。在 Apple MVC 世界中,它作为 View 和 Model 的中间人,在两者之间充当协调者的角色。它以观察者控制器开始,响应模型更改、更新视图、使用目标操作从视图中接受用户交互、然后更新模型。

Apple MVC 图解(Apple 公司提供)

作为一名 iOS 开发者,很多次我们将面临处理庞大的 View Controller 问题,即便我们使用了像 MVVM、MVP 或 VIPER 这样的架构。某些时刻,View Controller 在一个屏幕上承担了太多职责。这违反了 SRP(单一职责原则),在模块之间形成了强度耦合,并使得重用和测试每个组件变得异常困难。

我们可以将下面的应用截图作为示例。你可以看到在一个屏幕上至少存在 3 种职责:

  1. 显示电影列表;
  2. 显示可以选择应用于电影列表的过滤列表;
  3. 清除所选过滤器的选项。

如果我们准备使用单一的 View Controller 来构建此屏幕,由于它在一个 view controller 中承担了过多职责,因此可以保证这个 view controller 将变得非常庞大和臃肿。

我们如何解决这个问题呢?其中一个解决方案是使用 View Controller 容器和子 View Controller。以下是使用该方案的好处:

  1. 将电影列表封装到 MovieListViewController 中,它只负责显示电影列表并对 Movie 模型中的更改做出响应。如果我们只想显示没有过滤器的电影列表,我们也可以在另一个屏幕中重用它。
  2. 将过滤器中的列表和选择逻辑封装到 FilterListViewController 中,它单独负责显示和过滤器的选择。当用户选择和取消选择时,我们可以使用委托与父 View Controller 进行通信。
  3. 将主 View Controller 缩减为一个 ContainerViewController,它只负责将选中的过滤器从过滤列表应用到 MovieListViewController 中的 Movie 模型。它还设置布局并将子 view controller 添加到容器视图中。

你可以在下面的 GitHub 代码仓库中查看完整的项目源代码。

  • alfianlosari/Filter-MVC-iOS

使用 Storyboard 来布置 View Controller

使用 Storyboard 来布置 View Controller

  1. ContainerViewController:View Controller 容器提供了 2 个容器视图,用于将子 View Controller 嵌入到水平 UIStackView 中。它还提供了单个 UIButton 来清空所选的过滤器。它还嵌入在充当初始 View Controller 的 UINavigationController 中。
  2. FilterListMovieController:它是 UITableViewController 的子类,具有分类样式和一个用来显示过滤器名称的标准单元格。它还分配了 Storyboard ID,因此可以通过编程的方式在 ContainerViewController 中对它进行实例化。
  3. MovieListViewController:它是 UITableViewController 的子类,具有 Plain 样式和一个用来显示 Movie 属性的小标题单元格。它还跟 FilterListViewController 一样分配了 Storyboard ID。

电影列表 View Controller

此 view controller 负责显示作为实例公开属性的 Movie 模型列表。我们使用 Swift 的 didSet 属性观察器来响应模型的更改,然后重新载入 UITableView。单元格使用默认小标题样式 UITableViewCellStyle 来显示电影的标题、持续时间、评级和流派。

import UIKit

struct Movie {

let title: String
let genre: String
let duration: TimeInterval
let rating: Float

}

class MovieListViewController: UITableViewController {

var movies = [Movie]() {
didSet {
tableView.reloadData()
}
}

let formatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute]
formatter.unitsStyle = .abbreviated
formatter.maximumUnitCount = 1
return formatter
}()

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return movies.count
}

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)

let movie = movies[indexPath.row]
cell.textLabel?.text = movie.title
cell.detailTextLabel?.text = "(formatter.string(from: movie.duration) ?? ""), (movie.genre.capitalized), rating: (movie.rating)"
return cell
}

}

过滤器列表 View Controller

过滤器列表在 3 个单独的部分中显示 MovieFilter 枚举:流派、评级和持续时间。MovieFilter 枚举本身符合 Hashable 协议,因此可以使用每个枚举及其属性的哈希值存储在唯一集合中。过滤器的选择存储在包含 MovieFilterSet 的实例属性下。

要与其他对象通信,通过 FilterListControllerDelegate 使用委托模式,委托有三个方法需要实现:

  1. 选择一个过滤器。
  2. 取消选择一个过滤器。
  3. 清空所有已选择过滤器。

import UIKit

enum MovieFilter: Hashable {

case genre(code: String, name: String)
case duration(duration: TimeInterval, name: String)
case rating(value: Float, name: String)

var hashValue: Int {

switch self {
case .genre(let code, let name):
return "(code)-(name)".hashValue

case .rating(let value, let name):
return "(value)-(name)".hashValue

case .duration(let duration, let name):
return "(duration)-(name)".hashValue

}
}

}

protocol FilterListViewControllerDelegate: class {

func filterListViewController(_ controller: FilterListViewController, didSelect filter: MovieFilter)
func filterListViewController(_ controller: FilterListViewController, didDeselect filter: MovieFilter)
func filterListViewControllerDidClearFilters(controller: FilterListViewController)

}

class FilterListViewController: UITableViewController {

let filters = MovieFilter.defaultFilters
weak var delegate: FilterListViewControllerDelegate?
var selectedFilters: Set<MovieFilter> = []

override func viewDidLoad() {
super.viewDidLoad()
}

func clearFilter() {
selectedFilters.removeAll()
delegate?.filterListViewControllerDidClearFilters(controller: self)

tableView.reloadData()
}

override func numberOfSections(in tableView: UITableView) -> Int {
return filters.count
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filters[section].filters.count
}

override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return filters[section].title
}

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let filter = filters[indexPath.section].filters[indexPath.row]
if selectedFilters.contains(filter) {
selectedFilters.remove(filter)
delegate?.filterListViewController(self, didDeselect: filter)
} else {
selectedFilters.insert(filter)
delegate?.filterListViewController(self, didSelect: filter)
}
tableView.reloadRows(at: [indexPath], with: .automatic)
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let filter = filters[indexPath.section].filters[indexPath.row]

switch filter {
case .genre(_, let name):
cell.textLabel?.text = name

case .rating(_, let name):
cell.textLabel?.text = name

case .duration(_, let name):
cell.textLabel?.text = name

}

if selectedFilters.contains(filter) {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}

return cell
}

}

在容器 View Controller 中集成

ContainerViewController 中,我们有以下几个实例属性:

  1. FilterListContainerViewMovieListContainerView: 用于添加子 view controller 的容器视图。
  2. FilterListViewControllerMovieListViewController:使用 Storyboard ID 实例化的影片列表和筛选器列表 view controller 的引用。
  3. movie:使用默认硬编码的电影实例的 Movie 数组。

viewDidLoad 被调用时,我们调用该方法来设置子 View Controller。以下是它要执行的几项任务:

  1. 使用 Storyboard ID 实例化 FilterListViewControllerMovieListViewController
  2. 将它们分配给实例属性;
  3. MovieListViewController 分配给 movies 数组;
  4. ContainerViewController 指定为 FilterListViewController 的委托,以便它可以响应过滤器选择;
  5. 设置子视图框架并使用扩展帮助方法将它们添加为子 View Controller。

对于 FilterListViewControllerDelegate 的实现,当选择或取消选择过滤器时,将针对每个类型、评级和持续时间过滤默认的电影数据。然后,过滤器的结果将分配给 MovieListViewControllermovies 属性。要取消选择所有过滤器,它只会分配默认的电影数据。

import UIKit

class ContainerViewController: UIViewController {

@IBOutlet weak var filterListContainerView: UIView!
@IBOutlet weak var movieListContainerView: UIView!

var filterListVC: FilterListViewController!
var movieListVC: MovieListViewController!

let movies = Movie.defaultMovies

override func viewDidLoad() {
super.viewDidLoad()
setupChildViewControllers()
}

private func setupChildViewControllers() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)

let filterListVC = storyboard.instantiateViewController(withIdentifier: "FilterListViewController") as! FilterListViewController
addChild(childController: filterListVC, to: filterListContainerView)
self.filterListVC = filterListVC
self.filterListVC.delegate = self

let movieListVC = storyboard.instantiateViewController(withIdentifier: "MovieListViewController") as! MovieListViewController
movieListVC.movies = movies
addChild(childController: movieListVC, to: movieListContainerView)
self.movieListVC = movieListVC
}

@IBAction func clearFilterTapped(_ sender: Any) {
filterListVC.clearFilter()
}

private func filterMovies(moviesFilter: [MovieFilter]) {
movieListVC.movies = movies
.filter(with: moviesFilter.genreFilters)
.filter(with: moviesFilter.ratingFilters)
.filter(with: moviesFilter.durationFilters)
}

}

extension ContainerViewController: FilterListViewControllerDelegate {

func filterListViewController(_ controller: FilterListViewController, didSelect filter: MovieFilter) {
filterMovies(moviesFilter: Array(controller.selectedFilters))
}

func filterListViewController(_ controller: FilterListViewController, didDeselect filter: MovieFilter) {
filterMovies(moviesFilter: Array(controller.selectedFilters))
}

func filterListViewControllerDidClearFilters(controller: FilterListViewController) {
movieListVC.movies = Movie.defaultMovies
}

}

结论

通过研究示例项目。我们可以看到在我们的应用中使用 View Controller 容器和子 View Controller 的好处。我们可以将单个 View Controller 的职责划分为单独的 View Controller,它们只具有单一职责(SRP)。我们还需要确保子 View Controller 对其父级没有任何依赖。为了让子 View Controller 与父级进行通信,我们可以使用委托模式。

该方法还提供了模块松耦合的优点,这可以为每个组件带来更好的可重用性和可测试性。随著我们的应用变得更大、更复杂,该方法确实有助于我们扩展它。让我们继续学习??,祝你圣诞快乐??,新年快乐??!继续使用 Swift 和 Cocoa !!??

在社交平台上关注我们:

  1. Facebook: facebook.com/AppCodamobile/
  2. Twitter: twitter.com/AppCodaMobile
  3. Instagram: instagram.com/AppCodadotcom

推荐阅读:

相关文章