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

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

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

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

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

Apple MVC 圖解(Apple 公司提供)
Apple MVC 圖解(Apple 公司提供)編程

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

咱們能夠將下面的應用截圖做爲示例。你能夠看到在一個屏幕上至少存在 3 種職責:api

  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 代碼倉庫中查看完整的項目源代碼。app

使用 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 來顯示電影的標題、持續時間、評級和流派。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
    }

}

過濾器列表 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
相關文章
相關標籤/搜索