Table View学习笔记—第一部分

公良扬
2023-12-01

各位iOS开发大佬们好:
我是一名Swift+SwiftUI栈的iOS小白,目前还在上大三,最近准备实习,面试的过程中发现现在大公司很多还在用OC + UIKit的技术栈,OC我还在考虑要不要学,目前想先把UIKit学完,这是我在官网学习UIKit英文文档时摘录的本人认为的重点,如果你们也觉得对你们有用的话欢迎持续关注,我大概一天更一节,有事除外。格式什么的我也就不做了,翻译都是我自己翻译的,哪里不对欢迎在评论区指正,感谢各位大佬支持

今天2021年12月15日,明天有点事,可能更不了

这一章是Table View的第一部分

Overview

table view其实就是一个纵向的可滚动视图被分割成一行一行的,section可以将这些行分组
一个table view由以下几部分组成
Cells. A cell provides the visual representation for your content. You can use the default cells provided by UIKit or define custom cells to suit the needs of your app.
Table view controller. You typically use a UITableViewController object to manage a table view. You can use other view controllers too, but a table view controller is required for some table-related features to work.
Your data source object. This object adopts the UITableViewDataSource protocol and provides the data for the table.
Your delegate object. This object adopts the UITableViewDelegate protocol and manages user interactions with the table’s contents.

单元,就是每一行,里面的内容可以由多个默认单元组合成自定义单元
TVC,管理tableview的各种比如大小,位置,布局等等乱七八糟的
数据源,遵守UITableViewDataSource 协议并为table view提供data
代理对象,遵守UITableViewDelegate 协议管理用户与内容的交互

Saving and Restoring the Table’s Current State
Table views support UIKit app restoration. To save and restore the table’s data, assign a nonempty value to the table view’s restorationIdentifier property. When you save its parent view controller, the table view automatically saves the index paths for the currently selected and visible rows. If the table’s data source object adopts the UIDataSourceModelAssociation protocol, the table stores the unique IDs that you provide for those items instead of their index paths.

保存和重载table当前状态,给TV的restorationIdentifier 属性赋一个非空值,当保存它的父VC时,TV自动保存当前被选中的可视行的索引路径,如果table的数据源遵守UIDataSourceModelAssociation 协议,那么table将会保存你提供给item的唯一ID

Filling a Table with Data

TV是数据驱动的,你需要提供一个遵守UITableViewDataSource 协议的数据源对象,TV管理着view并且和数据源对象一起保证数据最新

Provide the Numbers of Rows and Sections

Before it appears onscreen, a table view asks you to specify the total number of rows and sections. Your data source object provides this information using two methods:
func numberOfSections(in tableView: UITableView) -> Int // Optional
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
In your implementations of these methods, return the row and section counts as quickly as possible. Doing so might require you to structure your data in a way that makes it easy to retrieve the row and section information. For example, consider using arrays to manage your table’s data. Arrays are good tools for organizing both sections and rows because they match the natural organization of the table view itself.
TV显示之前必须知道一共有多少行有多少section,用这两个方法告诉它
func numberOfSections(in tableView: UITableView) -> Int // Optional
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
这两个方法的实现当中,尽快的返回行数和section数,需要你以一个简单的方法结构化你的数据使它能够快速取到行数和section数的信息,比如将数据弄成数组,与TV很贴合,取大小也能直接用内置count

Before a table view appears onscreen, it asks its data source object to provide cells for the rows in or near the visible portion of the table. Your data source object’s tableView(:cellForRowAt:) method must respond quickly. Implement this method with the following pattern:
Call the table view’s dequeueReusableCell(withIdentifier:for:) method to retrieve a cell object.
Configure the cell’s views with your app’s custom data.
Return the cell to the table view.
For standard cell styles, UITableViewCell contains properties with the views you need to configure. For custom cells, you add views to your cell at design time and outlets to access them.
TV可视前需要数据源提供每行的单元,数据源对象的tableView(
:cellForRowAt:) 方法必须快速响应,以下模式实现这个方法:
调用TV的 dequeueReusableCell(withIdentifier:for:) 方法取一个单元对象
用数据配置单元
返回单元给TV
对于标准单元风格, UITableViewCell里面有你需要的属性,对于自定义单元,你需要在设计时就添加好view并为他们设计访问接口
代码示例

