1. 为iOS APP 构建一个通用的可重用的UITableViewController

TableView Controller是一个基本的UIKit组件,几乎在每个iOS应用中都使用它来在列表中呈现数据集合。当我们想要在UITableViewController中显示不同类型的数据时,大多数时候我们创建一个新子类来显示相关类型的数据。这种方法可以工作,但是如果应用程序中有许多不同类型的数据,则可能导致重复和维护困难。

我们该如何处理和解决这个问题呢?其中一种方法是,我们可以使用简单的抽象使用Swift通用抽象数据类型来创建通用UITableViewController子类,使用Swift通用约束可以用来配置和显示不同类型的数据。

2. Building the Generic TableViewController

我们创建UITableViewController的一个子类叫做GenericTableViewController,我们添加了2种类型的Generic T和Cell。我们添加了一个约束,即Cell必须是UITableViewCell的子类。T将被用作数据的抽象,而Cell将被注册到UITableView,出列以显示每一行的数据为UITableViewCell。

class GenericTableViewController<T, Cell: UITableViewCell>: UITableViewController {  var items: [T]
  var configure: (Cell, T) -> Void
  var selectHandler: (T) -> Void  
  init(items: [T], configure: @escaping (Cell, T) -> Void, selectHandler: @escaping (T) -> Void) {
    self.items = items
    self.configure = configure
    self.selectHandler = selectHandler
    super.init(style: .plain)
    self.tableView.register(Cell.self, forCellReuseIdentifier: "Cell")
  }  ...
}

让我们看一下初始化器,它接受3个参数:

  1. Generic T数组:它将被指定为驱动UITableViewDataSource的实例变量。

  2. Configure closure:这个配置闭包将在传递T数据和Cell时被调用,当tableview将Cell从队列中取出以显示在每一行时。这里,我们设置如何使用数据显示UITableViewCell。(通过在参数中显式声明Cell的类型,编译器将能够隐式推断Cell的类型,只要它是UITableViewCell的子类)

  3. Cell选中的处理闭包。当用户选择/点击单元格中的行时,将通过传递selected来调用此闭包。在这里,我们可以添加当用户点击一行时将调用的逻辑或操作。

初始化器将这3个参数的每已个都分配为类的实例变量,然后它用一个可重用的标识符将Cell注册到UITableView,这个标识符可以用来为数据源将UITableViewCell从可重用队列取出。

class GenericTableViewController<T, Cell: UITableViewCell>: UITableViewController {  ....  //1
  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return items.count
  }  
  
  //2  
  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! Cell
    let item = items[indexPath.row]
    configure(cell, item)
    return cell
  }

  //3
  override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)
    let item = items[indexPath.row]
    selectHandler(item)
  }
}

下面是我们需要覆盖的UITableViewDataSource和UITableViewDelegate方法:

  1. tableView:numberOfRowsInSection: : Here we just return the number of data in the array of T object.
  2. tableView:cellForRowAtIndexPath: : We dequeue the UITableViewCell using the reusable identifier and then cast it as Cell. Then, we get the data from the T array using the index path row. After that, we invoke the configuration closure passing the cell and the data for it to be customized before displayed.
  3. tableView:didSelectRowAtIndexPath: : Here we just get our data from the array using the index path row the invoke the selected handler closure passing the data.

2.1. Using the GenericTableViewController

为了尝试使用不同类型的对象来使用GenericTableViewController,我们创建了两个简单的结构体,Person和Film。在每个结构内部,我们创建一个静态计算变量,该变量将为每个结构返回一个硬编码存根对象数组。

struct Person {
  let name: String
  static var stubPerson: [Person] {
    return [
      Person(name: "Mark Hamill"),
      Person(name: "Harrison Ford"),
      Person(name: "Carrie Fisher"),
      Person(name: "Hayden Christensen"),
      Person(name: "Ewan McGregor"),
      Person(name: "Natalie Portman"),
      Person(name: "Liam Neeson")
   ]
  }
}
struct Film {  
  let title: String
  let releaseYear: Int  
  static var stubFilms: [Film] {
    return [
      Film(title: "Star Wars: A New Hope", releaseYear: 1978),
      Film(title: "Star Wars: Empire Strikes Back", releaseYear: 1982),
      Film(title: "Star Wars: Return of the Jedi", releaseYear:  1984),
      Film(title: "Star Wars: The Phantom Menace", releaseYear: 1999),
      Film(title: "Star Wars: Clone Wars", releaseYear: 2003),
      Film(title: "Star Wars: Revenge of the Sith", releaseYear: 2005)]
  }
}

2.2. Setting Up the Person GenericTableViewController

let personsVC = GenericTableViewController(items: Person.stubPerson,          configure: { (cell: UITableViewCell, person) in
        cell.textLabel?.text = person.name
    }) { (person) in
        print(person.name)
    }

我们将使用标准的UITableViewCell Basic样式显示Person列表。这里,我们实例化传递Person对象数组的GenericTableViewController。完成闭包使用标准UITableViewCell的Cell类型,在配置中,我们只是使用人名分配textLabel文本属性。对于所选的处理程序闭包,我们只需将所选人员的名字打印到控制台。您可以在这里看到Swift隐式类型引用的强大功能,编译器将自动用Person结构替换T泛型。

2.3. Setting Up the Film GenericTableViewController

class SubtitleTableViewCell: UITableViewCell {
  override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: .subtitle, reuseIdentifier: nil)
  }
  ...
}

对于film,我们将使用带有Subtitle样式的UITableViewCell来显示它。为了能够做到这一点,我们需要创建一个子类来覆盖默认样式来使用Subtitle样式,我们称之为SubtitleTableViewCell。

let filmsVC = GenericTableViewController(items: Film.stubFilms, configure: { (cell: SubtitleTableViewCell, film) in
  cell.textLabel?.text = film.title
  cell.detailTextLabel?.text = "\(film.releaseYear)"
}) { (film) in
  print(film.title)
}

我们实例化了传入电影对象数组的GenericTableViewController。对于配置闭包,我们将cell参数的单元格类型显式设置为SubtitleTableViewCell,然后在闭包内部,我们只使用电影的标题和发行年份设置单元格textLabel和detailTextLabel文本属性。对于所选的处理程序闭包,我们只将所选电影的标题打印到控制台。

2.4. Final integration using UITabBarController as Container View Controller

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {  var window: UIWindow?  
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {    

    // Instantiate person and film table view controller
    ...    
    let tabVC = UITabBarController(nibName: nil, bundle: nil)
    tabVC.setViewControllers([
      UINavigationController(rootViewController: personsVC),
      UINavigationController(rootViewController: filmsVC)
    ], animated: false)
    
    window = UIWindow(frame: UIScreen.main.bounds)
    window?.rootViewController = tabVC
    window?.makeKeyAndVisible()
   
    return true
  }
}

为了在iOS项目中显示它,我们将使用一个UITabBarController,它包含了Person和Film实例的GenericTableViewController作为ViewControllers。我们将标签栏控制器设置为UIWindow根视图控制器,并将每个通用表视图控制器嵌入到UINavigationController中。

2.5. Conclusion

我们最终使用Swift泛型为UITableViewController创建了一个抽象容器类。这种方法真的帮助我们能够重用相同的UITableViewController与不同类型的数据源,我们仍然可以自定义使用通用单元格,符合UITableViewCell。Swift泛型是一个非常棒的范例,我们可以用它来创建一个非常强大的抽象。加油快乐,加油万岁,加油😋。