首发于:【译】通过视图控制器容器和子视图控制器避免庞大的视图控制器
通过视图控制器容器和子视图控制器避免庞大的视图控制器
视图控制器容器和子视图控制器图解
View Controller 是一个提供基本构建块的组件,在 iOS 开发中我们以它为基础构建应用。在 Apple MVC 世界中,它作为 View 和 Model 的中间人,在两者之间充当协调者的角色。它以观察者控制器开始,响应模型更改、更新视图、使用目标操作从视图中接受用户交互、然后更新模型。
Apple MVC 图解(Apple 公司提供)
作为一名 iOS 开发者,很多次我们将面临处理庞大的 View Controller 问题,即便我们使用了像 MVVM、MVP 或 VIPER 这样的架构。某些时刻,View Controller 在一个屏幕上承担了太多职责。这违反了 SRP(单一职责原则),在模块之间形成了强度耦合,并使得重用和测试每个组件变得异常困难。
我们可以将下面的应用截图作为示例。你可以看到在一个屏幕上至少存在 3 种职责:
- 显示电影列表;
- 显示可以选择应用于电影列表的过滤列表;
- 清除所选过滤器的选项。
如果我们准备使用单一的 View Controller 来构建此屏幕,由于它在一个 view controller 中承担了过多职责,因此可以保证这个 view controller 将变得非常庞大和臃肿。
我们如何解决这个问题呢?其中一个解决方案是使用 View Controller 容器和子 View Controller。以下是使用该方案的好处:
- 将电影列表封装到
MovieListViewController
中,它只负责显示电影列表并对Movie
模型中的更改做出响应。如果我们只想显示没有过滤器的电影列表,我们也可以在另一个屏幕中重用它。 - 将过滤器中的列表和选择逻辑封装到
FilterListViewController
中,它单独负责显示和过滤器的选择。当用户选择和取消选择时,我们可以使用委托与父 View Controller 进行通信。 - 将主 View Controller 缩减为一个 ContainerViewController,它只负责将选中的过滤器从过滤列表应用到
MovieListViewController
中的Movie
模型。它还设置布局并将子 view controller 添加到容器视图中。
你可以在下面的 GitHub 代码仓库中查看完整的项目源代码。
使用 Storyboard 来布置 View Controller
使用 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
此 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
协议,因此可以使用每个枚举及其属性的哈希值存储在唯一集合
中。过滤器的选择存储在包含 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
}
}
在容器 View Controller 中集成
在 ContainerViewController
中,我们有以下几个实例属性:
-
FilterListContainerView
和MovieListContainerView
: 用于添加子 view controller 的容器视图。 -
FilterListViewController
和MovieListViewController
:使用 Storyboard ID 实例化的影片列表和筛选器列表 view controller 的引用。 -
movie
:使用默认硬编码的电影实例的Movie
数组。
当 viewDidLoad
被调用时,我们调用该方法来设置子 View Controller。以下是它要执行的几项任务:
- 使用 Storyboard ID 实例化
FilterListViewController
和MovieListViewController
; - 将它们分配给实例属性;
- 将
MovieListViewController
分配给 movies 数组; - 将
ContainerViewController
指定为FilterListViewController
的委托,以便它可以响应过滤器选择; - 设置子视图框架并使用扩展帮助方法将它们添加为子 View Controller。
对于 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 !!😋
在社交平台上关注我们:
- Facebook: facebook.com/AppCodamobile/
- Twitter: twitter.com/AppCodaMobile
- Instagram: instagram.com/AppCodadotcom
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。