原文地址:Avoiding Massive View Controller using Containment &
Child View Controller
通過檢視控制器容器和子檢視控制器避免龐大的檢視控制器
View Controller 是一個提供基本構建塊的元件,在 iOS 開發中我們以它為基礎構建應用。在 Apple MVC 世界中,它作為 View 和 Model 的中間人,在兩者之間充當協調者的角色。它以觀察者控制器開始,響應模型更改、更新檢視、使用目標操作從檢視中接受使用者互動、然後更新模型。
作為一名 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
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 UIKitstruct 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 UIKitenum 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 UIKitclass 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/AppCodamobi…
- Twitter: twitter.com/AppCodaMobi…
- Instagram: instagram.com/AppCodadotc…