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

淚已無痕發表於2018-12-29

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

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

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

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

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

Apple MVC 圖解(Apple 公司提供)

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

使用 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/AppCodamobi…
  2. Twitter: twitter.com/AppCodaMobi…
  3. Instagram: instagram.com/AppCodadotc…

相關文章