iOS UICollecionViewFlowLayout でカスタムレイアウトを作ろう ～ Swift版

※2017.07.31追記： 「MOREMALL（モアモール）」事業、サービス終了のお知らせ

初めまして、Objective-C未経験Swift歴6ヶ月のamitanです。

WWDC2015 にてSwift2.0オープンソース化が発表されて、テンションMAXです！！

UICollectionViewはiOS 6.0で追加された、 grid形式で表示可能なViewです。

今回は、MOREMALLアプリでも使用されているUICollecionViewFlowLayoutを継承したお手軽カスタマイズ方法をご紹介します。

UICollectionViewでのアニメーション方法についてはこちらからどうぞ。

カスタムレイアウトの作成

列数及び縦横の長さ、行列の位置を指定することで、UICollectionViewに描画ができるカスタムレイアウトを作成します。

ソースコードはgithubにあります。

開発環境

Xcode 6.3.2

iOS 8.3

Swift 1.2

UICollectionViewFlowLayoutサブクラスの作成

UICollecionViewFlowLayoutを継承したクラスを作成します。

以下メソッドをオーバーライドして実装します。

prepareLayout()

レイアウトの事前計算を行うメソッドです。

private static let kMaxRow = 3 var maxColumn = kMaxRow private var cellPattern:[(sideLength: CGFloat, heightLength:CGFloat, column:CGFloat, row:CGFloat)] = [] private var sectionCells = [[CGRect]]() private var contentSize = CGSizeZero override public func prepareLayout() { super.prepareLayout() sectionCells = [[CGRect]]() if let collectionView = self.collectionView { contentSize = CGSize(width: collectionView.bounds.width - collectionView.contentInset.left - collectionView.contentInset.right, height: 0) let smallCellSideLength: CGFloat = (contentSize.width - super.sectionInset.left - super.sectionInset.right - (super.minimumInteritemSpacing * (CGFloat(maxColumn) - 1.0))) / CGFloat(maxColumn) for section in (0..<collectionView.numberOfSections()) { var cells = [CGRect]() var numberOfCellsInSection = collectionView.numberOfItemsInSection(section); var height = contentSize.height for i in (0..<numberOfCellsInSection) { let position = i % (numberOfCellsInSection) let cellPosition = position % cellPattern.count let cell = cellPattern[cellPosition] let x = (cell.column * (smallCellSideLength + super.minimumInteritemSpacing)) + super.sectionInset.left let y = (cell.row * (smallCellSideLength + super.minimumLineSpacing)) + contentSize.height + super.sectionInset.top let cellwidth = (cell.sideLength * smallCellSideLength) + ((cell.sideLength-1) * super.minimumInteritemSpacing) let cellheight = (cell.heightLength * smallCellSideLength) + ((cell.heightLength-1) * super.minimumLineSpacing) let cellRect = CGRectMake(x, y, cellwidth, cellheight) cells.append(cellRect) if (height < cellRect.origin.y + cellRect.height) { height = cellRect.origin.y + cellRect.height } } contentSize = CGSize(width: contentSize.width, height: height) sectionCells.append(cells) } } } 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 private static let kMaxRow = 3 var maxColumn = kMaxRow private var cellPattern : [ ( sideLength : CGFloat , heightLength : CGFloat , column : CGFloat , row : CGFloat ) ] = [ ] private var sectionCells = [ [ CGRect ] ] ( ) private var contentSize = CGSizeZero override public func prepareLayout ( ) { super . prepareLayout ( ) sectionCells = [ [ CGRect ] ] ( ) if let collectionView = self . collectionView { contentSize = CGSize ( width : collectionView . bounds . width - collectionView . contentInset . left - collectionView . contentInset . right , height : 0 ) let smallCellSideLength : CGFloat = ( contentSize . width - super . sectionInset . left - super . sectionInset . right - ( super . minimumInteritemSpacing * ( CGFloat ( maxColumn ) - 1.0 ) ) ) / CGFloat ( maxColumn ) for section in ( 0 .. < collectionView . numberOfSections ( ) ) { var cells = [ CGRect ] ( ) var numberOfCellsInSection = collectionView . numberOfItemsInSection ( section ) ; var height = contentSize . height for i in ( 0 .. < numberOfCellsInSection ) { let position = i % ( numberOfCellsInSection ) let cellPosition = position % cellPattern . count let cell = cellPattern [ cellPosition ] let x = ( cell . column * ( smallCellSideLength + super . minimumInteritemSpacing ) ) + super . sectionInset . left let y = ( cell . row * ( smallCellSideLength + super . minimumLineSpacing ) ) + contentSize . height + super . sectionInset . top let cellwidth = ( cell . sideLength * smallCellSideLength ) + ( ( cell . sideLength - 1 ) * super . minimumInteritemSpacing ) let cellheight = ( cell . heightLength * smallCellSideLength ) + ( ( cell . heightLength - 1 ) * super . minimumLineSpacing ) let cellRect = CGRectMake ( x , y , cellwidth , cellheight ) cells . append ( cellRect ) if ( height < cellRect . origin . y + cellRect . height ) { height = cellRect . origin . y + cellRect . height } } contentSize = CGSize ( width : contentSize . width , height : height ) sectionCells . append ( cells ) } } }