override func tableView(_ tableView: UITableView,
                        cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   // Ask for a cell of the appropriate type.
   let cell = tableView.dequeueReusableCell(withIdentifier: "basicStyleCell", for: indexPath)
        
   // Configure the cell’s contents with the row and section number.
   // The Basic cell style guarantees a label view is present in textLabel.
   cell.textLabel!.text = "Row \(indexPath.row)"
   return cell
}

Table views do not ask you to create cells for each of the table’s rows. Instead, table views manage cells lazily, asking you only for those cells that are in or near the visible portion of the table. Creating cells lazily reduces the amount of memory the table uses. However, it also means your data source object must create cells quickly. Do not use your tableView(:cellForRowAt:) method to load your table’s data or perform lengthy operations
TV不会要求你为table的每一行创建单元,TV懒管理(即有需要才管理)单元格,仅要求您提供表可见部分内或附近的单元格。懒加载减少了table的内存使用,当然这就需要数据源对象必须很快的创建单元,所以不要用tableView(
:cellForRowAt:) 方法加载table的数据或执行耗时操作
提前匹配数据以提高性能
滚动体验对于TV很重要,如果table匹配数据会引起很繁琐的操作,比如需要动数据库,那就用一个遵守UITableViewDataSourcePrefetching协议的prefetching 数据源对象就行,在滑动前异步加载数据

Estimating the Height of a Table’s Scrolling Area

Before a table view appears onscreen, it must compute the height of its content view, because it needs that information to configure scrolling-related parameters. If you don’t provide estimated heights for items, the table view must compute the actual height of items up front, which can be expensive.
TV显示之前必须计算内容的高度,因为TV需要这个值配置跟滑动有关的参数,如果你不提供高度预估,那么TV必须计算实际已显示的内容高度,这对性能影响很大

The table view provides default height estimates for table view items based on the standard header, footer, and row styles. If your table’s items are significantly shorter or taller than the default values, you can supply custom estimates by assigning values to your table’s estimatedRowHeight, estimatedSectionHeaderHeight, and estimatedSectionFooterHeight properties. If the height of individual items varies, provide custom estimates using the following methods of your delegate object:

tableView(_:estimatedHeightForRowAt:)
tableView(_:estimatedHeightForHeaderInSection:)
tableView(_:estimatedHeightForFooterInSection:) 

When estimating the heights of headers, footers, and rows, speed is more important than precision. The table view asks for estimates for every item in your table, so do not perform long-running operations in your delegate methods. Instead, generate estimates that are close enough to be useful for scrolling. The table view replaces your estimates with the actual item heights as those items appear onscreen.
TV根据标准header,footer还有row style提供了默认的高度预估,如果你的这些东西都明显比标准的高或者低,那么需要通过给table的estimatedRowHeight, estimatedSectionHeaderHeight, 和 estimatedSectionFooterHeight 属性赋值来自定义预估值,如果个别的差距很大的话用一下代理对象的方法提供预估值:
tableView(:estimatedHeightForRowAt:)
tableView(
:estimatedHeightForHeaderInSection:)
tableView(_:estimatedHeightForFooterInSection:)
预估时越快越好,因为最后当那些东西出现在屏幕上时TV会用实际值替代预估值
When the table view uses height estimates, it actively manages the contentOffset and contentSize properties inherited from its scroll view. Do not attempt to read or modify these properties directly. Their values are meaningful only to UITableView.
当TV用高度预估的时候,它会管理从scroll view那继承来的contentOffset 和 contentSize 属性,不要尝试读取或更改这俩属性值,

为每个单元赋一个重用标识符

