Jiwift

[iOS/Swift] Carousel CollectionView / Horizontal Card View / UIKit으로 카드 뷰 구현 본문

iOS Dev/UIKit

[iOS/Swift] Carousel CollectionView / Horizontal Card View / UIKit으로 카드 뷰 구현

지위프트 2022. 12. 22. 21:50
반응형

 

 

 

 오늘은 위와 같이 좌우로 움직이는 CollectionView를 만들어보려고 합니다. 평소에 저는 이 방법을 검색할 때 Horizontal Card View 이런 식으로 찾았습니다. 하지만 어느 날 우리 빛과 같은 종권님 블로그에서 Carousel이라는 단어로 글로 올리신 걸 보고 찾아봤습니다.

 

 왼쪽이 Carousel 결과이고 오른쪽이 horizontal card view입니다. 확실히 검색어에 따라서 원하는 정보량이 달라지긴 하네요. 이렇게 용어를 하나 배웠습니다. 하지만 오늘은 종권님 블로그에 작성된 내용을 이용해서 진행해보려고 합니다.

 

[iOS - swift] 1. 스크롤 영역을 암시해주는 Carousel 구현 방법 (UICollectionView, 수평 스크롤 뷰, paging 구현) (tistory.com)

 

[iOS - swift] 1. 스크롤 영역을 암시해주는 Carousel 구현 방법 (UICollectionView, 수평 스크롤 뷰, paging 구

1. 스크롤 영역을 암시해주는 Carousel 구현 - (UICollectionView, 수평 스크롤 뷰, paging 구현) 2. 스크롤 영역을 암시해주는 Carousel 구현 - 포커스 영역 이펙트 구현 아이디어 수평 스크롤 뷰 구현 방법은 F

ios-development.tistory.com

 우선 위는 원글 내용입니다. 기존 글에서는 레이아웃과 구현 로직 모두 코드로 작성하셨습니다. 저는 UI는 Storyboard에서 작업하려고 합니다. 구현 로직과 설명은 원래 글인 위 링크를 통해서 읽으시는 게 맞다고 판단하여 여기서는 단순하게 작업 내용만 올리도록 하겠습니다. 

(UIKit을 사용지 않는 분들은 위 글을 통해 방법을 보시고 Storyboard를 사용하실 분들도 방법은 한번 보고 오시는 것을 추천합니다.)

 

아래는 Storyboard입니다.

 잘 보이기 위해서 일단 배경 이지미를 하나 깔았습니다. CollectionView를 원하는 크기로 배치해 주세요. 저는 높이 300으로 주었고 leading과 trailing 모두 0으로 주었습니다. 오늘 코드 기준으로 높이는 자유이지만 leading, trailing은 0으로 주세요. 

 

 그리고 Cell은 Custom으로 사용할 예정이니 클릭해서 삭제해줍니다. Storyboard에서 클릭해도 되고 왼쪽 빨간색 네모 부분을 눌러서 삭제해도 됩니다. 

 

 이제 깔끔해진 CollectionView를 사용을 원하는 ViewController에 IBOutlet으로 연결합니다. 저는 'myCollectionView'로 이름을 정했습니다.

 

  이제 myCollectionView를 datasource와 delegate를 다 잡아줍니다. 저는 delegate를 extension으로 뺏습니다. 이 상태에서 에러가 발생하는데 에러를 클릭하고 'fix'를 누르고 일단 다음으로 넘어갑니다.

'UICollectionViewDataSource', 'UICollectionViewDelegateFlowLayout'

 

 그리고 커스텀 Cell을 만들건데 우리 프로젝트에 폴더 하나 생성에서 Cell을 거기다 넣을 겁니다.(이건 각자 자유롭게 생성) 폴더 이름은 'MyCollectionViewCell'로 정했습니다.

 

 그리고 폴더에 UICollectionViewCell을 생성할 겁니다. 이름은 'MyCollectionViewCell'로 하고 'Also create XIB file'을 체크해 주세요.

 

 생성된 Cell의 identifier를 'MyCollectionViewCell'로 설정합니다.

 그리고 Cell의 높이는 아까 위에 생성한 CollectionView와 같이 300으로 줄 거고요. Cell은 제약조건이 없어서 그냥 높이를 300으로만 주면 되는데요. 그냥 그림을 그리기 위한 공간?으로 생각하시고 그림이 담기고 작업할 화면의 크기를 정한다고 생각하시면 됩니다. 

  이제는 cell 안에 UIView를 생성하고 제약조건을 상/하/좌/우 모두 0으로 주고 높이 제약도 300으로 줍니다. UIView 높이는 제약조건을 주어야 우리가 원하는 높이가 제대로 반영됩니다. 

// in ViewController
private enum Const {
    static let itemSize = CGSize(width: 300, height: 400)
    static let itemSpacing = 24.0

    static var insetX: CGFloat {
    	(UIScreen.main.bounds.width - Self.itemSize.width) / 2.0
    }

    static var collectionViewContentInset: UIEdgeInsets {
    	UIEdgeInsets(top: 0, left: Self.insetX, bottom: 0, right: Self.insetX)
    }
}


// in ViewController
private let collectionViewFlowLayout: UICollectionViewFlowLayout = {
    let layout = UICollectionViewFlowLayout()
    layout.scrollDirection = .horizontal
    layout.itemSize = Const.itemSize // <-
    layout.minimumLineSpacing = Const.itemSpacing // <-
    layout.minimumInteritemSpacing = 0
    return layout
}()

 다음은 위 코드를 우리가 사용하는 CollectionView가 있는 곳에 넣어주세요. Const는 cell의 너비와 cell의 좌/우 spacing을 결정합니다. 값을 조절하면서 여백을 조절하면 됩니다. 

private func myCollectionViewInit() {
    self.myCollectionView.collectionViewLayout = self.collectionViewFlowLayout
    self.myCollectionView.isScrollEnabled = true
    self.myCollectionView.showsHorizontalScrollIndicator = false
    self.myCollectionView.showsVerticalScrollIndicator = true
    self.myCollectionView.backgroundColor = .clear
    self.myCollectionView.clipsToBounds = true
    self.myCollectionView.register(UINib(nibName: "MyCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "MyCollectionViewCell")
    self.myCollectionView.isPagingEnabled = false // <- 한 페이지의 넓이를 조절 할 수 없기 때문에 scrollViewWillEndDragging을 사용하여 구현
    self.myCollectionView.contentInsetAdjustmentBehavior = .never // <- 내부적으로 safe area에 의해 가려지는 것을 방지하기 위해서 자동으로 inset조정해 주는 것을 비활성화
    self.myCollectionView.contentInset = Const.collectionViewContentInset // <-
    self.myCollectionView.decelerationRate = .fast // <- 스크롤이 빠르게 되도록 (페이징 애니메이션같이 보이게하기 위함)
    self.myCollectionView.translatesAutoresizingMaskIntoConstraints = false
}

 그리고 위 함수를 넣어서 Collection View를 설정합니다. 중간에 register를 통해서 우리가 사용하고 싶은 Cell을 등록합니다. 여러 개의 Cell을 등록할 수 있으니 원하시는 대로 등록하고 상황마다 다르게 사용하시면 됩니다.

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    let scrolledOffsetX = targetContentOffset.pointee.x + scrollView.contentInset.left
    let cellWidth = Const.itemSize.width + Const.itemSpacing
    let index = round(scrolledOffsetX / cellWidth)
    targetContentOffset.pointee = CGPoint(x: index * cellWidth - scrollView.contentInset.left, y: scrollView.contentInset.top)
}

 DataSource는 원하시는 방법으로 설정을 해주시면 되고, 아래 UICollectionViewDelegateFlowLayout에는 위 코드를 넣어주시면 됩니다. 이게 있어야 CollectionView 드래그하면서 index도 나오고 애매한 위치에 멈추면 자리를 잡아주는 기능을 합니다. 

 

 

아래는 전체 코드입니다.

//
//  ViewController.swift
//  blog
//
//  Created by 김지태 on 2022/12/20.
//

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var myCollectionView: UICollectionView!
    
    // in ViewController
    private enum Const {
        static let itemSize = CGSize(width: 300, height: 400)
        static let itemSpacing = 24.0

        static var insetX: CGFloat {
            (UIScreen.main.bounds.width - Self.itemSize.width) / 2.0
        }
            
        static var collectionViewContentInset: UIEdgeInsets {
            UIEdgeInsets(top: 0, left: Self.insetX, bottom: 0, right: Self.insetX)
        }
    }
    
    // in ViewController
    private let collectionViewFlowLayout: UICollectionViewFlowLayout = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        layout.itemSize = Const.itemSize // <-
        layout.minimumLineSpacing = Const.itemSpacing // <-
        layout.minimumInteritemSpacing = 0
        return layout
    }()
        
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        self.myCollectionView.dataSource = self
        self.myCollectionView.delegate = self
        
        self.myCollectionViewInit()
    }
    
    private func myCollectionViewInit() {
        self.myCollectionView.collectionViewLayout = self.collectionViewFlowLayout
        self.myCollectionView.isScrollEnabled = true
        self.myCollectionView.showsHorizontalScrollIndicator = false
        self.myCollectionView.showsVerticalScrollIndicator = true
        self.myCollectionView.backgroundColor = .clear
        self.myCollectionView.clipsToBounds = true
        self.myCollectionView.register(UINib(nibName: "MyCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "MyCollectionViewCell")
        self.myCollectionView.isPagingEnabled = false // <- 한 페이지의 넓이를 조절 할 수 없기 때문에 scrollViewWillEndDragging을 사용하여 구현
        self.myCollectionView.contentInsetAdjustmentBehavior = .never // <- 내부적으로 safe area에 의해 가려지는 것을 방지하기 위해서 자동으로 inset조정해 주는 것을 비활성화
        self.myCollectionView.contentInset = Const.collectionViewContentInset // <-
        self.myCollectionView.decelerationRate = .fast // <- 스크롤이 빠르게 되도록 (페이징 애니메이션같이 보이게하기 위함)
        self.myCollectionView.translatesAutoresizingMaskIntoConstraints = false
    }
}


extension ViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 10
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = self.myCollectionView.dequeueReusableCell(withReuseIdentifier: "MyCollectionViewCell", for: indexPath) as! MyCollectionViewCell
        return cell
    }
}

extension ViewController: UICollectionViewDelegateFlowLayout {
    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        let scrolledOffsetX = targetContentOffset.pointee.x + scrollView.contentInset.left
        let cellWidth = Const.itemSize.width + Const.itemSpacing
        let index = round(scrolledOffsetX / cellWidth)
        targetContentOffset.pointee = CGPoint(x: index * cellWidth - scrollView.contentInset.left, y: scrollView.contentInset.top)
    }
}
반응형