今回は、画面幅から1辺の長さを算出し、それに基づきRect値を算出しています。

要素全てのRect値をsectionCellsに格納し、コンテンツサイズの計算も行っています。

collectionViewContentSize()

コンテンツサイズを返却します。

override public func collectionViewContentSize() -> CGSize { return contentSize } 1 2 3 override public func collectionViewContentSize ( ) -> CGSize { return contentSize }

正しいコンテンツサイズを返却しない場合、スクロールされなくなるので注意してください。

layoutAttributesForElementsInRect(rect: CGRect)

引数で与えられた範囲内に表示される要素のレイアウト情報UICollectionViewLayoutAttributesの配列を返却するメソッドです。

override public func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? { var layoutAttributes = [UICollectionViewLayoutAttributes]() if let collectionView = self.collectionView { for (var i = 0 ;i<collectionView.numberOfSections(); i++) { var sectionIndexPath = NSIndexPath(forItem: 0, inSection: i) var numberOfCellsInSection = collectionView.numberOfItemsInSection(i); for (var j = 0; j<numberOfCellsInSection; j++) { let indexPath = NSIndexPath(forRow:j, inSection:i) if let attributes = layoutAttributesForItemAtIndexPath(indexPath) { if (CGRectIntersectsRect(rect, attributes.frame)) { layoutAttributes.append(attributes) } } } } } return layoutAttributes } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 override public func layoutAttributesForElementsInRect ( rect : CGRect ) -> [ AnyObject ] ? { var layoutAttributes = [ UICollectionViewLayoutAttributes ] ( ) if let collectionView = self . collectionView { for ( var i = 0 ; i < collectionView . numberOfSections ( ) ; i ++ ) { var sectionIndexPath = NSIndexPath ( forItem : 0 , inSection : i ) var numberOfCellsInSection = collectionView . numberOfItemsInSection ( i ) ; for ( var j = 0 ; j < numberOfCellsInSection ; j ++ ) { let indexPath = NSIndexPath ( forRow : j , inSection : i ) if let attributes = layoutAttributesForItemAtIndexPath ( indexPath ) { if ( CGRectIntersectsRect ( rect , attributes . frame ) ) { layoutAttributes . append ( attributes ) } } } } } return layoutAttributes }

表示範囲に含まれている要素全てをCGRectIntersectsRectで計算し、返却しています。

layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath)

引数に与えられたNSIndexPath に対応するレイアウト情報UICollectionViewLayoutAttributesを返却するメソッドです。