Reuse identifiers facilitate the creation and recycling of your table’s cells. A reuse identifier is a string that you assign to each prototype cell of your table.Each cell in a table view must have a unique reuse identifier
When you need a cell object at runtime, call the table view’s dequeueReusableCell(withIdentifier:for:) method, passing the reuse identifier for the cell you want. The table view maintains an internal queue of already-created cells. If the queue contains a cell of the requested type, the table view returns that cell. If not, it creates a new cell using the prototype cell in your storyboard. Reusing cells improves performance by minimizing memory allocations during critical times, such as during scrolling.
重用标识符简化了单元创建和回收的过程,它是一个赋值给table里每个prototype单元的字符串,table中的每个单元都必须有唯一的重用标识符。当你在运行时需要一个单元对象,调用TV的dequeueReusableCell(withIdentifier:for:) 方法并传入你需要的单元的重用标识符。TV持有一个已创建单元的队列,如果这个队列里有传进来的那个单元,那么此方法将返回这个单元,如果没有,他会用原始类型的单元创建一个新的单元。单元的重用通过减少在关键时刻比如滚动时对内存的申请和使用提高性能

Configure a Cell with a Built-In Style

The simplest way to configure a cell is to use one of the built-in styles provided by UITableViewCell. You use these styles as is; you do not need to provide a custom subclass to manage the cell. Each style incorporates one or two labels, with the style determining the positions of the labels within the cell’s content area. Most of the styles also incorporate an image at the leading edge of the cell’s content.
In your tableView(:cellForRowAt:) method, configure the content of your cell using the textLabel, detailTextLabel, and imageView properties of UITableViewCell. Those properties contain views, but the cell object only assigns a view if the style supports the corresponding content. For example, the Basic cell style does not support a detail string, so the detailTextLabel property is nil for that style.
使用UITableViewCell提供的内置style配置单元,不需要提供子类去管理单元。在tableView(
:cellForRowAt:) 方法中使用UITableViewCell的 textLabel, detailTextLabel, and imageView 属性配置单元的内容。这些属性包含视图,但单元对象单元对象仅会在样式支持相应内容时分配视图。
示例代码如下

override func tableView(_ tableView: UITableView, 
             cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   // Reuse or create a cell. 
   let cell = tableView.dequeueReusableCell(withIdentifier: "basicStyle", for: indexPath)


   // For a standard cell, use the UITableViewCell properties.
   cell.textLabel!.text = "Title text"
   cell.imageView!.image = UIImage(named: "bunny")
   return cell
}

Configure a Cell with Custom Views

For appearances other than the standard styles, use the custom cell style. With a custom cell, you specify the views you want in the cell, their configurations, and their sizes and positions. Static views such as labels and images make the best content for cells. Avoid views that require user interactions such as controls. Do not include scroll views, table views, collection views, or other complex container views in cells. You may include stack views in your cells, but minimize the number of items in your stack view to improve performance.
For custom cells, you need to define a UITableViewCell subclass to access your cell’s views. Add outlets to your subclass and connect those outlets to the corresponding views in your prototype cell.
自定义单元最好用静态视图,不要用容器VC,可以用横纵排列的视图,但要控制横纵排列的量,写一个 UITableViewCell 子类访问单元的视图,记得写好接口

class FoodCell: UITableViewCell {
    @IBOutlet var name : UILabel?
    @IBOutlet var plantDescription : UILabel?
    @IBOutlet var picture : UIImageView?
}

In the tableView(:cellForRowAt:) method of your data source, use your cell’s outlets to assign values to any views.
在数据源的tableView(
:cellForRowAt:) 方法中,用单元的接口给其他视图赋值

override func tableView(_ tableView: UITableView, 
             cellForRowAt indexPath: IndexPath) -> UITableViewCell {


   // Reuse or create a cell of the appropriate type.
   let cell = tableView.dequeueReusableCell(withIdentifier: "foodCellType", 
                         for: indexPath) as! FoodCell


   // Fetch the data for the row.
   let theFood = foods[indexPath.row]
        
   // Configure the cell’s contents with data from the fetched object.
   cell.name?.text = theFood.name
   cell.plantDescription?.text = theFood.description
   cell.picture?.image = theFood.picture
        
   return cell
}

