๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

๐ŸŽ iOS & Swift

[iOS] CollectionView์™€ PageControl๋กœ Pager ๊ตฌํ˜„ํ•˜๊ธฐ

CollectionView์™€ PageControl๋กœ Pager ๊ตฌํ˜„ํ•˜๊ธฐ

 

๐Ÿ’ฌ ๋“ค์–ด๊ฐ€๊ธฐ ์ „์—(Prepare)

์ƒ๊ฐ๋ณด๋‹ค ์šฐ๋ฆฌ๋Š” ํŽ˜์ด์ง• ๋˜๋Š” ํ™”๋ฉด์„ ์ž์ฃผ ๋งŒ๋‚ฉ๋‹ˆ๋‹ค.

 

์•ฑ์˜ ์˜จ๋ณด๋”ฉ ํ™”๋ฉด๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด, ์บ๋Ÿฌ์…€ ๋ทฐ(Carousel View), ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ๋Š” View Pager๋ผ๊ณ  ๋ถˆ๋ฆฌ์šฐ๋Š” ํƒญ ๊ฐ„ ์ „ํ™˜๋˜๋Š” ํ™”๋ฉด ๋“ฑ ์ •๋ง ๋งŽ์€๋ฐ์š”! (์‚ฌ์‹ค ์˜ˆ์‹œํ™”๋ฉด์„ ๋‹ค ์ฒจ๋ถ€ํ•˜๊ณ  ์‹ถ์ง€๋งŒ,,, ๊ท€์ฐฎ์€๊ฑด ์•ˆ๋น„๋ฐ€..ใ…Žใ…Ž,,, ๊ผญ ์ฒจ๋ถ€ํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค..)

 

์œ„์—์„œ ์–ธ๊ธ‰ํ•œ ํ™”๋ฉด์„ ๊ตฌํ˜„ํ•˜๋Š”๋ฐ์—๋Š” ๊ทธ ์ข…๋ฅ˜๋งŒํผ ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•๋“ค์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ด๋‹ค๊ฑฐ๋‚˜ ์Šคํฌ๋กค๋ทฐ๋ฅผ ์ด์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•, ๊ทธ๋ฆฌ๊ณ  ์˜ค๋Š˜ ์ œ๊ฐ€ ์†Œ๊ฐœํ•ด๋“œ๋ฆด ์ปฌ๋ ‰์…˜๋ทฐ๋ฅผ ์ด์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๊ฒ ๋„ค์š”.

ํ•„์š”ํ•œ ์ƒํ™ฉ์— ๋งž๊ฒŒ ์ ์ ˆํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋ฉด ๋˜๊ฒ ์ฃ ?

 

์˜ค๋Š˜ ์ œ๊ฐ€ ์˜ˆ์‹œ๋กœ ์„ค๋ช…๋“œ๋ฆด ํ™”๋ฉด์€ ์•ฑ์„ ์‚ฌ์šฉ์ž๊ฐ€ ์ฒ˜์Œ ์‹œ์ž‘ํ•˜๊ฒŒ ๋˜๋ฉด ๋งŒ๋‚˜๊ฒŒ ๋˜๋Š” ์˜จ๋ณด๋”ฉ ํ™”๋ฉด(Onboarding View)์ž…๋‹ˆ๋‹ค.

(๊ทธ๋‚˜์ €๋‚˜ ์ผ๋Ÿฌ์ŠคํŠธ ๋„ˆ๋ฌด ์˜ˆ์˜์ง€ ์•Š๋‚˜์š”...? ๋‘๋ฆฌ๋ฒˆ ๋””์ž์ด๋„ˆ ๋Œ€๋ฐ•.. ์ผ๋Ÿฌ์ŠคํŠธ ์ €์ž‘๊ถŒ์€ ๋‘๋ฆฌ๋ฒˆ ํŒ€์— ์žˆ์Šต๋‹ˆ๋‹ค!)

 

'์ž‘์€ ์›€์ง์ž„์ด ๋งŒ๋“œ๋Š” ์šฐ๋ฆฌ๋‹ค์šด ์—ฌํ–‰'  ์•ฑ ์„œ๋น„์Šค ๋‘๋ฆฌ๋ฒˆ(Dooribon)์˜ ์˜จ๋ณด๋”ฉ ํŽ˜์ด์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (Preview)

 

