はじめに
こんにちは。Yossyです。
最近、CollectionViewを利用してアイコンが格子状に並んでいるViewを作成しています。
ただ、2行と3行目の間だけは他の行よりも間隔を大きくして、ボタンを設置します。
そこで、カスタムレイアウト(手作りのレイアウト)を作成してみたので、備忘録を兼ねて作り方を紹介してみたいと思います。
作成するレイアウト
レイアウト
4×3の格子状。2行目と3行目の間にボタン設置用の間隔を空ける。
手順
UICollectionViewFlowLayout
を継承したクラスの中で下記を4つを実装します。
詳細はリンク先を参照して下さい。
prepare( )、layoutAttributesForItem( at:)、layoutAttributesForElements(in:)、collectionViewContentSize
大枠の流れとしては、
①prepare( )
内で12個分のセルの位置(CGRect)を定義する。
②それを基にlayoutAttributesForItem(at: )
で各セルのUICollectionViewLayoutAttributes
を生成する。
③layoutAttributesForElements(in: )
でレイアウト作成に必要な[UICollectionViewLayoutAttributes]
を返す。
④CollectionViewContentSize
に値を入力する。
prepare( )
レイアウトの初回作成時と、レイアウト変更時に(invalidateLayout( )が呼ばれる度に)呼ばれます。
ここで、12個分のセルの位置情報(CGRect)を定義します。
具体的な実装としては、セルの位置情報の配列を作って、以後の処理に使用しています。
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 | overridefuncprepare(){ super.prepare() ifletcollectionView=self.collectionView{ contentSize=CGSize(width:collectionView.bounds.width-collectionView.contentInset.left-collectionView.contentInset.right,height:0) // セルの幅 letcellWidth:CGFloat=(contentSize.width-super.sectionInset.left-super.sectionInset.right-(super.minimumInteritemSpacing*(CGFloat(numOfCellsinLine)-1.0)))/CGFloat(numOfCellsinLine) // セルの高さ letcellHeight:CGFloat=84 // セルの位置情報用のプロパティ varx:CGFloat=0 vary:CGFloat=0 letnumberOfCellsInSection=collectionView.numberOfItems(inSection:0) // セル毎の位置情報の計算 foriin(0..<numberOfCellsInSection){ if(i+1)%4==1{ x=super.sectionInset.left }else{ x+=cellWidth+self.minimumInteritemSpacing } letrow=floor(Double(i)/4.0) if row<2{ y=(cellHeight+super.sectionInset.top)*CGFloat(row) }else{ y=(cellHeight+super.sectionInset.top)*CGFloat(row)+56 } letcellRect=CGRect(x:x,y:y,width:cellWidth,height:cellHeight) cellRects.append(cellRect) } } } |
layoutAttributesForItem(at indexPath: IndexPath)
各indexPathに対してのUICollectionViewLayoutAttributesを返却します。
UICollectionViewLayoutAttributesは、セルサイズや位置情報のプロパティを持っています。
具体的な実装としては、prepare( )で作成した配列から、セルサイズと位置情報を含んだUICollectionViewLayoutAttributesを生成しています。
1 2 3 4 5 | overridefunclayoutAttributesForItem(at indexPath:IndexPath)->UICollectionViewLayoutAttributes?{ letattributes=super.layoutAttributesForItem(at:indexPath)?.copy()as!UICollectionViewLayoutAttributes attributes.frame=cellRects[indexPath.row] returnattributes } |
layoutAttributesForElements(in rect: CGRect)
引数のrectに含まれる全てのセルやviewのUICollectionViewLayoutAttributesの配列を返却します。
ここで返却されるUICollectionViewLayoutAttributesに従い、各セルのレイアウトが作成されます。
具体的な実装としては、rect.intersects
で前述のlayoutAttributesForItem(at: )で取得したrect範囲内のUICollectionViewLayoutAttributesの配列を返しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | overridefunclayoutAttributesForElements(inrect:CGRect)->[UICollectionViewLayoutAttributes]?{ varlayoutAttributes:[UICollectionViewLayoutAttributes]=[] letnumberOfCellsInSection=collectionView.numberOfItems(inSection:0) foriin0..<numberOfCellsInSection{ letindexPath=IndexPath(row:i,section:0) ifletattributes=layoutAttributesForItem(at:indexPath){ if(rect.intersects(attributes.frame)){ layoutAttributes.append(attributes) } } } returnlayoutAttributes } |
CollectionViewContentSize
レイアウト作成時に、カスタムレイアウトのスクロール量を判断するのに使用されます。
カスタムレイアウト全体の大きさを返却してあげる必要があります。
デフォルトではCGSizeZeroとなっているらしいので、オーバーライトで値を設定する必要があります。
1 2 3 4 | override varcollectionViewContentSize:CGSize{ // 作成するレイアウトの幅と高さ returnCGSize(width:self.collectionView.frame.width,height:self.collectionView.frame.height) } |
まとめ
コード全体としては以下の通りとなります。
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 | classCustomCollectionViewFlowLayout:UICollectionViewFlowLayout{ // 1行のセル数 varnumOfCellsinLine=4 privatevarcellRects:[CGRect]=[] privatevarcontentSize=CGSize.zero required init?(coder aDecoder:NSCoder){ super.init(coder:aDecoder) } override init(){ super.init() self.sectionInset=UIEdgeInsets(top:10,left:10,bottom:10,right:10) self.minimumLineSpacing=10 self.minimumInteritemSpacing=10 } override varcollectionViewContentSize:CGSize{ // 作成するレイアウトの幅と高さ returnCGSize(width:self.collectionView!.frame.width,height:self.collectionView!.frame.height) } override func prepare(){ super.prepare() iflet collectionView=self.collectionView{ contentSize=CGSize(width:collectionView.bounds.width-collectionView.contentInset.left-collectionView.contentInset.right,height:0) // セルの幅 let cellWidth:CGFloat=(contentSize.width-super.sectionInset.left-super.sectionInset.right-(super.minimumInteritemSpacing *(CGFloat(numOfCellsinLine)-1.0)))/CGFloat(numOfCellsinLine) // セルの高さ let cellHeight:CGFloat=84 // セルの位置情報用のプロパティ varx:CGFloat=0 vary:CGFloat=0 let numberOfCellsInSection=collectionView.numberOfItems(inSection:0) // セル毎の位置情報の計算 foriin(0..<numberOfCellsInSection){ if(i+1)%4==1{ x=super.sectionInset.left }else{ x+=cellWidth+self.minimumInteritemSpacing } let row=floor(Double(i)/4.0) if row<2{ y=(cellHeight+super.sectionInset.top)*CGFloat(row) }else{ y=(cellHeight+super.sectionInset.top)*CGFloat(row)+56 } let cellRect=CGRect(x:x,y:y,width:cellWidth,height:cellHeight) cellRects.append(cellRect) } } } override func layoutAttributesForItem(at indexPath:IndexPath)->UICollectionViewLayoutAttributes?{ let attributes=super.layoutAttributesForItem(at:indexPath)?.copy()as!UICollectionViewLayoutAttributes attributes.frame=cellRects[indexPath.row] returnattributes } override func layoutAttributesForElements(inrect:CGRect)->[UICollectionViewLayoutAttributes]?{ varlayoutAttributes:[UICollectionViewLayoutAttributes]=[] let numberOfCellsInSection=collectionView!.numberOfItems(inSection:0) foriin0..<numberOfCellsInSection{ let indexPath=IndexPath(row:i,section:0) iflet attributes=layoutAttributesForItem(at:indexPath){ if(rect.intersects(attributes.frame)){ layoutAttributes.append(attributes) } } } returnlayoutAttributes } } |
最後に、CollectionViewのインスタンスを生成する際の引数に指定します。
1 2 | letcustomLayout=CustomCollectionViewFlowLayout() collectionView=UICollectionView(frame:CGRect,collectionViewLayout:customLayout) |
さいごに
カスタムレイアウトを上手に使えるようになれば、CollectionViewを使用しての実装の幅が大きく広がると感じました。
今回のカスタムレイアウトはシンプルなものでしたが、機会があればもう少し複雑なものにも挑戦してみたいです。