首发于: 【译】经过视图控制器容器和子视图控制器避免庞大的视图控制器
视图控制器容器和子视图控制器图解git
View Controller 是一个提供基本构建块的组件,在 iOS 开发中咱们以它为基础构建应用。在 Apple MVC 世界中,它做为 View 和 Model 的中间人,在二者之间充当协调者的角色。它以观察者控制器开始,响应模型更改、更新视图、使用目标操做从视图中接受用户交互、而后更新模型。github
Apple MVC 图解(Apple 公司提供)编程
做为一名 iOS 开发者,不少次咱们将面临处理庞大的 View Controller 问题,即使咱们使用了像 MVVM、MVP 或 VIPER 这样的架构。某些时刻,View Controller 在一个屏幕上承担了太多职责。这违反了 SRP(单一职责原则),在模块之间造成了强度耦合,并使得重用和测试每一个组件变得异常困难。swift
咱们能够将下面的应用截图做为示例。你能够看到在一个屏幕上至少存在 3 种职责:api
若是咱们准备使用单一的 View Controller 来构建此屏幕,因为它在一个 view controller 中承担了过多职责,所以能够保证这个 view controller 将变得很是庞大和臃肿。数组
咱们如何解决这个问题呢?其中一个解决方案是使用 View Controller 容器和子 View Controller。如下是使用该方案的好处:架构
MovieListViewController
中,它只负责显示电影列表并对 Movie
模型中的更改作出响应。若是咱们只想显示没有过滤器的电影列表,咱们也能够在另外一个屏幕中重用它。FilterListViewController
中,它单独负责显示和过滤器的选择。当用户选择和取消选择时,咱们能够使用委托与父 View Controller 进行通讯。MovieListViewController
中的 Movie
模型。它还设置布局并将子 view controller 添加到容器视图中。你能够在下面的 GitHub 代码仓库中查看完整的项目源代码。app
使用 Storyboard 来布置 View Controller框架
ContainerViewController
:View Controller 容器提供了 2 个容器视图,用于将子 View Controller 嵌入到水平 UIStackView
中。它还提供了单个 UIButton
来清空所选的过滤器。它还嵌入在充当初始 View Controller 的 UINavigationController
中。FilterListMovieController
:它是 UITableViewController
的子类,具备分类样式和一个用来显示过滤器名称的标准单元格。它还分配了 Storyboard ID,所以能够经过编程的方式在 ContainerViewController
中对它进行实例化。MovieListViewController
:它是 UITableViewController
的子类,具备 Plain 样式和一个用来显示 Movie
属性的小标题单元格。它还跟 FilterListViewController
同样分配了 Storyboard ID。此 view controller 负责显示做为实例公开属性的 Movie
模型列表。咱们使用 Swift 的 didSet
属性观察器来响应模型的更改,而后从新加载 UITableView
。单元格使用默认小标题样式 UITableViewCellStyle
来显示电影的标题、持续时间、评级和流派。ide
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 } }
过滤器列表在 3 个单独的部分中显示 MovieFilter
枚举:流派、评级和持续时间。MovieFilter
枚举自己符合 Hashable
协议,所以能够使用每一个枚举及其属性的哈希值存储在惟一集合
中。过滤器的选择存储在包含 MovieFilter
的 Set
的实例属性下。
要与其余对象通讯,经过 FilterListControllerDelegate
使用委托
模式,委托有三个方法须要实现:
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 } }
在 ContainerViewController
中,咱们有如下几个实例属性:
FilterListContainerView
和 MovieListContainerView
: 用于添加子 view controller 的容器视图。FilterListViewController
和 MovieListViewController
:使用 Storyboard ID 实例化的影片列表和筛选器列表 view controller 的引用。movie
:使用默认硬编码的电影实例的 Movie
数组。当 viewDidLoad
被调用时,咱们调用该方法来设置子 View Controller。如下是它要执行的几项任务:
FilterListViewController
和 MovieListViewController
;MovieListViewController
分配给 movies 数组;ContainerViewController
指定为 FilterListViewController
的委托,以便它能够响应过滤器选择;对于 FilterListViewControllerDelegate
的实现,当选择或取消选择过滤器时,将针对每一个类型、评级和持续时间过滤默认的电影数据。而后,过滤器的结果将分配给 MovieListViewController
的 movies
属性。要取消选择全部过滤器,它只会分配默认的电影数据。
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 !!😋