๐Ÿง ์Šค์ผ€์น˜(Sketch)

์ „์ฒด์ ์ธ ์Šค์ผ€์น˜

 

์ „์ฒด์ ์ธ ์Šค์ผ€์น˜๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

ํฐ ํ‹€์€ ์Šคํ† ๋ฆฌ๋ณด๋“œ ๋‚ด์—์„œ CollectionView์™€ PageControl ๊ทธ๋ฆฌ๊ณ  Button์„ ๋ฐฐ์น˜ํ•ด์„œ ์žก์•˜๊ตฌ์š”.

 

์…€ ๋””์ž์ธ ๊ฐ™์€ ๊ฒฝ์šฐ๋Š” CollectionViewCell Xib๋ฅผ ๋งŒ๋“ค์–ด์„œ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฒˆ์— ๋””์ž์ธ ํ•œ ๊ฒƒ์€ Lottie ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•ด์„œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํŒŒ์ผ์„ ๋„ฃ๊ฒŒ ๋˜์–ด์„œ

์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋“ค์–ด๊ฐˆ ์ž๋ฆฌ๋ฅผ UIView๋กœ ์˜์—ญ๋งŒ ์žก์•„์ฃผ์—ˆ์–ด์š”~

์ „์ฒด ๋””์ž์ธ
์…€ ๋””์ž์ธ

 

๐Ÿง ํด๋” ๊ตฌ์กฐ(Foldering)

์ „์ฒด ํด๋” ๊ตฌ์กฐ

 

๐Ÿง‘๐Ÿป‍๐Ÿ’ป ๊ตฌํ˜„ํ•˜๊ธฐ (Development)

์„ค์ •๋ถ€

์ผ๋‹จ ์ปฌ๋ ‰์…˜ ๋ทฐ๋ฅผ ๋ฐฐ์น˜ํ•˜๊ณ  ๋‚œ ๋‹ค์Œ์— ์ž˜ ์„ค์ •ํ•ด์•ผํ•˜๋Š” ๋ถ€๋ถ„์ด 2๊ตฐ๋ฐ ์ •๋„ ์žˆ์–ด์š”.

์™ผ์ชฝ์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋“ฏ์ด Paging Enabled์— ์ฒดํฌโœ…๋ฅผ ๊ทธ๋ฆฌ๊ณ  ์˜ค๋ฅธ์ชฝ์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋“ฏ์ด Estimate Size๋Š” None์œผ๋กœ ์„ค์ •ํ•ด์ฃผ์„ธ์š”.

 

๋”ฑํžˆ ์–ด๋ ค์šด ๋ถ€๋ถ„์€ ์—†์–ด์š”. ์ˆœ์„œ๋Œ€๋กœ ํ•œ ๋ฒˆ ๊ฐ€๋ณผ๊ฒŒ์š”.

์ฝ”๋“œ๋ถ€

1. ๊ธฐ๋ณธ ์„ธํŒ…

  • ํ”„๋กœํผํ‹ฐ ์„ธํŒ…
    • onboardingDate: ์˜จ๋ณด๋”ฉ ๊ฐ ํ™”๋ฉด์— ๋“ค์–ด๊ฐˆ ๋ฐ์ดํ„ฐ ๋ณ€์ˆ˜ (animation, title, description)
    • currentPage: ํ˜„์žฌ ํŽ˜์ด์ง€ ์œ„์น˜๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ณ€์ˆ˜
  • UI, ์ปฌ๋ ‰์…˜๋ทฐ, ๋”๋ฏธ๋ฐ์ดํ„ฐ ์„ธํŒ…
    // MARK: - Properties
    var onboardingData: [OnboardingDataModel] = []
    var currentPage: Int = 0 {
        didSet {
            pageControl.currentPage = currentPage
            if currentPage == onboardingData.count - 1 {
                nextButton.setTitle("Start", for: .normal)
            } else {
                nextButton.setTitle("Next", for: .normal)
            }
        }
    }
