Swift - 多列表格组件的实现

时间:2021-11-04 12:27:56
(本文代码已升级至Swift3)
与桌面、Web应用不同,受限于屏幕尺寸,移动APP常常采用单列表格来显示列表数据。但有时我们需要使用多列表格来展示数据(比如:报表数据显示,或iPad这种大屏设备上展示多栏数据),这些通过网格(UICollectionView)的自定义布局功能就可以实现。
1,多列表格(multi-column table control)效果图Swift - 多列表格组件的实现
2,功能说明:(1)表格列头的标题文字加粗,内容区域的文字正常(2)表格边框为1像素黑色边框(3)第一列文字居左,其余列文字居中显示(居左的文字离左侧还是有5个像素距离)(4)每列单元格宽度不是平均分配的。而是从右往左,根据表头文字计算当前列的宽度。剩下的空间就都分配给第一列。(5)整个组件内部设置了 contentInset,给左右两侧各设置了10像素的距离。这样组件外部设置100%宽时,左右边框也不会顶到屏幕边缘。同时如果有滚动条的时候,滚动条也不会盖在表格内容区域上方。(6)点击单元格控制台会打印出对应的坐标位置。Swift - 多列表格组件的实现

3,关于collection view重新计算布局时机(1)shouldInvalidateLayout() 方法返回 true,表示当 collection view  bounds 改变时,就要重新计算布局。
(2)除了collection view 改变尺寸大小时 bounds 会改变, scroll view  bounds 在滚动时也会改变。
(3)本例中,collection view 在滚动的情况下没必要计算更新布局,否则拖动滚动条的时候布局会不断地丢弃重新计算,影响性能。 
(4)这里在 shouldInvalidateLayout() 中做判断,只有 collection view 宽度变化时才返回true重新计算布局,否则返回false
4,项目代码 --- UICollectionGridViewController.swift(组件类) ---
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 import Foundationimport UIKit //多列表格组件(通过CollectionView实现)class UICollectionGridViewController: UICollectionViewController {    //表头数据    var cols: [String]! = []    //行数据    var rows: [[Any]]! = []    //单元格内容居左时的左侧内边距    private var cellPaddingLeft:CGFloat = 5         init() {        //初始化表格布局        let layout = UICollectionGridViewLayout()        super.init(collectionViewLayout: layout)        layout.viewController = self        collectionView!.backgroundColor = UIColor.white        collectionView!.register(UICollectionViewCell.self,                                      forCellWithReuseIdentifier: "cell")        collectionView!.delegate = self        collectionView!.dataSource = self        collectionView!.isDirectionalLockEnabled = true        collectionView!.contentInset = UIEdgeInsetsMake(0, 10, 0, 10)        collectionView!.bounces = false    }         required init?(coder aDecoder: NSCoder) {        fatalError("UICollectionGridViewController.init(coder:) has not been implemented")    }         //设置列头数据    func setColumns(columns: [String]) {        cols = columns    }         //添加行数据    func addRow(row: [Any]) {        rows.append(row)        collectionView!.collectionViewLayout.invalidateLayout()        collectionView!.reloadData()    }         override func viewDidLoad() {        super.viewDidLoad()    }         override func viewDidLayoutSubviews() {        collectionView!.frame = CGRect(x:0, y:0,                                       width:view.frame.width, height:view.frame.height)    }         override func didReceiveMemoryWarning() {        super.didReceiveMemoryWarning()    }         //返回表格总行数    override func numberOfSections(in collectionView: UICollectionView) -> Int {            if cols.isEmpty {                return 0            }            //总行数是:记录数+1个表头            return rows.count + 1    }         //返回表格的列数    override func collectionView(_ collectionView: UICollectionView,                                 numberOfItemsInSection section: Int) -> Int {        return cols.count    }         //单元格内容创建    override func collectionView(_ collectionView: UICollectionView,                            cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell",                                                for: indexPath) as UICollectionViewCell        //单元格边框        cell.layer.borderWidth = 1        cell.backgroundColor = UIColor.white        cell.clipsToBounds = true                 //先清空内部原有的元素        for subview in cell.subviews {            subview.removeFromSuperview()        }                 //添加内容标签        let label = UILabel(frame: CGRect(x:0, y:0, width:cell.frame.width,                                          height:cell.frame.height))                 //第一列的内容左对齐,其它列内容居中        if indexPath.row != 0 {            label.textAlignment = .center        }else {            label.textAlignment = .left            label.frame.origin.x = cellPaddingLeft        }                 //设置列头单元格,内容单元格的数据        if indexPath.section == 0 {            let text = NSAttributedString(string: cols[indexPath.row], attributes: [                NSFontAttributeName:UIFont.boldSystemFont(ofSize: 15)                ])            label.attributedText = text        } else {            label.font = UIFont.systemFont(ofSize: 15)            label.text = "\(rows[indexPath.section-1][indexPath.row])"        }        cell.addSubview(label)                 return cell    }         //单元格选中事件    override func collectionView(_ collectionView: UICollectionView,                                 didSelectItemAt indexPath: IndexPath) {        //打印出点击单元格的[行,列]坐标        print("点击单元格的[行,列]坐标: [\(indexPath.section),\(indexPath.row)]")    }}