Change the Height of Rows

A table view tracks the height of rows separately from the cells that represent them. UITableView provides default sizes for rows, but you can override the default height by assigning a custom value to the table view’s rowHeight property. Always use this property when the height of all of your rows is the same. Doing so is more efficient than returning the height values from your delegate object.
If the row heights are not all the same, or can change dynamically, provide the heights using the tableView(:heightForRowAt:) method of your delegate object. When you implement this method, you must provide values for every row in your table. The following example code shows how to return a custom height for the first row of each section and use the default height for all other rows.
改变行高
UIKIt默认提供行的大小,也可通过给TV的rowHeight 属性赋值自定义行高,当table的行高都一样的时候必须用这个方法,比从代理对象那取值方便,如果行高不同或可以动态变化,那么就用代理对象的tableView(
:heightForRowAt:) 方法提供行高,当你实现这个方法的时候,你必须提供每一行的高度,底下示例代码

override func tableView(_ tableView: UITableView, 
           heightForRowAt indexPath: IndexPath) -> CGFloat {
   // Make the first row larger to accommodate a custom cell.
  if indexPath.row == 0 {
      return 80
   }


   // Use the default size for all other rows.
   return UITableView.automaticDimension
}

The table view asks for the heights of visible rows only. As the user scrolls, the table view asks you to provide the height for each row as it appears, including when it moves offscreen and then back onscreen.
TV只会要可见的行的行高,滑动时当滑出会滑入屏幕时会需要行高

Restore Your Cell’s Original Appearance Before Reuse

When a cell moves offscreen, the table view removes it from its view hierarchy and places it in an internally managed recycling queue. When you request a new cell using the table view’s dequeueReusableCell(withIdentifier:for:) method, the table view returns cells from the recycling queue first. If the queue is empty, the table view instantiates a new cell from your storyboard.
If you change the appearance of your custom cell’s views, implement the prepareForReuse() method of your cell subclass. In your implementation, return the appearance of your cell’s views to their original state. For example, if you change the alpha property of a view in your cell, return that property to its original value. You do not need to clear label text, set images to nil, or do anything that would be corrected by your tableView(_:cellForRowAt:) method when configuring the cell for display.
在重用之前重载单元的原始样式
当单元滑出屏幕时,TV会将它从视图层级中移除,并放在回收队列中。当你调用TV的dequeueReusableCell(withIdentifier:for:) 方法准备取一个新单元时,TV先从回收队列中取一个单元,如果队列为空,UIKit才会新实例化一个单元。
如果你尝试改变单元View的外观,实现一下单元子类的prepareForReuse() 方法,在此方法的实现中,将单元View的外观还原成最开始的样子

Add an Accessory View to Your Cell

An accessory view is an optional, system-defined view that appears at the trailing edge of a cell. You use accessory views to communicate standard cell behaviors to users. For example, you add a detail button to let the user know that tapping the row displays more information about the row.
To configure an accessory view:
In your storyboard, use the cell’s Accessory attribute to select the accessory view you want.
In your code, change the value of the cell’s accessoryType property.
Users expect accessory views to have specific behaviors when tapped. For information about how to implement those behaviors, see UITableViewCell.AccessoryType.

给单元添加辅助view
辅助view是可选的,系统定义的出现在单元的边缘的view,比如说一个detail按钮,点了之后显示出详细信息那种,改一下
单元的accessoryType 属性值就可以配置辅助view

To create a basic header or footer with a text label, override the tableView(:titleForHeaderInSection:) or tableView(:titleForFooterInSection:) method of your table’s data source object. The table view creates a standard header or footer for you and inserts it into the table at the specified location.
通过重写table的数据源对象的 tableView(:titleForHeaderInSection:) or tableView(:titleForFooterInSection:)方法来创建基础的带有lable的header和footer

