原文地址:Avoiding Massive View Controller using Containment & Child View Controllergit
View Controller 是一个提供基本构建块的组件,在 iOS 开发中咱们以它为基础构建应用。在 Apple MVC 世界中,它做为 View 和 Model 的中间人,在二者之间充当协调者的角色。它以观察者控制器开始,响应模型更改、更新视图、使用目标操做从视图中接受用户交互、而后更新模型。github
做为一名 iOS 开发者,不少次咱们将面临处理庞大的 View Controller 问题,即使咱们使用了像 MVVM、MVP 或 VIPER 这样的架构。某些时刻,View Controller 在一个屏幕上承担了太多职责。这违反了 SRP(单一职责原则),在模块之间造成了强度耦合,并使得重用和测试每一个组件变得异常困难。编程
咱们能够将下面的应用截图做为示例。你能够看到在一个屏幕上至少存在 3 种职责:swift
若是咱们准备使用单一的 View Controller 来构建此屏幕,因为它在一个 view controller 中承担了过多职责,所以能够保证这个 view controller 将变得很是庞大和臃肿。api
咱们如何解决这个问题呢?其中一个解决方案是使用 View Controller 容器和子 View Controller。如下是使用该方案的好处:数组
MovieListViewController
中,它只负责显示电影列表并对 Movie
模型中的更改作出响应。若是咱们只想显示没有过滤器的电影列表,咱们也能够在另外一个屏幕中重用它。FilterListViewController
中,它单独负责显示和过滤器的选择。当用户选择和取消选择时,咱们可使用委托与父 View Controller 进行通讯。MovieListViewController
中的 Movie
模型。它还设置布局并将子 view controller 添加到容器视图中。你能够在下面的 GitHub 代码仓库中查看完整的项目源代码。架构
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
来显示电影的标题、持续时间、评级和流派。app
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
使用委托
模式,委托有三个方法须要实现:ide
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 !!😋