// MARK: - Custom Functions
extension OnboardingViewController {
    private func setUI() {
        nextButton.layer.cornerRadius = 10
        pageControl.isUserInteractionEnabled = false
    }
    
    private func setCollectionView() {
        onboardingCollectionView.delegate = self
        onboardingCollectionView.dataSource = self
        
        let onboardingNib = UINib(nibName: OnboardingCollectionViewCell.cellId, bundle: nil)
        onboardingCollectionView.register(onboardingNib, forCellWithReuseIdentifier: OnboardingCollectionViewCell.cellId)
    }
    
    private func setOnboardingData() {
        onboardingData.append(contentsOf: [
            OnboardingDataModel(lottieName: "onboarding1_img",
                                title: "์—ฌํ–‰์„ ๋– ๋‚˜๋ณผ๊นŒ์š”?",
                                description: "์—ฌํ–‰์„ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜ ์ฐธ์—ฌ ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•˜์—ฌ\n์ƒˆ๋กœ์šด ์—ฌํ–‰์„ ์‹œ์ž‘ํ•˜์„ธ์š”"),
            OnboardingDataModel(lottieName: "onboarding2_img",
                                title: "์šฐ๋ฆฌ๋‹ค์šด ์—ฌํ–‰์„ ๋งŒ๋“ค์–ด์š”",
                                description: "์œ ํ˜• ํ…Œ์ŠคํŠธ๋กœ ์—ฌํ–‰ ์Šคํƒ€์ผ์„ ํŒŒ์•…ํ•˜๊ณ ,\n๋ณด๋“œ์—์„œ ์†Œํ†ตํ•˜๋ฉฐ ์„œ๋กœ๋ฅผ ์•Œ์•„๊ฐˆ ์ˆ˜ ์žˆ์–ด์š”"),
            OnboardingDataModel(lottieName: "onboarding3_img",
                                title: "ํ•จ๊ป˜ ์—ฌํ–‰์„ ๋งŒ๋“ค์–ด์š”",
                                description: "์—ฌํ–‰ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ณต์œ ํ•˜์—ฌ\n์ผ์ •์„ ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”")
        ])
    }
}

 

2. ์ปฌ๋ ‰์…˜๋ทฐ ์„ธํŒ…

- ๊ทธ ๋‹ค์Œ์— ์šฐ๋ฆฌ๊ฐ€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ปฌ๋ ‰์…˜๋ทฐ๋ฅผ ๋‹ค๋ฃจ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ Delegate, DataSource, FlowLayout์„ ์„ค์ •์„ ํ•ด์š”.

- ์Šคํฌ๋กค๋ทฐ Delegate ๊ด€๋ จ ์ฝ”๋“œ๊ฐ€ ์žˆ๋Š”๋ฐ ๋’ค์—์„œ ์„ค๋ช…ํ• ๊ฒŒ์š”!

// MARK: - CollectionView Delegate, DataSource
extension OnboardingViewController: UICollectionViewDelegate, UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return onboardingData.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = onboardingCollectionView.dequeueReusableCell(withReuseIdentifier: OnboardingCollectionViewCell.cellId, for: indexPath) as? OnboardingCollectionViewCell else { return UICollectionViewCell() }
        cell.setOnboardingSlides(onboardingData[indexPath.row])
        return cell
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let width = scrollView.frame.width
        currentPage = Int(scrollView.contentOffset.x / width)
    }
}

// MARK: - CollectionView Delegate Flow Layout
extension OnboardingViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: collectionView.frame.width, height: collectionView.frame.height)
    }
}

 

3. ์Šคํฌ๋กค์— ๋”ฐ๋ฅธ ์˜จ๋ณด๋”ฉ ํŽ˜์ด์ง€ ์Šค์™€์ดํ”„