// Create a standard header that includes the returned text.
override func tableView(_ tableView: UITableView, titleForHeaderInSection 
                            section: Int) -> String? {
   return "Header \(section)"
}


// Create a standard footer that includes the returned text.
override func tableView(_ tableView: UITableView, titleForFooterInSection 
                            section: Int) -> String? {
   return "Footer \(section)"
}

Note
Headers and footers normally apply to a single section, but you can also provide a single header or footer view for the entire table by using the tableHeaderView or tableFooterView properties of your table view. The global header appears at the top of your table’s content, and the global footer appears at the bottom.
header和footer一般用于单个section,如果想为整个TV添加的话用TV的 tableHeaderView or tableFooterView 属性。全局的 header和footer会显示在整个TV的上方和下方

Customize the Header and Footer Views

With custom headers and footers, you specify the views you want and position them anywhere within the allotted space. You can also provide different header or footer views for different sections of your table.
To create custom header or footer views:
Use UITableViewHeaderFooterView objects to define the appearance of your headers and footers.
Register your UITableViewHeaderFooterView objects with your table view.
Implement the tableView(:viewForHeaderInSection:) and tableView(:viewForFooterInSection:) methods in your table view delegate object to create and configure your views.
自定义header和footer有以下步骤
用UITableViewHeaderFooterView 对象定义header和footer的外观
给TV整一个UITableViewHeaderFooterView 对象
实现TV代理对象的tableView(:viewForHeaderInSection:) 和 tableView(:viewForFooterInSection:) 方法去创建和配置view
Always use a UITableViewHeaderFooterView object for your headers and footers. That view supports the same reuse model that cells employ, allowing you to recycle views instead of creating them every time. After you register your header or footer view, use the table view’s dequeueReusableHeaderFooterView(withIdentifier:) method to request instances of that view. If recycled header or footer views are available, the table view returns those first, before creating new ones.
You can use a UITableViewHeaderFooterView object as is and add views to its contentView property, or you can subclass and add your views. Position your subviews inside the content view by using a stack view or Auto Layout constraints. To change the background behind your content, modify the backgroundView property of the header-footer view. The following example code shows a custom header view that positions the image and label views at creation time

最好用 UITableViewHeaderFooterView对象处理header和footer,跟单元一样支持复用,当注册好header和footer后用VC的dequeueReusableHeaderFooterView(withIdentifier:) 方法做一份view的实例

Handling Row Selection in a Table View

配置行选择
行选择行为相关属性
allowsSelection
Determines whether users can select a row when the table isn’t in editing mode. The default is true.
allowsMultipleSelection
Determines whether users can select more than one row when the table isn’t in editing mode. The default is false.
allowsSelectionDuringEditing
Determines whether user can select a row while the table view is in editing mode. The default is false.
allowsMultipleSelectionDuringEditing
Determines whether users can select a more than one row while in editing mode. The default is false.

字面意思都能看懂我就不翻译了

When the user taps a row, the table view calls the delegate method tableView(:didSelectRowAt:). At this point, your app performs the action, such as displaying the details of the selected hiking trail:
响应行选择,当用户点击一行时,TV调用它代理方法tableView(
:didSelectRowAt:). 在此方法中执行一些操作

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let selectedTrail = trails[indexPath.row]
    
    if let viewController = storyboard?.instantiateViewController(identifier: "TrailViewController") as? TrailViewController {
        viewController.trail = selectedTrail
        navigationController?.pushViewController(viewController, animated: true)
    }
}

If you respond to the cell selection by pushing a new view controller onto the navigation stack, deselect the cell when the view controller pops off the stack. If you’re using a UITableViewController to display a table view, you get the behavior by setting the clearsSelectionOnViewWillAppear property to true. Otherwise, you can clear the selection in your view controller’s viewWillAppear( method:
如果你想在行选择时让新VC进入navigation栈中,当VC从navigation栈弹出时取消选择,如果你使用UITableViewController 显示TV的话,应该将clearsSelectionOnViewWillAppear设置为true,你也可以在VC的viewWillAppear(
:)方法中取消选择。

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    if let selectedIndexPath = tableView.indexPathForSelectedRow {
        tableView.deselectRow(at: selectedIndexPath, animated: animated)
    }
}

