雪花臺灣

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

原文地址: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 代碼倉庫中查看完整的項目源代碼。

使用 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

推薦閱讀:

相關文章