首發於: 【譯】經過視圖控制器容器和子視圖控制器避免龐大的視圖控制器
視圖控制器容器和子視圖控制器圖解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 !!😋