美文网首页
UITableViewCell 高度自动计算

UITableViewCell 高度自动计算

作者: 半心_忬 | 来源:发表于2019-11-29 16:59 被阅读0次

    前言:iOS开发中,UITableview的使用场景是非常广泛的,随着业务逐渐复杂,对于cell的高度的处理也是越来越麻烦的,有的时候甚至会非常痛苦。

    一、常规解决方案

    当前主流的做法是在model中计算一次然后缓存在model中,以提升体验和性能,至于计算的时机,可以在model解析完成后计算,也可以在heightForRowAt indexPath调用时再计算,都可以,是可以解决问题,不过存在弊端:

    • 做这些高度的计算非常的麻烦,麻烦随复杂度提升,会出现很多的if-else

    • 高度计算并不精确

      a. 计算label高度 不精确
      b. 计算attributeString高度 不精确
      c. ...

    • 为了计算高度,做了许多额外的事情,额外的内存开销(ps:计算过的人都懂的

    • 如果是在model解析完成后计算,那首次加载和下一页时,会出现卡顿

    • 如果在heightForRowAt indexPath调用时计算,cell首次被渲染会出现卡顿

    二、Cell 高度自动计算

    于是需要一个小工具,可以自动计算cell的高度,来规避这些麻烦。

    cell高度计算考虑的点:

    • 1.计算原理:使用数据设置完cell后,强制布局self.layoutIfNeeded(),然后获取高度

    • 2.计算的方式:

      a. 最初的思路是,直接拿整个cell的话,遍历所有的子视图,循环累加
      b. 后来觉得没有必要做一次循环,使用者传入一个用来计算指定的位于底部的视图,用这视图的y值加上height得到的就是cell的高度了,也方便一些复杂cell中各种隐藏和显示的使用,也略微提升性能

    • 3.保证性能:每个cell必须只计算一次,换句话来说,需要有缓存的功能

    • 4.场景覆盖:

      a. 有些cell不一样的状态,需要显示不一样的内容,此时的高度很有可能不一样,需要支持
      b. 一些带操作事件的按钮,执行完一些操作后,cell的元素可能出现增减,此时很可能需要重新计算高度,需要支持

    • 5.其他:cell之间一般都是有间距的,以卡片风格为例,其实真正需要关注的只是卡片的高度,而实际高度是需要加上间距的,需要支持一下这种类型,算出实际的高度之后加上一个偏移值,其他场景可能也需要,默认值为0

    三、源码

    为UITableview扩展两个属性,用于实现缓存功能:

    • cacheHeightDictionary:缓存cell行高的DictionarykeymodelJSONString或指定的其他唯一标识,value为自动计算好的行高
    • cacheCellDictionary:缓存用来获取或计算行高的cell,保证性能(理论上只需要一个cell来计算行高即可,降低消耗)
    /*
     TableViewCell 使用 SnapKit 布局 自动计算行高并缓存
    */
    public extension UITableView {
        /// 缓存 cell 行高 的 DIC(key为model的JSONString或指定的其他唯一标识,value为自动计算好的行高)
        var cacheHeightDictionary: NSMutableDictionary? {
            get {
                let dict = objc_getAssociatedObject(self, &kUITableViewSnapKitCellCacheHeightDictKey) as? NSMutableDictionary
                if let cache = dict {
                    return cache
                }
                let newDict = NSMutableDictionary()
                objc_setAssociatedObject(self, &kUITableViewSnapKitCellCacheHeightDictKey, newDict, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
                return newDict
            }
        }
        
        /// 缓存用来获取或计算行高的cell,保证性能(理论上只需要一个cell来计算行高即可,降低消耗)
        var cacheCellDictionary: NSMutableDictionary? {
            get {
                let dict = objc_getAssociatedObject(self, &kUITableViewSnapKitCellCacheHeightReuseCellsKey) as? NSMutableDictionary
                if let cache = dict {
                    return cache
                }
                let newDict = NSMutableDictionary()
                objc_setAssociatedObject(self, &kUITableViewSnapKitCellCacheHeightReuseCellsKey, newDict, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
                return newDict
            }
        }
    }
    

    UITableViewCell扩展两个属性,用于实现高度的计算:

    • hpc_lastViewInCell:所指定的距离cell底部较近的参考视图,必须指定,若不指定则会assert失败
    • hpc_bottomOffsetFromLastViewInCell:可选设置的属性,表示cell的高度需要从指定的lastViewInCell需要偏移多少,默认为0,小于0也为0
    /**
     TableViewCell 使用 Masonry布局 自动计算 cell 行高 category
     
     -- UI布局必须放在UITableViewCell的初始化方法中:- initWithStyle:reuseIdentifier:
     */
    public extension UITableViewCell {
        
        // 可选设置的属性,表示cell的高度需要从指定的lastViewInCell需要偏移多少,默认为0,小于0也为0
        @objc var hpc_bottomOffsetFromLastViewInCell: CGFloat {
            
            get {
                if let number = objc_getAssociatedObject(self, &kBottomOffsetFromLastViewInCellKey) as? NSNumber {
                    return CGFloat(number.floatValue)
                }
                return 0.0
            }
            
            set {
                objc_setAssociatedObject(self, &kBottomOffsetFromLastViewInCellKey, NSNumber(value: Float(newValue)), .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
        
        /// 所指定的距离cell底部较近的参考视图,必须指定,若不指定则会assert失败
        var hpc_lastViewInCell: UIView? {
            
            get {
                let lastView = objc_getAssociatedObject(self, &kLastViewInCellKey)
                return lastView as? UIView
            }
            
            set {
                objc_setAssociatedObject(self, &kLastViewInCellKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }
    

    计算逻辑:

    extension UITableViewCell {
        /// 带缓存功能,自动计算行高
        ///
        /// - Parameters:
        ///   - tableView: 目标tableview
        ///   - config:    计算行高配置回调
        ///   - cache:     缓存参数(key,唯一指定key【可以是model的id,或者model的JSONString】,stateKey,行高状态【可选】,shouldUpdate,【可选,默认false,是否要更新指定stateKey中缓存高度,若为true, 不管有没有缓存,都会重新计算)
        /// - Returns: 行高
        public static func cellHeight(forTableView tableView: UITableView,
                                         config: ((_ cell: UITableViewCell) -> Void)?,
                                         updateCacheIfNeeded cache: (() -> (key: String, stateKey: String?, shouldUpdate: Bool?))?) -> CGFloat {
            //  有缓存则从缓存中取
            if let cacheBlock = cache {
                let keyGroup     = cacheBlock()
                let key          = keyGroup.key
                let stateKey     = keyGroup.stateKey ?? kSnapKitCellCacheStateDefaultKey
                let shouldUpdate = keyGroup.shouldUpdate ?? false
                if shouldUpdate == false {
                    if let cacheDict = tableView.cacheHeightDictionary,
                        let stateDict = cacheDict[key] as? NSMutableDictionary, // 状态高度缓存
                        let height = stateDict[stateKey] as? NSNumber {
                        if height.intValue != 0 {
                            return CGFloat(height.floatValue)
                        }
                    }
                }
            }
            
            let className = self.description()
            var cell = tableView.cacheCellDictionary?.object(forKey: className) as? UITableViewCell
            if cell == nil {
                if Thread.isMainThread {
                    cell = self.init(style: .default, reuseIdentifier: nil)
                } else {
                    // 这里在第一次计算时,可能会卡住主线程,需要优化,考虑使用信号量 @山竹
                    DispatchQueue.main.sync {
                        cell = self.init(style: .default, reuseIdentifier: nil)
                    }
                }
                tableView.cacheCellDictionary?.setObject(cell!, forKey: className as NSCopying)
            }
            
            if let block = config { block(cell!) }
            //  添加子线程处理
            var height: CGFloat = 0
            if Thread.isMainThread {
                height = cell!.calculateCellHeight(forTableView: tableView, updateCacheIfNeeded: cache)
            } else {
                DispatchQueue.main.sync {
                    height = cell!.calculateCellHeight(forTableView: tableView, updateCacheIfNeeded: cache)
                }
            }
            return height
        }
        
        /// 获取缓存高度并缓存
        ///
        /// - Parameters:
        ///   - tableView: 目标tableview
        ///   - cache:     缓存参数
        /// - Returns: 高度
        private func calculateCellHeight(forTableView tableView: UITableView,
                                             updateCacheIfNeeded cache: (() -> (key: String, stateKey: String?, shouldUpdate: Bool?))?) -> CGFloat {
            
            assert(self.hpc_lastViewInCell != nil, "hpc_lastViewInCell property can't be nil")
            self.layoutIfNeeded()
            var height = self.hpc_lastViewInCell!.frame.origin.y + self.hpc_lastViewInCell!.frame.size.height
            height += self.hpc_bottomOffsetFromLastViewInCell
            if let cacheBlock = cache {
                let keyGroup = cacheBlock()
                let key      = keyGroup.key
                let stateKey = keyGroup.stateKey ?? kSnapKitCellCacheStateDefaultKey
                if let cacheDict = tableView.cacheHeightDictionary {
                    // 状态高度缓存
                    let stateDict = cacheDict[key] as? NSMutableDictionary
                    if stateDict != nil {
                        stateDict?[stateKey] = NSNumber(value: Float(height))
                    } else {
                        cacheDict[key] = NSMutableDictionary(object: NSNumber(value: Float(height)), forKey: stateKey as NSCopying)
                    }
                }
            }
            return height
        }
    }
    

    核心代码只有三处,其他都是一些判断,是否有缓存,有的话从缓存中取,没有的话,计算高度,然后缓存起来:

      1. 初次调用,会new一个cell出来,用来计算高度,然后缓存起来,用于下次调用
    let className = self.description()
    var cell = tableView.cacheCellDictionary?.object(forKey: className) as? UITableViewCell
    if cell == nil {
        if Thread.isMainThread {
            cell = self.init(style: .default, reuseIdentifier: nil)
        } else {
            // 这里在第一次计算时,可能会卡住主线程,需要优化,考虑使用信号量 @山竹
            DispatchQueue.main.sync {
                cell = self.init(style: .default, reuseIdentifier: nil)
            }
        }
        tableView.cacheCellDictionary?.setObject(cell!, forKey: className as NSCopying)
    }
    
      1. cell实例通过回调给调用者用于渲染cell
    回调出去:
    if let block = config { block(cell!) }
    
    此处为使用使用的地方,config回调,得到的就是用于计算高度的cell实例:
    
    OrderListCell.cellHeight(forTableView: tableView, config: { (targetCell) in
        if let cell = targetCell as? OrderListCell {
            cell.configCell(itemModel, self.viewModel.orderType, self.viewModel.orderState, indexPath.section)
        }
    }, updateCacheIfNeeded: { () -> (key: String, stateKey: String?, shouldUpdate: Bool?) in
        return (itemModel.cellHeightCacheKey.0 + "+\(indexPath.section)+\(indexPath.row)", nil, itemModel.cellHeightCacheKey.1)
    })
    
      1. 计算高度,通过强制布局刷新,用指定用来计算的子视图的y加上功能height,然后加上用户定义的偏移值,就是cell需要的实际高度
    self.layoutIfNeeded()
    var height = self.hpc_lastViewInCell!.frame.origin.y + self.hpc_lastViewInCell!.frame.size.height
    height += self.hpc_bottomOffsetFromLastViewInCell
    

    四、使用

      1. cellui初始化时,指定hpc_lastViewInCell
    // 自动高度计算
    hpc_lastViewInCell = countLabel
    hpc_bottomOffsetFromLastViewInCell = 6
    
      1. 在渲染cell的地方,如果有需要,需要修改hpc_lastViewInCell的值,有些复杂cell中各种组合非常多,底部子视图不确定是哪一个,是需要修改hpc_lastViewInCell的,有些底部子视图是固定的cell则不用
    // 以下为自动高度计算代码
    var lastView: UIView = countLabel
    var offset: CGFloat = 6
    
    if !(model.expectDeliveryTime ?? "").isEmpty {
        if model.haveMaterial {
            lastView = checkMaterialViewWithDelivery
            offset = 8
        } else {
            lastView = expectDeliveryLabel
        }
    } else {
        lastView = checkMaterialView
        offset = 8
    }
    
    hpc_lastViewInCell = lastView
    hpc_bottomOffsetFromLastViewInCell = offset
    
      1. heightForRowAt indexPath方法中,调用自动高度计算,有些特殊需求,有一个最小高度,小于最小高度则使用最小高度,反之则使用计算的高度,没有这种需求的,直接返回计算的高度就好
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        if viewModel.showItemArray?.count ?? 0 > indexPath.section, viewModel.showItemArray?[indexPath.section].tradeOrderTOList?.count ?? 0 > indexPath.row,
            let itemModel = viewModel.showItemArray?[indexPath.section].tradeOrderTOList?[indexPath.row] {
            // 自动高度计算
            let calculateHeight = OrderListCell.cellHeight(forTableView: tableView, config: { (targetCell) in
                if let cell = targetCell as? OrderListCell {
                    cell.configCell(itemModel, self.viewModel.orderType, self.viewModel.orderState, indexPath.section)
                }
            }, updateCacheIfNeeded: { () -> (key: String, stateKey: String?, shouldUpdate: Bool?) in
                return (itemModel.cellHeightCacheKey.0 + "+\(indexPath.section)+\(indexPath.row)", nil, itemModel.cellHeightCacheKey.1)
            })
        
            return calculateHeight > itemModel.cellHeight ? calculateHeight : itemModel.cellHeight
        }
        return 0
    }
    

    五、性能

    以下是第一页20条数据计算所花费的时间,第一条数据由于要new一个cell实例出来,耗时略长,其他的都是很少的,对性能无任何影响:

    calculate cell height use time: 9.206771850585938 ms
    calculate cell height use time: 0.8568763732910156 ms
    calculate cell height use time: 0.7119178771972656 ms
    calculate cell height use time: 0.6990432739257812 ms
    calculate cell height use time: 0.6499290466308594 ms
    calculate cell height use time: 1.8820762634277344 ms
    calculate cell height use time: 0.7300376892089844 ms
    calculate cell height use time: 0.9169578552246094 ms
    calculate cell height use time: 0.7901191711425781 ms
    calculate cell height use time: 0.14495849609375 ms
    calculate cell height use time: 0.4162788391113281 ms
    calculate cell height use time: 0.4220008850097656 ms
    calculate cell height use time: 0.17404556274414062 ms
    calculate cell height use time: 0.15878677368164062 ms
    calculate cell height use time: 0.6649494171142578 ms
    calculate cell height use time: 0.14901161193847656 ms
    calculate cell height use time: 0.12803077697753906 ms
    calculate cell height use time: 0.12111663818359375 ms
    calculate cell height use time: 0.7219314575195312 ms
    calculate cell height use time: 1.0538101196289062 ms
    calculate cell height use time: 0.0209808349609375 ms
    calculate cell height use time: 0.0171661376953125 ms
    calculate cell height use time: 0.01811981201171875 ms
    

    相关文章

      网友评论

          本文标题:UITableViewCell 高度自动计算

          本文链接:https://www.haomeiwen.com/subject/fspgwctx.html