本指南是供初学者通过 Swift
中的自动布局以编程方式学习 UITableView
类的基础的。
如何以代码方式创建表视图? 让我们直接进入编码部分,但首先:启动 Xcode
,创建一个新的 iOS
单视图应用程序项目,像往常一样输入该项目的名称和详细信息,使用 Swift
,最后立即打开 ViewController.swift
文件。 现在抓住键盘! ⌨️
在本教程中,我不会使用 Interface Builder
,那么我们如何以代码方式创建视图? 有一个称为 loadView
的方法,你应该在其中将自定义视图添加到视图层次结构中。 你可以选择+单击Xcode中的方法名称并阅读有关 loadView
方法的讨论,但让我总结一下整个过程。
我们将使用弱属性来保存对表格视图的引用。 接下来,我们重写 loadView
方法并调用 super
,以使用视图对象(如果有控制器的话,在 nib or a storyboard
文件中)加载控制器的 self.view
属性。 之后,我们将全新的视图分配给本地属性,关闭系统提供的布局,然后将表视图插入到视图层次结构中。 最后,我们使用锚创建一些实际的约束,并保存指向弱属性的指针。 简单! 🤪
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class ViewController : UIViewController { weak var tableView: UITableView! override func loadView () { super .loadView() let tableView = UITableView (frame: .zero, style: .plain) tableView.translatesAutoresizingMaskIntoConstraints = false self .view.addSubview(tableView) NSLayoutConstraint .activate([ self .view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: tableView.topAnchor), self .view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), self .view.leadingAnchor.constraint(equalTo: tableView.leadingAnchor), self .view.trailingAnchor.constraint(equalTo: tableView.trailingAnchor), ]) self .tableView = tableView } }
始终使用自动布局锚来指定视图约束,如果你不知道如何使用它们,请查看 《Swift使用布局锚点添加约束》
,学习此 API
仅需15分钟,并且你不会后悔。 对于任何 iOS
开发者来说,这都是一个非常有用的工具! 😉
你可能会问:我应该使用弱属性还是强属性作为视图引用? 我会说,在大多数情况下,如果你不压制 self.view
,则应使用弱项! 视图层次结构将通过强大的参考来保存你的自定义视图,因此不需要愚蠢的保留周期和内存泄漏。 相信我! 🤥
UITableView DataSource 好的,我们有一个空的表格视图,让我们显示一些单元格! 为了用真实数据填充表格视图,我们必须遵守 UITableViewDataSource
协议。 通过简单的委托模式,我们可以为 UITableView
类提供各种信息,因此它将知道需要多少节和行,应该为每行显示哪种单元格以及更多的小细节。
另一件事是,UITableView
是一个非常有效的类。 它会重用当前屏幕上未显示的所有单元格,因此,如果你需要处理数百或数千个项目,它将消耗比 UIScrollView
更少的内存。 为了支持这种行为,我们必须使用重用标识符注册我们的单元格类,因此基础系统将知道特定位置需要哪种单元格。 ⚙️
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class ViewController : UIViewController { var items: [String ] = [ "👽" , "🐱" , "🐔" , "🐶" , "🦊" , "🐵" , "🐼" , "🐷" , "💩" , "🐰" , "🤖" , "🦄" , "🐻" , "🐲" , "🦁" , "💀" , "🐨" , "🐯" , "👻" , "🦖" , ] override func viewDidLoad () { super .viewDidLoad() self .tableView.register(UITableViewCell .self , forCellReuseIdentifier: "UITableViewCell" ) self .tableView.dataSource = self } } extension ViewController : UITableViewDataSource { func tableView (_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self .items.count } func tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell" , for : indexPath) let item = self .items[indexPath.item] cell.textLabel?.text = item return cell } }
在我们的视图控制器文件中添加几行代码后,表格视图现在可以显示一个漂亮的表情符号列表! 我们正在使用 UIKit
的内置 UITableViewCell
类,如果你很好地使用 “iOS-system-like”
的单元格设计,那么它将非常方便。 通过告诉我们的节中有多少项(目前只有一个节),我们还符合数据源协议,并在 indexPath
委托方法的行中为行配置了我们的单元格。 😎
自定义UItableViewCell UITableViewCell
可以提供一些基本元素来显示数据(标题,详细信息,不同样式的图像),但是通常你需要自定义设计的单元格。 这是自定义单元格子类的基本模板,在代码之后,我将解释所有方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class MyCell : UITableViewCell { override init (style: UITableViewCell .CellStyle , reuseIdentifier: String? ) { super .init (style: style, reuseIdentifier: reuseIdentifier) self .initialize() } required init ?(coder aDecoder: NSCoder ) { super .init (coder: aDecoder) self .initialize() } func initialize () { } override func prepareForReuse () { super .prepareForReuse() } }
如果要以编程方式使用默认的UITableViewCell
,但使用不同的样式(在初始化单元格后没有设置 cellStyle
的选项),则 init(style:reuseIdentifier
) 方法是重写单元格样式属性的好地方。 例如,如果你需要一个 .value1
样式的单元格,只需将参数直接传递给超级调用即可。 这样,你可以使用 4
种预定义的单元格样式。
***提示:
*** 你还必须实现 init(coder :)
,所以你应该创建一个通用的 initialize()
函数,在其中你可以在视图层次结构中添加自定义视图,就像我们在上面的 loadView
方法中所做的那样。 如果使用的是 xib
文件和 IB
,则可以使用 awakeFromNib
方法通过标准 @IBOutlet
属性为视图添加额外的样式(或向层次结构中添加额外的视图)。 👍
我们要讨论的最后一个方法是 prepareForReuse
。 正如我之前提到的,单元格被重用,因此,如果要重置某些属性(例如单元格的背景),可以在此处进行操作。 在单元将被重用之前将调用此方法。
让我们创建两个新的单元格子类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class DetailCell : UITableViewCell { override init (style: UITableViewCell .CellStyle , reuseIdentifier: String? ) { super .init (style: .subtitle, reuseIdentifier: reuseIdentifier) self .initialize() } required init ?(coder aDecoder: NSCoder ) { super .init (coder: aDecoder) self .initialize() } func initialize () { } override func prepareForReuse () { super .prepareForReuse() self .textLabel?.text = nil self .detailTextLabel?.text = nil self .imageView?.image = nil } }
我们的自定义单元格将具有大的图像背景,并在视图的中心添加一个带有自定义大小的系统字体的标题标签。 另外,我已将 Swift logo
作为 asset
添加到项目中,因此我们可以得到一个不错的演示图像。 🖼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 class CustomCell : UITableViewCell { weak var coverView: UIImageView! weak var titleLabel: UILabel! override init (style: UITableViewCell .CellStyle , reuseIdentifier: String? ) { super .init (style: style, reuseIdentifier: reuseIdentifier) self .initialize() } required init ?(coder aDecoder: NSCoder ) { super .init (coder: aDecoder) self .initialize() } func initialize () { let coverView = UIImageView (frame: .zero) coverView.translatesAutoresizingMaskIntoConstraints = false self .contentView.addSubview(coverView) self .coverView = coverView let titleLabel = UILabel (frame: .zero) titleLabel.translatesAutoresizingMaskIntoConstraints = false self .contentView.addSubview(titleLabel) self .titleLabel = titleLabel NSLayoutConstraint .activate([ self .contentView.topAnchor.constraint(equalTo: self .coverView.topAnchor), self .contentView.bottomAnchor.constraint(equalTo: self .coverView.bottomAnchor), self .contentView.leadingAnchor.constraint(equalTo: self .coverView.leadingAnchor), self .contentView.trailingAnchor.constraint(equalTo: self .coverView.trailingAnchor), self .contentView.centerXAnchor.constraint(equalTo: self .titleLabel.centerXAnchor), self .contentView.centerYAnchor.constraint(equalTo: self .titleLabel.centerYAnchor), ]) self .titleLabel.font = UIFont .systemFont(ofSize: 64 ) } override func prepareForReuse () { super .prepareForReuse() self .coverView.image = nil } }
就是这样,让我们开始使用这些新单元格。 我什至会告诉你如何为给定单元格设置自定义高度,以及如何正确处理单元格选择,但是首先我们需要了解另一个委托协议。 🤝
UITableViewDelegate 使用 UITableViewDelegate
负责很多事情,但是现在,我们将只讨论一些有趣的方面,例如如何处理单元格选择以及为表格中的每个项目提供自定义单元格高度。 示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class ViewController : UIViewController { override func viewDidLoad () { super .viewDidLoad() self .tableView.register(UITableViewCell .self , forCellReuseIdentifier: "UITableViewCell" ) self .tableView.register(DetailCell .self , forCellReuseIdentifier: "DetailCell" ) self .tableView.register(CustomCell .self , forCellReuseIdentifier: "CustomCell" ) self .tableView.dataSource = self self .tableView.delegate = self } } extension ViewController : UITableViewDataSource { func tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell" , for : indexPath) as ! CustomCell let item = self .items[indexPath.item] cell.titleLabel.text = item cell.coverView.image = UIImage (named: "Swift" ) return cell } } extension ViewController : UITableViewDelegate { func tableView (_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 128 } func tableView (_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true ) let item = self .items[indexPath.item] let alertController = UIAlertController (title: item, message: "is in da house!" , preferredStyle: .alert) let action = UIAlertAction (title: "Ok" , style: .default ) { _ in } alertController.addAction(action) self .present(alertController, animated: true , completion: nil ) } }
如你所见,我正在 viewDidLoad
方法中注册全新的自定义单元格类。 我还更改了 cellForRowAt indexPath
方法中的代码,因此我们可以使用 CustomCell
类代替 UITableViewCells
。 不必担心强制转换,如果此时出现问题,你的应用程序应该崩溃。 🙃
我们在这里使用两种委托方法。 在第一个中,我们必须返回一个数字,系统将使用该高度作为单元格。 如果要在每行中使用不同的单元格高度,则也可以通过检查 indexPath
属性或类似属性来实现。 第二个是选择的处理程序。 如果有人点击某个单元格,则将调用此方法,你可以执行一些操作。
带标题和页脚的 Section 表格视图中可能有多个部分,我不会赘述,因为它非常简单。 你只需要使用 indexPaths
即可获取/设置/返回每个节和单元格的正确数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 import UIKitclass ViewController : UIViewController { weak var tableView: UITableView! var placeholderView = UIView (frame: .zero) var isPullingDown = false enum Style { case `default ` case subtitle case custom } var style = Style .default var items: [String : [String ]] = [ "Originals" : ["👽" , "🐱" , "🐔" , "🐶" , "🦊" , "🐵" , "🐼" , "🐷" , "💩" , "🐰" ,"🤖" , "🦄" ], "iOS 11.3" : ["🐻" , "🐲" , "🦁" , "💀" ], "iOS 12" : ["🐨" , "🐯" , "👻" , "🦖" ], ] override func loadView () { super .loadView() let tableView = UITableView (frame: .zero, style: .plain) tableView.translatesAutoresizingMaskIntoConstraints = false self .view.addSubview(tableView) NSLayoutConstraint .activate([ self .view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: tableView.topAnchor), self .view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), self .view.leadingAnchor.constraint(equalTo: tableView.leadingAnchor), self .view.trailingAnchor.constraint(equalTo: tableView.trailingAnchor), ]) self .tableView = tableView } override func viewDidLoad () { super .viewDidLoad() self .tableView.register(UITableViewCell .self , forCellReuseIdentifier: "UITableViewCell" ) self .tableView.register(DetailCell .self , forCellReuseIdentifier: "DetailCell" ) self .tableView.register(CustomCell .self , forCellReuseIdentifier: "CustomCell" ) self .tableView.dataSource = self self .tableView.delegate = self self .tableView.separatorStyle = .singleLine self .tableView.separatorColor = .lightGray self .tableView.separatorInset = .zero self .navigationItem.rightBarButtonItem = .init (barButtonSystemItem: .refresh, target: self , action: #selector(self .toggleCells)) } @objc func toggleCells () { switch self .style { case .default : self .style = .subtitle case .subtitle: self .style = .custom case .custom: self .style = .default } DispatchQueue .main.async { self .tableView.reloadData() } } func key (for section: Int) -> String { let keys = Array (self .items.keys).sorted { first, last -> Bool in if first == "Originals" { return true } return first < last } let key = keys[section] return key } func items (in section: Int) -> [String ] { let key = self .key(for : section) return self .items[key]! } func item (at indexPath: IndexPath) -> String { let items = self .items(in : indexPath.section) return items[indexPath.item] } } extension ViewController : UITableViewDataSource { func numberOfSections (in tableView: UITableView) -> Int { return self .items.keys.count } func tableView (_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self .items(in : section).count } func tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let item = self .item(at: indexPath) let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell" , for : indexPath) as ! CustomCell cell.titleLabel.text = item cell.coverView.image = UIImage (named: "Swift" ) return cell } func tableView (_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return self .key(for : section) } } extension ViewController : UITableViewDelegate { func tableView (_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 128 } func tableView (_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true ) let item = self .item(at: indexPath) let alertController = UIAlertController (title: item, message: "is in da house!" , preferredStyle: .alert) let action = UIAlertAction (title: "Ok" , style: .default ) { _ in } alertController.addAction(action) self .present(alertController, animated: true , completion: nil ) } }
尽管上面的代码片段中添加了一个有趣的内容。 你可以为每个部分都有一个自定义标题,只需添加 titleForHeaderInSection
数据源方法即可。 是的,看起来像狗屎一样,但这与 UI / UX
无关。 😂
但是,如果你对部分标题的布局不满意,可以创建一个自定义类并使用它来代替内置类。 这是执行自定义节标题视图的方法。 这是可重用视图的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class HeaderView : UITableViewHeaderFooterView { weak var titleLabel: UILabel! override init (reuseIdentifier: String? ) { super .init (reuseIdentifier: reuseIdentifier) self .initialize() } required init ?(coder aDecoder: NSCoder ) { super .init (coder: aDecoder) self .initialize() } func initialize () { let titleLabel = UILabel (frame: .zero) titleLabel.translatesAutoresizingMaskIntoConstraints = false self .contentView.addSubview(titleLabel) self .titleLabel = titleLabel NSLayoutConstraint .activate([ self .contentView.centerXAnchor.constraint(equalTo: self .titleLabel.centerXAnchor), self .contentView.centerYAnchor.constraint(equalTo: self .titleLabel.centerYAnchor), ]) self .contentView.backgroundColor = .black self .titleLabel.font = UIFont .boldSystemFont(ofSize: 16 ) self .titleLabel.textAlignment = .center self .titleLabel.textColor = .white } }
只剩下几件事要做,你必须注册标题视图,就像你为单元格所做的一样。 完全相同,只是页眉和页脚视图有一个单独的注册“池”。 最后,你必须实现两个其他但相对简单(和熟悉)的委托方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 extension ViewController : UITableViewDelegate { func tableView (_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { return 32 } func tableView (_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderView" ) as ! HeaderView view.titleLabel.text = self .key(for : section) return view } }
页脚的工作原理与页眉完全相同,只需支持相应的数据源和委托方法即可。
根据行或节的索引或任何特定的业务需求,你甚至可以在同一张表视图中具有多个单元格。 我不会在这里进行演示,因为我有一个更好的解决方案,用于混合和重用 CoreKit
框架中的单元格。 🤓
Section titles & indexes 好吧,如果你的大脑还没有融化,我将向你展示另外两个对于初学者来说可能很有趣的小东西。 第一个基于两种其他数据源方法,对于长列表而言,这是一个非常令人愉快的添加。 (我更喜欢搜索栏!)🤯
1 2 3 4 5 6 7 8 9 10 11 extension ViewController : UITableViewDataSource { func sectionIndexTitles (for tableView: UITableView) -> [String ]? { return ["1" , "2" , "3" ] } func tableView (_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int { return index } }
如果要在上面实现这些方法,则可以在表视图的右侧为各节提供一个小的索引视图,因此最终用户将能够在各节之间快速跳转。 就像在官方联系人应用程序中一样。 📕
Selection vs highlight 当你用手指按住单元格时,单元格高亮。 如果你从单元格中松开手指,则将选择该单元格。
不要过于复杂。 你只需在自定义单元格类中实现两个方法即可使所有工作正常进行。 我更喜欢立即取消选择我的单元格(如果某些数据选择器布局未使用它们)。 这是代码:
1 2 3 4 5 6 7 8 9 10 11 12 class CustomCell : UITableViewCell { override func setSelected (_ selected: Bool, animated: Bool) { self .coverView.backgroundColor = selected ? .red : .clear } override func setHighlighted (_ highlighted: Bool, animated: Bool) { self .coverView.backgroundColor = highlighted ? .blue : .clear } }
如你所见,这非常简单,但是大多数初学者都不知道该怎么做。 此外,他们通常会在重用逻辑发生之前忘记重置单元格,因此列表会不断弄乱单元格状态。 不必太担心这些问题,它们会消失,因为你将对 UITableView API
更有经验。