์œ„์—์„œ ๋ณด์•˜๋˜ ์ฝ”๋“œ์ธ๋ฐ์š”. ์• ํ”Œ ๊ณต์‹๋ฌธ์„œ๋ฅผ ๋ณด๋ฉด ์ปจํ…์ธ ๋ฅผ ์Šคํฌ๋กคํ• ๋•Œ ์ปจํ…์ธ  ์˜คํ”„์…‹์˜ ๋ณ€๊ฒฝ ๋‚ด์šฉ์„ ๋Œ€๋ฆฌ์ž์—๊ฒŒ ์•Œ๋ฆฐ๋‹ค๋ผ๊ณ  ์„ค๋ช…ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ„๋‹จํ•˜๊ฒŒ ๋ณด๋ฉด ์ปฌ๋ ‰์…˜๋ทฐ๋„ ์Šคํฌ๋กค๋ทฐ์˜ ์ผ์ข…์ด์ž–์•„์š”? ์˜†์œผ๋กœ ์Šค์™€์ดํ”„ ํ•  ๋•Œ๋งˆ๋‹ค ์œ„์น˜๋ฅผ ๊ฐ€์ ธ์™€์„œ ํŽ˜์ด์ง•์„ ์ฒ˜๋ฆฌํ•ด์ฃผ๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค!

Apple Developer ๊ณต์‹๋ฌธ์„œ

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let width = scrollView.frame.width
        currentPage = Int(scrollView.contentOffset.x / width)
    }

์ด 3๊ฐ€์ง€์˜ ์ปจํ…์ธ ๊ฐ€ ์žˆ์œผ๋‹ˆ๊น ์ปจํ…์ธ  ์˜คํ”„์…‹์„ ์Šคํฌ๋กค๋ทฐ์˜ ์ „์ฒด ๋„“์ด๋กœ ๋‚˜๋ˆ ๋ณด๋ฉด

currentPage๋Š” ์ฝ”๋“œ์— ๋”ฐ๋ผ์„œ 0, 1, 2๊ฐ€ ๋‚˜์˜ฌ ์ˆ˜ ์žˆ๊ฒ ๋„ค์š”!

 

4. Next ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ๋•Œ๋งˆ๋‹ค ์ปจํ…์ธ  ์ „ํ™˜

์œ„์—์„œ gif๋ฅผ ๋ณด์‹œ๋ฉด ์šฐ์ธก ์•„๋ž˜์˜ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ๋•Œ๋งˆ๋‹ค ํ™”๋ฉด์ด ์ „ํ™˜๋˜๋Š” ๊ฒƒ์„ ๋ณด์‹ค ์ˆ˜ ์žˆ์–ด์š”.

์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€๋ฐ์š”!

 

๊ฐ€์žฅ ๋งˆ์ง€๋ง‰ ์ปจํ…์ธ ์ผ๋•Œ๋Š” ๋‚˜์ค‘์— ํ™”๋ฉด์ „ํ™˜์„ ํ•ด์•ผํ•˜๋‹ˆ๊นŒ ๋ถ„๊ธฐ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ๊ณ ,

๊ทธ ์ „ ์ƒํ™ฉ์—์„œ๋Š” currentPage๋ฅผ 1์”ฉ ์ฆ๊ฐ€์‹œ์ผœ์ฃผ๊ณ ,

indexPath๋ฅผ ๊ตฌํ•ด์„œ scrollToItem์ด๋ผ๋Š” ๋ฉ”์„œ๋“œ๋กœ ์ปฌ๋ ‰์…˜๋ทฐ๋ฅผ ํŽ˜์ด์ง• ํ•ด์ฃผ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

(์ด ๋ถ€๋ถ„์€ ๊ณต์‹๋ฌธ์„œ์—์„œ ๋ฉ”์„œ๋“œ๋ฅผ ์ข€ ๋” ์ฐพ์•„๋ณด๋ฉด ์ข‹์„ ๋“ฏ ํ•˜๋„ค์š”..!, ๋‹ค ์„ค๋ช…ํ•˜๊ธฐ์—๋Š” ๋‚ด์šฉ์ด ๋„ˆ๋ฌด ๊ธธใ…‡...)

    // MARK: - Actions
    @IBAction private func nextButtonTapped(_ sender: Any) {
        if currentPage == onboardingData.count - 1 {
            print("go to main")
        } else {
            currentPage += 1
            let indexPath = IndexPath(item: currentPage, section: 0)
            onboardingCollectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
        }
    }

 

5. ํ”„๋กœํผํ‹ฐ ๊ด€์ฐฐ์ž๋ฅผ ํ™œ์šฉํ•ด ๋ฒ„ํŠผ ์ปจํ…์ธ  ๋ณ€๊ฒฝ

