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个参数:
-
Generic T数组:它将被指定为驱动UITableViewDataSource的实例变量。
-
Configure closure:这个配置闭包将在传递T数据和Cell时被调用,当tableview将Cell从队列中取出以显示在每一行时。这里,我们设置如何使用数据显示UITableViewCell。(通过在参数中显式声明Cell的类型,编译器将能够隐式推断Cell的类型,只要它是UITableViewCell的子类)
-
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方法:
- tableView:numberOfRowsInSection: : Here we just return the number of data in the array of T object.
- 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.
- 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泛型是一个非常棒的范例,我们可以用它来创建一个非常强大的抽象。加油快乐,加油万岁,加油😋。