在 UITableView 中使用自动布局进行动态单元格布局 &可变行高 [英] Using Auto Layout in UITableView for dynamic cell layouts & variable row heights

查看:24
本文介绍了在 UITableView 中使用自动布局进行动态单元格布局 &可变行高的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

如何在表格视图中的 UITableViewCell 中使用自动布局,让每个单元格的内容和子视图(自身/自动)确定行高,同时保持平滑的滚动性能?

解决方案

TL;DR: 不喜欢阅读?直接跳转到 GitHub 上的示例项目:

  • 在 iOS 8 中,Apple 已经内化了之前必须由您在 iOS 8 之前实现的大部分工作.为了让自定大小的单元格机制起作用,您必须首先设置 rowHeight<表视图上的/code> 属性为常量 UITableView.automaticDimension.然后,您只需要通过将 table view 的 estimatedRowHeight 属性设置为非零值来启用行高估计,例如:

    self.tableView.rowHeight = UITableView.automaticDimension;self.tableView.estimatedRowHeight = 44.0;//设置为您的平均值";单元格高度是

    这样做是为表格视图提供一个临时估计/占位符,用于尚未出现在屏幕上的单元格的行高.然后,当这些单元格即将在屏幕上滚动时,将计算实际的行高.为了确定每一行的实际高度,表格视图会根据内容视图的已知固定宽度(基于表格视图的宽度,减去任何其他内容,例如部分索引或附件视图)以及您添加到单元格内容视图和子视图的自动布局约束.一旦确定了该实际单元格高度,该行的旧估计高度将更新为新的实际高度(并且根据需要对表格视图的 contentSize/contentOffset 进行任何调整).

    一般来说,您提供的估计不必非常准确——它仅用于正确调整表格视图中滚动指示器的大小,并且表格视图可以很好地调整滚动指示器的不正确当您在屏幕上滚动单元格时估计.您应该将表视图上的 estimatedRowHeight 属性(在 viewDidLoad 或类似中)设置为常量值,即平均值"行高.仅当您的行高具有极大的可变性(例如,相差一个数量级)并且您注意到滚动指示器跳跃"时当您滚动时,您是否应该费心实施 tableView:estimatedHeightForRowAtIndexPath: 以执行返回每行更准确估计所需的最少计算.

    对于 iOS 7 支持(自己实现自动单元格大小调整)

    3.做一个Layout Pass &获取单元格高度

    首先,实例化一个表格视图单元的屏幕外实例,每个重用标识符一个实例,严格用于高度计算.(离屏意味着单元格引用存储在视图控制器上的属性/ivar 中,并且永远不会从 tableView:cellForRowAtIndexPath: 返回以便表格视图实际呈现在屏幕上.)接下来,单元格必须配置为如果要在表格视图中显示,它将包含的确切内容(例如文本、图像等).

    然后,强制单元格立即布局其子视图,然后使用UITableViewCellcontentView上的systemLayoutSizeFittingSize:方法找到出所需的单元格高度是多少.使用 UILayoutFittingCompressedSize 获得适合单元格所有内容所需的最小尺寸.然后可以从 tableView:heightForRowAtIndexPath: 委托方法返回高度.

    4.使用估计的行高

    如果你的 table view 有几十行,你会发现在第一次加载 table view 时,做 Auto Layout 约束求解会很快卡住主线程,如 tableView:heightForRowAtIndexPath: 在第一次加载时对每一行调用(为了计算滚动指示器的大小).

    从 iOS 7 开始,您可以(并且绝对应该)在 table view 上使用 estimatedRowHeight 属性.这样做是为表格视图提供一个临时估计/占位符,用于尚未出现在屏幕上的单元格的行高.然后,当这些单元格即将在屏幕上滚动时,将计算实际行高(通过调用tableView:heightForRowAtIndexPath:),并将估计高度更新为实际行高.

    一般来说,您提供的估计不必非常准确——它仅用于正确调整表格视图中滚动指示器的大小,并且表格视图可以很好地调整滚动指示器的不正确当您在屏幕上滚动单元格时估计.您应该将表视图上的 estimatedRowHeight 属性(在 viewDidLoad 或类似中)设置为常量值,即平均值"行高.仅当您的行高具有极大的可变性(例如,相差一个数量级)并且您注意到滚动指示器跳跃"时当您滚动时,您是否应该费心实施 tableView:estimatedHeightForRowAtIndexPath: 以执行返回每行更准确估计所需的最少计算.

    5.(如果需要)添加行高缓存

    如果您已完成上述所有操作,但仍然发现在 tableView:heightForRowAtIndexPath: 中进行约束求解时性能低得令人无法接受,那么您很遗憾需要为单元格高度实现一些缓存.(这是 Apple 工程师建议的方法.)总体思路是让 Autolayout 引擎第一次解决约束,然后缓存该单元格的计算高度,并将缓存的值用于该单元格高度的所有未来请求.当然,诀窍是确保在发生任何可能导致单元格高度发生变化的事情时清除单元格的缓存高度——主要是当该单元格的内容发生变化或其他重要事件发生时(例如用户调整动态类型文字大小滑块).

    iOS 7 通用示例代码(有很多有趣的评论)

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{//确定应在此单元格使用哪个重用标识符//索引路径,取决于所需的特定布局(您可能有//只有一个,也可能有多个).NSString *reuseIdentifier = ...;//为重用标识符出列一个单元格.//请注意,如果没有,此方法将初始化并返回一个新单元格//一个在重用池中可用,所以在这一行之后//编写代码,您将拥有一个准备好使用正确约束的单元格.UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier];//使用给定 indexPath 的内容配置单元格,例如://cell.textLabel.text = someTextForThisCell;//...//确保已经为这个单元设置了约束,因为它//可能刚刚从头开始创建.使用以下几行,//假设您正在从单元格的内部设置约束//更新约束方法:[cell setNeedsUpdateConstraints];[cell updateConstraintsIfNeeded];//如果您使用的是多行 UILabels,请不要忘记//preferredMaxLayoutWidth 需要正确设置.这样做//指出如果您不在 UITableViewCell 子类中执行此操作//-[layoutSubviews] 方法.例如://cell.multiLineLabel.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds);返回单元格;}- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{//确定应在此单元格使用哪个重用标识符//索引路径.NSString *reuseIdentifier = ...;//使用离屏单元格字典来获取一个单元格以供重用//标识符,创建一个单元格并将其存储在字典中(如果有)//尚未为重用标识符添加.警告:不要//在这里调用 table view 的 dequeueReusableCellWithIdentifier: 方法//因为这会在创建单元格时导致内存泄漏,但是//从未从 tableView:cellForRowAtIndexPath: 方法返回!UITableViewCell *cell = [self.offscreenCells objectForKey:reuseIdentifier];如果(!单元格){cell = [[YourTableViewCellClass alloc] init];[self.offscreenCells setObject:cell forKey:reuseIdentifier];}//使用给定 indexPath 的内容配置单元格,例如://cell.textLabel.text = someTextForThisCell;//...//确保已经为这个单元设置了约束,因为它//可能刚刚从头开始创建.使用以下几行,//假设您正在从单元格的内部设置约束//更新约束方法:[cell setNeedsUpdateConstraints];[cell updateConstraintsIfNeeded];//设置单元格的宽度以匹配表格视图的宽度.这个//很重要,这样我们才能获得不同的单元格高度//如果单元格的高度取决于其宽度(由于//多行 UILabels 自动换行等).我们不需要这样做//上面的 -[tableView:cellForRowAtIndexPath] 因为它发生了//在表格视图中使用单元格时自动执行.另请注意,//单元格的最终宽度可能不是表格视图中的宽度//某些情况,例如当一个部分索引显示时//表格视图的右侧.您必须考虑减少的//单元格宽度.cell.bounds = CGRectMake(0.0, 0.0, CGRectGetWidth(tableView.bounds), CGRectGetHeight(cell.bounds));//在单元格上执行布局传递,这将计算框架//基于约束的所有视图.(请注意,您必须设置//内多行 UILabels 上的 preferredMaxLayoutWidth//UITableViewCell子类的-[layoutSubviews]方法,还是做//在下面 2 行之前的这一点手动!)[cell setNeedsLayout];[单元格布局如果需要];//获取cell的contentView所需的实际高度CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;//在高度上添加一个额外的点以说明单元格分隔符,//它被添加到单元格的 contentView 的底部和//表格视图单元格的底部.高度 += 1.0;返回高度;}//注意:设置表视图的estimatedRowHeight 属性而不是//实现下面的方法,除非你有极大的可变性//你的行高,你注意到滚动指示器跳跃";//当你滚动时.- (CGFloat)tableView:(UITableView *)tableView EstimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath{//执行能够返回一个所需的最少计算//估计的行高在一个数量级内//实际高度.例如:if ([self isTallCellAtIndexPath:indexPath]) {返回 350.0;} 别的 {返回 40.0;}}

    示例项目

    由于表格视图单元格包含 UILabels 中的动态内容,因此这些项目是具有可变行高的表格视图的完整工作示例.

    Xamarin (C#/.NET)

    如果您使用 Xamarin,请查看 示例项目https://stackoverflow.com/users/5380/kent-boogaart">@KentBoogaart.

    How do you use Auto Layout within UITableViewCells in a table view to let each cell's content and subviews determine the row height (itself/automatically), while maintaining smooth scrolling performance?

    解决方案

    TL;DR: Don't like reading? Jump straight to the sample projects on GitHub:

    Conceptual Description

    The first 2 steps below are applicable regardless of which iOS versions you are developing for.

    1. Set Up & Add Constraints

    In your UITableViewCell subclass, add constraints so that the subviews of the cell have their edges pinned to the edges of the cell's contentView (most importantly to the top AND bottom edges). NOTE: don't pin subviews to the cell itself; only to the cell's contentView! Let the intrinsic content size of these subviews drive the height of the table view cell's content view by making sure the content compression resistance and content hugging constraints in the vertical dimension for each subview are not being overridden by higher-priority constraints you have added. (Huh? Click here.)

    Remember, the idea is to have the cell's subviews connected vertically to the cell's content view so that they can "exert pressure" and make the content view expand to fit them. Using an example cell with a few subviews, here is a visual illustration of what some (not all!) of your constraints would need to look like:

    You can imagine that as more text is added to the multi-line body label in the example cell above, it will need to grow vertically to fit the text, which will effectively force the cell to grow in height. (Of course, you need to get the constraints right in order for this to work correctly!)

    Getting your constraints right is definitely the hardest and most important part of getting dynamic cell heights working with Auto Layout. If you make a mistake here, it could prevent everything else from working -- so take your time! I recommend setting up your constraints in code because you know exactly which constraints are being added where, and it's a lot easier to debug when things go wrong. Adding constraints in code can be just as easy as and significantly more powerful than Interface Builder using layout anchors, or one of the fantastic open source APIs available on GitHub.

    • If you're adding constraints in code, you should do this once from within the updateConstraints method of your UITableViewCell subclass. Note that updateConstraints may be called more than once, so to avoid adding the same constraints more than once, make sure to wrap your constraint-adding code within updateConstraints in a check for a boolean property such as didSetupConstraints (which you set to YES after you run your constraint-adding code once). On the other hand, if you have code that updates existing constraints (such as adjusting the constant property on some constraints), place this in updateConstraints but outside of the check for didSetupConstraints so it can run every time the method is called.

    2. Determine Unique Table View Cell Reuse Identifiers

    For every unique set of constraints in the cell, use a unique cell reuse identifier. In other words, if your cells have more than one unique layout, each unique layout should receive its own reuse identifier. (A good hint that you need to use a new reuse identifier is when your cell variant has a different number of subviews, or the subviews are arranged in a distinct fashion.)

    For example, if you were displaying an email message in each cell, you might have 4 unique layouts: messages with just a subject, messages with a subject and a body, messages with a subject and a photo attachment, and messages with a subject, body, and photo attachment. Each layout has completely different constraints required to achieve it, so once the cell is initialized and the constraints are added for one of these cell types, the cell should get a unique reuse identifier specific to that cell type. This means when you dequeue a cell for reuse, the constraints have already been added and are ready to go for that cell type.

    Note that due to differences in intrinsic content size, cells with the same constraints (type) may still have varying heights! Don't confuse fundamentally different layouts (different constraints) with different calculated view frames (solved from identical constraints) due to different sizes of content.

    • Do not add cells with completely different sets of constraints to the same reuse pool (i.e. use the same reuse identifier) and then attempt to remove the old constraints and set up new constraints from scratch after each dequeue. The internal Auto Layout engine is not designed to handle large scale changes in constraints, and you will see massive performance issues.

    For iOS 8 - Self-Sizing Cells

    3. Enable Row Height Estimation

    To enable self-sizing table view cells, you must set the table view’s rowHeight property to UITableViewAutomaticDimension. You must also assign a value to the estimatedRowHeight property. As soon as both of these properties are set, the system uses Auto Layout to calculate the row’s actual height

    Apple: Working with Self-Sizing Table View Cells

    With iOS 8, Apple has internalized much of the work that previously had to be implemented by you prior to iOS 8. In order to allow the self-sizing cell mechanism to work, you must first set the rowHeight property on the table view to the constant UITableView.automaticDimension. Then, you simply need to enable row height estimation by setting the table view's estimatedRowHeight property to a nonzero value, for example:

    self.tableView.rowHeight = UITableView.automaticDimension;
    self.tableView.estimatedRowHeight = 44.0; // set to whatever your "average" cell height is
    

    What this does is provide the table view with a temporary estimate/placeholder for the row heights of cells that are not yet onscreen. Then, when these cells are about to scroll on screen, the actual row height will be calculated. To determine the actual height for each row, the table view automatically asks each cell what height its contentView needs to be based on the known fixed width of the content view (which is based on the table view's width, minus any additional things like a section index or accessory view) and the auto layout constraints you have added to the cell's content view and subviews. Once this actual cell height has been determined, the old estimated height for the row is updated with the new actual height (and any adjustments to the table view's contentSize/contentOffset are made as needed for you).

    Generally speaking, the estimate you provide doesn't have to be very accurate -- it is only used to correctly size the scroll indicator in the table view, and the table view does a good job of adjusting the scroll indicator for incorrect estimates as you scroll cells onscreen. You should set the estimatedRowHeight property on the table view (in viewDidLoad or similar) to a constant value that is the "average" row height. Only if your row heights have extreme variability (e.g. differ by an order of magnitude) and you notice the scroll indicator "jumping" as you scroll should you bother implementing tableView:estimatedHeightForRowAtIndexPath: to do the minimal calculation required to return a more accurate estimate for each row.

    For iOS 7 support (implementing auto cell sizing yourself)

    3. Do a Layout Pass & Get The Cell Height

    First, instantiate an offscreen instance of a table view cell, one instance for each reuse identifier, that is used strictly for height calculations. (Offscreen meaning the cell reference is stored in a property/ivar on the view controller and never returned from tableView:cellForRowAtIndexPath: for the table view to actually render onscreen.) Next, the cell must be configured with the exact content (e.g. text, images, etc) that it would hold if it were to be displayed in the table view.

    Then, force the cell to immediately layout its subviews, and then use the systemLayoutSizeFittingSize: method on the UITableViewCell's contentView to find out what the required height of the cell is. Use UILayoutFittingCompressedSize to get the smallest size required to fit all the contents of the cell. The height can then be returned from the tableView:heightForRowAtIndexPath: delegate method.

    4. Use Estimated Row Heights

    If your table view has more than a couple dozen rows in it, you will find that doing the Auto Layout constraint solving can quickly bog down the main thread when first loading the table view, as tableView:heightForRowAtIndexPath: is called on each and every row upon first load (in order to calculate the size of the scroll indicator).

    As of iOS 7, you can (and absolutely should) use the estimatedRowHeight property on the table view. What this does is provide the table view with a temporary estimate/placeholder for the row heights of cells that are not yet onscreen. Then, when these cells are about to scroll on screen, the actual row height will be calculated (by calling tableView:heightForRowAtIndexPath:), and the estimated height updated with the actual one.

    Generally speaking, the estimate you provide doesn't have to be very accurate -- it is only used to correctly size the scroll indicator in the table view, and the table view does a good job of adjusting the scroll indicator for incorrect estimates as you scroll cells onscreen. You should set the estimatedRowHeight property on the table view (in viewDidLoad or similar) to a constant value that is the "average" row height. Only if your row heights have extreme variability (e.g. differ by an order of magnitude) and you notice the scroll indicator "jumping" as you scroll should you bother implementing tableView:estimatedHeightForRowAtIndexPath: to do the minimal calculation required to return a more accurate estimate for each row.

    5. (If Needed) Add Row Height Caching

    If you've done all the above and are still finding that performance is unacceptably slow when doing the constraint solving in tableView:heightForRowAtIndexPath:, you'll unfortunately need to implement some caching for cell heights. (This is the approach suggested by Apple's engineers.) The general idea is to let the Autolayout engine solve the constraints the first time, then cache the calculated height for that cell and use the cached value for all future requests for that cell's height. The trick of course is to make sure you clear the cached height for a cell when anything happens that could cause the cell's height to change -- primarily, this would be when that cell's content changes or when other important events occur (like the user adjusting the Dynamic Type text size slider).

    iOS 7 Generic Sample Code (with lots of juicy comments)

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        // Determine which reuse identifier should be used for the cell at this 
        // index path, depending on the particular layout required (you may have
        // just one, or may have many).
        NSString *reuseIdentifier = ...;
    
        // Dequeue a cell for the reuse identifier.
        // Note that this method will init and return a new cell if there isn't
        // one available in the reuse pool, so either way after this line of 
        // code you will have a cell with the correct constraints ready to go.
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier];
             
        // Configure the cell with content for the given indexPath, for example:
        // cell.textLabel.text = someTextForThisCell;
        // ...
        
        // Make sure the constraints have been set up for this cell, since it 
        // may have just been created from scratch. Use the following lines, 
        // assuming you are setting up constraints from within the cell's 
        // updateConstraints method:
        [cell setNeedsUpdateConstraints];
        [cell updateConstraintsIfNeeded];
    
        // If you are using multi-line UILabels, don't forget that the 
        // preferredMaxLayoutWidth needs to be set correctly. Do it at this 
        // point if you are NOT doing it within the UITableViewCell subclass 
        // -[layoutSubviews] method. For example: 
        // cell.multiLineLabel.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds);
        
        return cell;
    }
    
    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        // Determine which reuse identifier should be used for the cell at this 
        // index path.
        NSString *reuseIdentifier = ...;
    
        // Use a dictionary of offscreen cells to get a cell for the reuse 
        // identifier, creating a cell and storing it in the dictionary if one 
        // hasn't already been added for the reuse identifier. WARNING: Don't 
        // call the table view's dequeueReusableCellWithIdentifier: method here 
        // because this will result in a memory leak as the cell is created but 
        // never returned from the tableView:cellForRowAtIndexPath: method!
        UITableViewCell *cell = [self.offscreenCells objectForKey:reuseIdentifier];
        if (!cell) {
            cell = [[YourTableViewCellClass alloc] init];
            [self.offscreenCells setObject:cell forKey:reuseIdentifier];
        }
        
        // Configure the cell with content for the given indexPath, for example:
        // cell.textLabel.text = someTextForThisCell;
        // ...
        
        // Make sure the constraints have been set up for this cell, since it 
        // may have just been created from scratch. Use the following lines, 
        // assuming you are setting up constraints from within the cell's 
        // updateConstraints method:
        [cell setNeedsUpdateConstraints];
        [cell updateConstraintsIfNeeded];
    
        // Set the width of the cell to match the width of the table view. This
        // is important so that we'll get the correct cell height for different
        // table view widths if the cell's height depends on its width (due to 
        // multi-line UILabels word wrapping, etc). We don't need to do this 
        // above in -[tableView:cellForRowAtIndexPath] because it happens 
        // automatically when the cell is used in the table view. Also note, 
        // the final width of the cell may not be the width of the table view in
        // some cases, for example when a section index is displayed along 
        // the right side of the table view. You must account for the reduced 
        // cell width.
        cell.bounds = CGRectMake(0.0, 0.0, CGRectGetWidth(tableView.bounds), CGRectGetHeight(cell.bounds));
    
        // Do the layout pass on the cell, which will calculate the frames for 
        // all the views based on the constraints. (Note that you must set the 
        // preferredMaxLayoutWidth on multiline UILabels inside the 
        // -[layoutSubviews] method of the UITableViewCell subclass, or do it 
        // manually at this point before the below 2 lines!)
        [cell setNeedsLayout];
        [cell layoutIfNeeded];
    
        // Get the actual height required for the cell's contentView
        CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
    
        // Add an extra point to the height to account for the cell separator, 
        // which is added between the bottom of the cell's contentView and the 
        // bottom of the table view cell.
        height += 1.0;
    
        return height;
    }
    
    // NOTE: Set the table view's estimatedRowHeight property instead of 
    // implementing the below method, UNLESS you have extreme variability in 
    // your row heights and you notice the scroll indicator "jumping" 
    // as you scroll.
    - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        // Do the minimal calculations required to be able to return an 
        // estimated row height that's within an order of magnitude of the 
        // actual height. For example:
        if ([self isTallCellAtIndexPath:indexPath]) {
            return 350.0;
        } else {
            return 40.0;
        }
    }
    

    Sample Projects

    These projects are fully working examples of table views with variable row heights due to table view cells containing dynamic content in UILabels.

    Xamarin (C#/.NET)

    If you're using Xamarin, check out this sample project put together by @KentBoogaart.

    这篇关于在 UITableView 中使用自动布局进行动态单元格布局 &amp;可变行高的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