๋„ค ๋งˆ์ง€๋ง‰ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค! ์šฐ๋ฆฌ๋Š” ํ”„๋กœํผํ‹ฐ ๊ด€์ฐฐ์ž๋ฅผ ์ด์šฉํ•ด์„œ ๋ณ€๊ฒฝ๋˜๋Š” ๋‚ด์šฉ์ด ์žˆ์„๋•Œ๋งˆ๋‹ค ์–ด๋–ค ์•ก์…˜์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ €๋„ ์ตœ๊ทผ์— ํ•ด๋‹น ๋‚ด์šฉ์„ ์ž์ฃผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š”๋ฐ์š”. ๊ธฐํšŒ๊ฐ€ ๋˜๋ฉด ๋‚ด์šฉ์„ ๋”ฐ๋กœ ์ •๋ฆฌํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

์šฐ์„  ์ฝ”๋“œ๋ฅผ ๋ณด์‹œ๋ฉด์š”!

    var currentPage: Int = 0 {
        didSet {
            pageControl.currentPage = currentPage
            if currentPage == onboardingData.count - 1 {
                nextButton.setTitle("Start", for: .normal)
            } else {
                nextButton.setTitle("Next", for: .normal)
            }
        }
    }

์š”๋ ‡๊ฒŒ! currentPage์˜ ๊ฐ’์ด ๋ฐ”๋€”๋•Œ๋งˆ๋‹ค pageControl์˜ ํ˜„์žฌ ํŽ˜์ด์ง€ ์œ„์น˜๋ฅผ ๋ฐ”๊ฟ”์ฃผ๊ณ ,

๋งˆ์ง€๋ง‰ ์˜จ๋ณด๋”ฉ ํŽ˜์ด์ง€์— ๋„๋‹ฌํ•˜๋ฉด ๋ฒ„ํŠผ์˜ ํƒ€์ดํ‹€์„ Next์—์„œ Start๋กœ ๋ฐ”๊ฟ”์ฃผ๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๐Ÿ™Œ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ(End)

์“ฐ๋‹ค๋ณด๋‹ˆ, ์–ด๋Š๋ง ๊ธ€์ด ๋์ด ๋‚ฌ๋„ค์š”..! ์‚ฌ์‹ค ๋‚ด์šฉ์ด ๋„ˆ๋ฌด ๊ธธ์–ด์งˆ ๊ฑฐ ๊ฐ™์•„ ์š”์•ฝํ•ด์„œ ๋‚ด์šฉ์„ ์ •๋ฆฌํ–ˆ๋Š”๋ฐ

ํ•„์š”ํ•˜์‹  ๋‚ด์šฉ์ด๋ผ๋ฉด ์ฒœ์ฒœํžˆ ๋”ฐ๋ผํ•ด๋ณด์‹œ๊ณ , ํ˜น์‹œ๋‚˜ ๊ถ๊ธˆํ•œ ์ ์ด๋‚˜ ์ž˜๋ชป๋œ ๋‚ด์šฉ์ด ์žˆ๋‹ค๋ฉด ๋Œ“๊ธ€ ๋‚จ๊ฒจ์ฃผ์„ธ์š” :)

 

ํŽ˜์ด์ € ๊ด€๋ จํ•ด์„œ๋Š” ๋” ์–ด๋ ค์šด ๋‚ด์šฉ๋“ค๋„ ๋งŽ์ด ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋Š”๋ฐ์š”.

๋‚˜์ค‘์— ๋‹ค๋ฅธ ๋‚ด์šฉ์œผ๋กœ ๋‹ค์‹œ ์ฐพ์•„์˜ค๊ฒ ์Šต๋‹ˆ๋‹ค :) ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค~

 

์ „์ฒด ์ฝ”๋“œ๋Š” ์ œ ๊นƒํ—ˆ๋ธŒ ๋งํฌ์— ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜์— ์ฒจ๋ถ€ํ•˜๋„๋ก ํ• ๊ฒŒ์š”!

https://github.com/Taehyeon-Kim/iOS-Wiki/tree/master/OnboardingSample