If the row displays the detail button accessory view and the user taps it, the table view calls the tableView(:accessoryButtonTappedForRowWith:) method instead of calling the tableView(:didSelectRowAt:) method.
如果行中有辅助view的话,点击时会调用tableView(:accessoryButtonTappedForRowWith:) 方法而非tableView(:didSelectRowAt:) 方法

The selection of a row may originate within the app itself rather than from a tap in the table view. For example, a user might add a new person to an address book, then return to the list of contacts. After returning to the contact list, the app scrolls the list to show the row of the newly added person. In this situation, use the table view method selectRow(at:animated:scrollPosition:) to select and scroll to the new row.
Note
Selecting a row programmatically doesn’t call the delegate methods tableView(:willSelectRowAt:) or tableView(:didSelectRowAt:), nor does it send selectionDidChangeNotification notifications to observers.
行选择可能更多的是由于app内部发生变化而非用户点击,比如用户添加了个新联系人,那么表里就会多出来一个,这种情况下用TV的selectRow(at:animated:scrollPosition:) 方法选择或滚动新行。
编程式的行选择不会调用代理方法tableView(:willSelectRowAt:) 或 tableView(:didSelectRowAt:),也不会给观察者发送selectionDidChangeNotification通知

Manage Selection Lists
You can also use cell selection to maintain inclusive and exclusive lists of selected items.
您还可以使用单元选择来维护所选项目的单选或多选列表表。
When providing a selection list in your app, don’t use the cell’s selected state to indicate the state of the item. Instead, display a checkmark or an accessory view. To indicate the state using a checkmark, implement the tableView(_:didSelectRowAt:) delegate method, then deselect the cell with deselectRow(at:animated:), followed by setting the cell’s accessoryType property to UITableViewCell.AccessoryType.checkmark.

在应用程序中提供选择列表时,不要使用单元格已被选定(就是变灰的那个)状态来指示项目的状态最好用辅助View,比如对勾或者其他什么的,实现 tableView(_:didSelectRowAt:) 委托方法,然后使用 deselectRow(at:animated:) 取消选择单元格,然后将单元格的 accessoryType 属性设置为 UITableViewCell.AccessoryType.checkmark。

示例代码:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    // Unselect the row, and instead, show the state with a checkmark.
    tableView.deselectRow(at: indexPath, animated: false)
    
    guard let cell = tableView.cellForRow(at: indexPath) else { return }
    
    // Update the selected item to indicate whether the user packed it or not.
    let item = packingList[indexPath.row]
    let newItem = PackingItem(name: item.name, isPacked: !item.isPacked)
    packingList.remove(at: indexPath.row)
    packingList.insert(newItem, at: indexPath.row)
    
    // Show a check mark next to packed items.
    if newItem.isPacked {
        cell.accessoryType = .checkmark
    } else {
        cell.accessoryType = .none
    }
}

Managing an exclusive list is similar. Deselect the row and display a checkmark or an accessory view to indicate the selected state. But unlike an inclusive list, limit the exclusive list to only one selected item at a time.

单选同理

示例代码

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    // Unselect the row.
    tableView.deselectRow(at: indexPath, animated: false)
    
    // Did the user tap on a selected filter item? If so, do nothing.
    let selectedFilterRow = selectedFilters[indexPath.section]
    if selectedFilterRow == indexPath.row {
        return
    }


    // Remove the checkmark from the previously selected filter item.
    if let previousCell = tableView.cellForRow(at: IndexPath(row: selectedFilterRow, section: indexPath.section)) {
        previousCell.accessoryType = .none
    }
    
    // Mark the newly selected filter item with a checkmark.
    if let cell = tableView.cellForRow(at: indexPath) {
        cell.accessoryType = .checkmark
    }
    
    // Remember this selected filter item.
    selectedFilters[indexPath.section] = indexPath.row
}

 类似资料: