Swift - 多列表格组件的实现

(本文代码已升级至Swift3)

与桌面、Web应用不同,受限于屏幕尺寸,移动APP常常采用单列表格来显示列表数据。但有时我们需要使用多列表格来展示数据(比如:报表数据显示,或iPad这种大屏设备上展示多栏数据),这些通过网格( UICollectionView)的自定义布局功能就可以实现。

1,多列表格(multi-column table control)效果图
原文:Swift - 多列表格组件的实现(样例1:基本功能的实现)

2,功能说明:
(1)表格列头的标题文字加粗,内容区域的文字正常
(2)表格边框为1像素黑色边框
(3)第一列文字居左,其余列文字居中显示(居左的文字离左侧还是有5个像素距离)
(4)每列单元格宽度不是平均分配的。而是从右往左,根据表头文字计算当前列的宽度。剩下的空间就都分配给第一列。
(5)整个组件内部设置了   contentInset,给左右两侧各设置了10像素的距离。这样组件外部设置100%宽时,左右边框也不会顶到屏幕边缘。同时如果有滚动条的时候,滚动条也不会盖在表格内容区域上方。
(6)点击单元格控制台会打印出对应的坐标位置。
原文:Swift - 多列表格组件的实现(样例1:基本功能的实现)


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(组件类) ---
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
import   Foundation
import   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(布局类) ---
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import   Foundation
import   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(测试类) ---
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
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,源码下载: hangge_1081.zip
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值