[译] 经过视图控制器容器和子视图控制器避免庞大的视图控制器

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

经过视图控制器容器和子视图控制器避免庞大的视图控制器

视图控制器容器和子视图控制器图解

视图控制器容器和子视图控制器图解

View Controller 是一个提供基本构建块的组件,在 iOS 开发中咱们以它为基础构建应用。在 Apple MVC 世界中,它做为 View 和 Model 的中间人,在二者之间充当协调者的角色。它以观察者控制器开始,响应模型更改、更新视图、使用目标操做从视图中接受用户交互、而后更新模型。github

Apple MVC 图解(Apple 公司提供)

Apple MVC 图解(Apple 公司提供)

做为一名 iOS 开发者,不少次咱们将面临处理庞大的 View Controller 问题,即使咱们使用了像 MVVM、MVP 或 VIPER 这样的架构。某些时刻,View Controller 在一个屏幕上承担了太多职责。这违反了 SRP(单一职责原则),在模块之间造成了强度耦合,并使得重用和测试每一个组件变得异常困难。编程

咱们能够将下面的应用截图做为示例。你能够看到在一个屏幕上至少存在 3 种职责:swift

  1. 显示电影列表;
  2. 显示能够选择应用于电影列表的过滤列表;
  3. 清除所选过滤器的选项。

若是咱们准备使用单一的 View Controller 来构建此屏幕,因为它在一个 view controller 中承担了过多职责,所以能够保证这个 view controller 将变得很是庞大和臃肿。api

咱们如何解决这个问题呢?其中一个解决方案是使用 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 来显示电影的标题、持续时间、评级和流派。app

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 使用委托模式,委托有三个方法须要实现:ide

  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…
相关文章
相关标签/搜索