[iOS] UICollectionView Custom FlowLayout

최근에 업무중 개선포인트를 정리하고자 합니다.

우선 iPhone, iPad 두가지 화면에서 SubMenu 부분의 Page처리가 된 icon부분을 하나의 View로 사용하여, 진입 및 그리는 부분의 통일화를 시키고자 합니다.

기존에 iPhone, iPad 두가지의 Xib가 따로 존재하였고 Xib를 합치는 과정에서 최대한 작업을 줄이고자 생각을 하였다. (이미지 참조)

iPad 화면

iPhone 화면



우선 기존의 작업은 All CodeType에
- ScrollView + UIButton 좌표식  으로 되어있었다.

iOS 7~8 부터 진행된 프로젝트이며, 급하게 진행되고 있었기 때문에 해당 방법은 틀리다고 생각하진 않으나, View가 Add가 되면 될 수록 좌표값이 맞지 않는 오류가 발생하였다.

유지보수를 하는 개발자분들이라면, 이해하실게 다른사람이 짠 코드중 x,y 좌표 +3 이런식으로 되어있는 코드는 개발 당시에는 당연하게 이해하면서 작업이 진행되겠지만, 나중에 봤을때는 본인도 "뭐지..?" 라는 생각이 드는 코드이다.

필자도 이런코드를 최대한 개선하고 최신코드로 유지하는 부분을 작업할까 합니다.
그렇다면 기존에 방법에서 최대한 깔끔하게 개선하는 점은 어떻게 할까 고민을 하여
2가지 방법으로 구현하는 방법을 생각했다.

1. ScrollView + StackView
2. CollectionView

1번 방법은 필자가 StackView를 자주 사용하다보니 당연하게 생각한 방법으로 폰일때는 문제가 없으나, 패드일때 조금 귀찮지만 Pageing 처리시 불필요한 Stack의 생성 결국 iPhone, iPad 부분에서 View의 갯수 및 계산법이 달라진다는 부분에서 과연 맞는걸까 고민하였다.

2번 방법은 기존에 필자는 CollectionView는 기본적인 방법으로 사용하였었고, (이미지 참고) 가로 세로방향에 따라 그려지는 순서가 원하는 방식이 아니라고 생각했었다.



그렇지만, 2번 방법에서 순서만 해결된다면 View를 그리는 부분 또한 맞게 된다는 장점이 보이기 시작하였다.

그래서 추가로 알아보게 된 부분이 UICollectionView FlowLayout 이다.
FlowLayout을 통해 순서부분을 수정하여 사용할 수 있다는 글들을 많이 찾게되었다.

필자와 같은 고민을 하는 사람들이 많았고, 이참에 필자도 직접 만들어보기로 했다.

Sample : https://github.com/kimjiwook/HorizontalUICollectionViewFlowLayoutSample


FlowLayout.swift

//
//  JWCollectionViewFlowLayout.swift
//  JWCollectionViewFlowLayout
//
//  Created by JW_Macbook on 12/04/2019.
//  Copyright © 2019 JW_Macbook. All rights reserved.
//
import UIKit
class JWCollectionViewFlowLayout: UICollectionViewFlowLayout {
    
    /// CollectionView Cell row count (horizontal)
    @objc var collectionViewRow:NSInteger = 0
    
    /// CollectionView Cell Column count (Vertical)
    @objc var collectionViewColumn:NSInteger = 0
    
    /// (Optional) CollectionView Page Width
    @objc var collectionViewPageWidth:CGFloat = 0.0
    
    /// (Optional) CollectionView Page Height
    @objc var collectionViewPageHeight:CGFloat = 0.0
    
    /// CollectionView Attatibutes Array
    var attributesArray:[UICollectionViewLayoutAttributes] = [UICollectionViewLayoutAttributes]()
    
    /// 3. CollectionView 전체 크기 알려주는 정보.
    override var collectionViewContentSize: CGSize {
        
        // (전체갯수/한 페이지 표현정보) * CollectionView Width 정보
        // ex) 2.1 페이지여도 증가 시켜야함. ceil 함수 참조.
        let verticalInfo:Double = Double(attributesArray.count) / Double(collectionViewRow * collectionViewColumn * 1)
        var width:CGFloat = collectionViewPageWidth
        var height:CGFloat = collectionViewPageHeight
        let isPageing:Bool = self.collectionView?.isPagingEnabled ?? false
        
        // 가로방향
        if self.scrollDirection == .horizontal {
            width = CGFloat(ceil(verticalInfo)) * collectionViewPageWidth
            
            // 페이징이 아닐때 간격으로 Size 다시구하기.
            if !isPageing {
                let rowPageCount = ceil(verticalInfo)
                var addValut:CGFloat = 0.0
                for i in 0 ... Int(rowPageCount) {
                    addValut = addValut + CGFloat(i) * self.minimumInteritemSpacing
                }
                width = width + addValut
            }
        }
        
        // 세로방향
        else {
            height = CGFloat(ceil(verticalInfo)) * collectionViewPageHeight
            
            // 페이징이 아닐때 간격으로 Size 다시구하기.
            if !isPageing {
                let columnItmeCount = CGFloat(attributesArray.count)/CGFloat(collectionViewRow)
                height = columnItmeCount * self.itemSize.height + columnItmeCount * self.minimumLineSpacing
            }
        }
        
        return CGSize(width: width, height: height)
    }
    