--- UICollectionGridViewLayout.swift(布局类) ---
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 import Foundationimport UIKit //多列表格组件布局类class UICollectionGridViewLayout: UICollectionViewLayout {    //记录每个单元格的布局属性    private var itemAttributes: [[UICollectionViewLayoutAttributes]] = []    private var itemsSize: [NSValue] = []    private var contentSize: CGSize = CGSize.zero    //表格组件视图控制器    var viewController: UICollectionGridViewController!         //准备所有view的layoutAttribute信息    override func prepare() {        if collectionView!.numberOfSections == 0 {            return        }                 var column = 0        var xOffset: CGFloat = 0        var yOffset: CGFloat = 0        var contentWidth: CGFloat = 0        var contentHeight: CGFloat = 0                 if itemAttributes.count > 0 {            return        }                 itemAttributes = []        itemsSize = []                 if itemsSize.count != viewController.cols.count {            calculateItemsSize()        }                 for section in 0 ..< (collectionView?.numberOfSections)! {            var sectionAttributes: [UICollectionViewLayoutAttributes] = []            for index in 0 ..< viewController.cols.count {                let itemSize = itemsSize[index].cgSizeValue                                 let indexPath = IndexPath(item: index, section: section)                let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)                //除第一列,其它列位置都左移一个像素,防止左右单元格间显示两条边框线                if index == 0{                    attributes.frame = CGRect(x:xOffset, y:yOffset, width:itemSize.width,                                              height:itemSize.height).integral                }else {                    attributes.frame = CGRect(x:xOffset-1, y:yOffset,                                              width:itemSize.width+1,                                              height:itemSize.height).integral                }                                 sectionAttributes.append(attributes)                                 xOffset = xOffset+itemSize.width                column += 1                                 if column == viewController.cols.count {                    if xOffset > contentWidth {                        contentWidth = xOffset                    }                                         column = 0                    xOffset = 0                    yOffset += itemSize.height                }            }            itemAttributes.append(sectionAttributes)        }                 let attributes = itemAttributes.last!.last! as UICollectionViewLayoutAttributes        contentHeight = attributes.frame.origin.y + attributes.frame.size.height        contentSize = CGSize(width:contentWidth, height:contentHeight)    }         //需要更新layout时调用    override func invalidateLayout() {        itemAttributes = []        itemsSize = []        contentSize = CGSize.zero        super.invalidateLayout()    }         // 返回内容区域总大小,不是可见区域    override var collectionViewContentSize: CGSize {        get {            return contentSize        }    }         // 这个方法返回每个单元格的位置和大小    override func layoutAttributesForItem(at indexPath: IndexPath)        -> UICollectionViewLayoutAttributes? {        return itemAttributes[indexPath.section][indexPath.row]    }         // 返回所有单元格位置属性    override func layoutAttributesForElements(in rect: CGRect)        -> [UICollectionViewLayoutAttributes]? {            var attributes: [UICollectionViewLayoutAttributes] = []            for section in itemAttributes {                attributes.append(contentsOf: section.filter(                    {(includeElement: UICollectionViewLayoutAttributes) -> Bool in                        return rect.intersects(includeElement.frame)                }))            }            return attributes    }         //当边界发生改变时,是否应该刷新布局。    //本例在宽度变化时,将重新计算需要的布局信息。    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {        let oldBounds = self.collectionView?.bounds        if oldBounds!.width != newBounds.width {            return true        }else {            return false        }    }         //计算所有单元格的尺寸(每一列各一个单元格)    func calculateItemsSize() {        var remainingWidth = collectionView!.frame.width -            collectionView!.contentInset.left - collectionView!.contentInset.right                 var index = viewController.cols.count-1        while index >= 0 {            let newItemSize = sizeForItemWithColumnIndex(columnIndex: index,                                                         remainingWidth: remainingWidth)            remainingWidth -= newItemSize.width            let newItemSizeValue = NSValue(cgSize: newItemSize)            //由于遍历列的时候是从尾部开始遍历了,因此将结果插入数组的时候都是放人第一个位置            itemsSize.insert(newItemSizeValue, at: 0)            index -= 1        }    }         //计算某一列的单元格尺寸    func sizeForItemWithColumnIndex(columnIndex: Int, remainingWidth: CGFloat) -> CGSize {        let columnString = viewController.cols[columnIndex]        //根据列头标题文件,估算各列的宽度        let size = NSString(string: columnString).size(attributes: [            NSFontAttributeName:UIFont.systemFont(ofSize: 15),            NSUnderlineStyleAttributeName:NSUnderlineStyle.styleSingle.rawValue            ])                 //如果有剩余的空间则都给第一列        if columnIndex == 0 {            return CGSize(width:max(remainingWidth, size.width + 17),                          height:size.height + 10)        }        //行高增加10像素,列宽增加17像素        return CGSize(width:size.width + 17, height:size.height + 10)    }}

--- ViewController.swift(测试类) ---
123456789101112131415161718192021222324252627282930 import UIKit class ViewController: UIViewController {         var gridViewController: UICollectionGridViewController!         override func viewDidLoad() {        super.viewDidLoad()                 gridViewController = UICollectionGridViewController()        gridViewController.setColumns(columns: ["客户", "消费金额", "消费次数", "满意度"])        gridViewController.addRow(row: ["hangge", "100", "8", "60%"])        gridViewController.addRow(row: ["张三", "223", "16", "81%"])        gridViewController.addRow(row: ["李四", "143", "25", "93%"])        gridViewController.addRow(row: ["王五", "75", "2", "53%"])        gridViewController.addRow(row: ["韩梅梅", "43", "12", "33%"])        gridViewController.addRow(row: ["李雷", "33", "27", "45%"])        gridViewController.addRow(row: ["王大力", "33", "22", "15%"])        view.addSubview(gridViewController.view)    }         override func viewDidLayoutSubviews() {        gridViewController.view.frame = CGRect(x:0, y:50, width:view.frame.width,                                               height:view.frame.height-60)    }         override func didReceiveMemoryWarning() {        super.didReceiveMemoryWarning()    }  }

5,源码下载: Swift - 多列表格组件的实现hangge_1081.zip