override public func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! { var attributes = super.layoutAttributesForItemAtIndexPath(indexPath) attributes.frame = sectionCells[indexPath.section][indexPath.row] return attributes } 1 2 3 4 5 override public func layoutAttributesForItemAtIndexPath ( indexPath : NSIndexPath ) -> UICollectionViewLayoutAttributes ! { var attributes = super . layoutAttributesForItemAtIndexPath ( indexPath ) attributes . frame = sectionCells [ indexPath . section ] [ indexPath . row ] return attributes }

UICollectionViewLayoutAttributesのframeに対応する要素のRect値を格納し返却しています。

storyboardの設定

Main.storyboardにCollectionViewを設追加し、Custom ClassにUICollecionViewFlowLayoutを継承したクラスを設定しています。



UICollecionViewControlelrの実装

UICollectionViewControlelrを継承したクラスを作成します。

UICollectionViewControlelrの実装方法は今回は割愛いたします。

private let cellIdentifier = "cell" override func viewDidLoad() { super.viewDidLoad() if let layout = self.collectionView?.collectionViewLayout as? CustomCollectionViewFlowLayout { layout.sectionInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) layout.minimumLineSpacing = 8 layout.minimumInteritemSpacing = 8 layout.maxColumn = 3 layout.cellPattern.append(sideLength: 2,heightLength: 2,column: 0,row: 0) layout.cellPattern.append(sideLength: 1,heightLength: 1,column: 2,row: 0) layout.cellPattern.append(sideLength: 1,heightLength: 2,column: 2,row: 1) layout.cellPattern.append(sideLength: 1,heightLength: 2,column: 0,row: 2) layout.cellPattern.append(sideLength: 1,heightLength: 1,column: 1,row: 2) layout.cellPattern.append(sideLength: 2,heightLength: 1,column: 1,row: 3) } collectionView?.registerClass(UICollectionViewCell.classForCoder(), forCellWithReuseIdentifier: cellIdentifier) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private let cellIdentifier = "cell" override func viewDidLoad ( ) { super . viewDidLoad ( ) if let layout = self . collectionView ? . collectionViewLayout as ? CustomCollectionViewFlowLayout { layout . sectionInset = UIEdgeInsets ( top : 8 , left : 8 , bottom : 8 , right : 8 ) layout . minimumLineSpacing = 8 layout . minimumInteritemSpacing = 8 layout . maxColumn = 3 layout . cellPattern . append ( sideLength : 2 , heightLength : 2 , column : 0 , row : 0 ) layout . cellPattern . append ( sideLength : 1 , heightLength : 1 , column : 2 , row : 0 ) layout . cellPattern . append ( sideLength : 1 , heightLength : 2 , column : 2 , row : 1 ) layout . cellPattern . append ( sideLength : 1 , heightLength : 2 , column : 0 , row : 2 ) layout . cellPattern . append ( sideLength : 1 , heightLength : 1 , column : 1 , row : 2 ) layout . cellPattern . append ( sideLength : 2 , heightLength : 1 , column : 1 , row : 3 ) } collectionView ? . registerClass ( UICollectionViewCell . classForCoder ( ) , forCellWithReuseIdentifier : cellIdentifier ) }

viewDidLoadメソッドで作成したCustomCollectionViewFlowLayoutに対して、列数及び縦横の長さ、行列の位置を設定しています。

これで実装完了です。

サンプル実行結果

まとめ

表示位置の計算をするだけで、比較的簡単にカスタムレイアウトが作成できました。

セクションごとにレイアウトを変えたり、UICollectionViewLayoutAttributes自体をカスタマイズしたり色々なレイアウトが楽しめそうですね。

良いSwiftライフを～

参考サイト

Apple UICollectionViewFlowLayout Class Reference

Apple Collection View Programming Guide for iOS/Creating Custom Layouts

Apple Collection View Programming Guide for iOS/Using the Flow Layout