    /// 1. CollectionView Prepare
    /// Cell 위치/크기 등 계산을 사전처리 가능
    override func prepare() {
        super.prepare()
        
        // PageWith 정보 없을시 화면 Size
        if 0.0 == collectionViewPageWidth {
            collectionViewPageWidth = self.collectionView?.frame.width ?? UIScreen.main.bounds.size.width
        }
        
        if 0.0 == collectionViewPageHeight {
            collectionViewPageHeight = self.collectionView?.frame.height ?? UIScreen.main.bounds.size.height
        }
        
        // itemSize 만들어주기.
        // 우선적으로 itemSize를 맞게 구해줍니다.
        // 가로 (Page가로 - (가로간격 * 가로갯수-1))/가로갯수
        let itemSizeWidth:CGFloat = ((self.collectionView?.frame.width)! - (self.minimumInteritemSpacing * CGFloat(collectionViewRow-1)))/CGFloat(collectionViewRow)
        // 세로 (Page세로 - (세로간격 * 세로갯수-1))/세로갯수
        let itemSizeHeight:CGFloat = ((self.collectionView?.frame.height)! - (self.minimumLineSpacing * CGFloat(collectionViewColumn-1)))/CGFloat(collectionViewColumn)
        
        self.itemSize = CGSize(width: itemSizeWidth, height: itemSizeHeight)
        
        // 1개의 section 으로 구성된 정보만 고려됨.
        // Attribute 정보 구하기.
        let itemCount:Int = self.collectionView?.numberOfItems(inSection: 0) ?? 0
        if itemCount > 0 {
            for i in 0 ... itemCount-1 {
                let indexPath = IndexPath(item: i, section: 0)
                let attributes = layoutAttributesForItem(at: indexPath)
                attributesArray.append(attributes!)
            }
        }
    }
    
    
    /// 2. prepare 에서 꾸민 Attributes Array 정보를 반환한다.
    ///  가장 마지막 전부다 Attributes 꾸미고 나서.
    /// - Parameter rect:
    /// - Returns:
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return attributesArray
    }
    
    /// 2. Cell 속성별 Size 정해주기.
    ///
    /// - Parameter indexPath:
    /// - Returns:
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        
        let attribute = UICollectionViewLayoutAttributes(forCellWith: indexPath)
        let index:Int = indexPath.item
        let page:Int = index / (collectionViewRow * collectionViewColumn)
        let isPageing:Bool = self.collectionView?.isPagingEnabled ?? false
        
        // 가로방향일때만 해당되는 데이터 내용.
        if self.scrollDirection == .horizontal {
            // 좌표계산하기.
            // (ex 2*3 칸으로 만들기)
            // 간격 : 가로, 세로 10 씩
            // 1. 가로위치 (row값에따른 위치 선정 0, 1)
            // 2. 아이템 크기 (기본+가로간격)
            // 3. 전체페이지의 위치값 (1페이지당 6개 아이템 씩)
            var x:CGFloat = CGFloat(index % collectionViewRow) *
                (self.itemSize.width + self.minimumInteritemSpacing) +
                CGFloat(page) * collectionViewPageWidth
            
            let y:CGFloat = CGFloat(index / collectionViewRow % collectionViewColumn) *
                (self.itemSize.height + self.minimumLineSpacing)
            
            // 페이징을 하지 않을때 간격정보 추가함.
            if !isPageing {
                x = x + CGFloat(page) * self.minimumInteritemSpacing
            }
            
            // 최종적인 cellRect 정보.
            attribute.frame = CGRect(x: x, y: y, width: self.itemSize.width, height: self.itemSize.height)
        } else {
            let x:CGFloat = CGFloat(index % collectionViewRow) *
                (self.itemSize.width + self.minimumInteritemSpacing)
            
            
            var y:CGFloat = CGFloat(index / collectionViewRow % collectionViewColumn) *
                (self.itemSize.height + self.minimumLineSpacing) +
                CGFloat(page) * collectionViewPageHeight
            
            // 페이징을 하지 않을때 간격정보 추가함.
            if !isPageing {
                y = y + CGFloat(page) * self.minimumLineSpacing
            }
            
            // 최종적인 cellRect 정보.
            attribute.frame = CGRect(x: x, y: y, width: self.itemSize.width, height: self.itemSize.height)
        }
        
        
        return attribute
    }
}
cs


이번 Sample의 거의 모든핵심이며, 필자도 조금 했깔렸던 부분이 실행 순서 였다.
주석에 실행되는 함수의 순서 및 기능의 역할을 적어두었습니다. (참고바람)


ViewController.swift (호출부분)

    override func viewDidLoad() {
        super.viewDidLoad()
    
        let customLayout = collectionView.collectionViewLayout as! JWCollectionViewHorizontalPageFlowLayout
        
        // 필자는 폰/패드가 row/column 이 틀리다.
        if UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.pad {
            customLayout.collectionViewRow = 2
            customLayout.collectionViewColumn = 3
        }
        
        else {
            customLayout.collectionViewRow = 4
            customLayout.collectionViewColumn = 1
        }
      
        collectionView.delegate = self
        collectionView.dataSource = self
        
        customLayout.scrollDirection = .horizontal
        collectionView.isPagingEnabled = true
        // 스크롤 시 빠르게 감속 되도록 설정
        collectionView.decelerationRate = .fast
        
    }
cs

결과 (Row, Column 값 틀리기 함.)



이와 같이 Custom FlowLayout 을 통해 원하는 형식으로 노출시키는 것이 가능하여, 필자의 업무중 하나는 위와 같은 형식으로 개선하였다.

기존에 TableView에 너무 적응이 되어있어서, Flowlayout을 이해 하지 않으려고 했던것 같아서 진작에 도전하지 않았던것이 아쉽다. 다음번에는 또 다른형식으로 개선했던 점이있으면 포스팅하겠다.



댓글