原文地址:Avoiding Massive View Controller using Containment & Child View Controller

首發於:【譯】通過視圖控制器容器和子視圖控制器避免龐大的視圖控制器

通過視圖控制器容器和子視圖控制器避免龐大的視圖控制器

視圖控制器容器和子視圖控制器圖解

View Controller 是一個提供基本構建塊的組件,在 iOS 開發中我們以它為基礎構建應用。在 Apple MVC 世界中,它作為 View 和 Model 的中間人,在兩者之間充當協調者的角色。它以觀察者控制器開始,響應模型更改、更新視圖、使用目標操作從視圖中接受用戶交互、然後更新模型。

Apple MVC 圖解(Apple 公司提供)

作為一名 iOS 開發者,很多次我們將面臨處理龐大的 View Controller 問題,即便我們使用了像 MVVM、MVP 或 VIPER 這樣的架構。某些時刻,View Controller 在一個屏幕上承擔了太多職責。這違反了 SRP(單一職責原則),在模塊之間形成了強度耦合,並使得重用和測試每個組件變得異常困難。

我們可以將下面的應用截圖作為示例。你可以看到在一個屏幕上至少存在 3 種職責:

  1. 顯示電影列表;
  2. 顯示可以選擇應用於電影列表的過濾列表;
  3. 清除所選過濾器的選項。

如果我們準備使用單一的 View Controller 來構建此屏幕,由於它在一個 view controller 中承擔了過多職責,因此可以保證這個 view controller 將變得非常龐大和臃腫。

我們如何解決這個問題呢?其中一個解決方案是使用 View Controller 容器和子 View Controller。以下是使用該方案的好處:

  1. 將電影列表封裝到 MovieListViewController 中,它只負責顯示電影列表並對 Movie 模型中的更改做出響應。如果我們只想顯示沒有過濾器的電影列表,我們也可以在另一個屏幕中重用它。
  2. 將過濾器中的列表和選擇邏輯封裝到 FilterListViewController 中,它單獨負責顯示和過濾器的選擇。當用戶選擇和取消選擇時,我們可以使用委託與父 View Controller 進行通信。
  3. 將主 View Controller 縮減為一個 ContainerViewController,它只負責將選中的過濾器從過濾列表應用到 MovieListViewController 中的 Movie 模型。它還設置布局並將子 view controller 添加到容器視圖中。

你可以在下面的 GitHub 代碼倉庫中查看完整的項目源代碼。

  • alfianlosari/Filter-MVC-iOS

使用 Storyboard 來布置 View Controller

使用 Storyboard 來布置 View Controller

  1. ContainerViewController:View Controller 容器提供了 2 個容器視圖,用於將子 View Controller 嵌入到水平 UIStackView 中。它還提供了單個 UIButton 來清空所選的過濾器。它還嵌入在充當初始 View Controller 的 UINavigationController 中。
  2. FilterListMovieController:它是 UITableViewController 的子類,具有分類樣式和一個用來顯示過濾器名稱的標準單元格。它還分配了 Storyboard ID,因此可以通過編程的方式在 ContainerViewController 中對它進行實例化。
  3. 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 協議,因此可以使用每個枚舉及其屬性的哈希值存儲在唯一集合中。過濾器的選擇存儲在包含 MovieFilterSet 的實例屬性下。

要與其他對象通信,通過 FilterListControllerDelegate 使用委託模式,委託有三個方法需要實現:

  1. 選擇一個過濾器。
  2. 取消選擇一個過濾器。
  3. 清空所有已選擇過濾器。

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 中,我們有以下幾個實例屬性:

  1. FilterListContainerViewMovieListContainerView: 用於添加子 view controller 的容器視圖。
  2. FilterListViewControllerMovieListViewController:使用 Storyboard ID 實例化的影片列表和篩選器列表 view controller 的引用。
  3. movie:使用默認硬編碼的電影實例的 Movie 數組。

viewDidLoad 被調用時,我們調用該方法來設置子 View Controller。以下是它要執行的幾項任務:

  1. 使用 Storyboard ID 實例化 FilterListViewControllerMovieListViewController
  2. 將它們分配給實例屬性;
  3. MovieListViewController 分配給 movies 數組;
  4. ContainerViewController 指定為 FilterListViewController 的委託,以便它可以響應過濾器選擇;
  5. 設置子視圖框架並使用擴展幫助方法將它們添加為子 View Controller。

對於 FilterListViewControllerDelegate 的實現,當選擇或取消選擇過濾器時,將針對每個類型、評級和持續時間過濾默認的電影數據。然後,過濾器的結果將分配給 MovieListViewControllermovies 屬性。要取消選擇所有過濾器,它只會分配默認的電影數據。

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 !!??

在社交平台上關注我們:

  1. Facebook: facebook.com/AppCodamobile/
  2. Twitter: twitter.com/AppCodaMobile
  3. Instagram: instagram.com/AppCodadotcom

推薦閱讀:

相关文章