지난번엔 맥덕 앱의 검색 기능 구현 관련해서 설명드렸습니다.

맥덕 앱에 대해선 추후에 런칭 이후 설명드리겠습니다.

https://developer-p.tistory.com/162

 

iOS | 검색기능 구현 | 검색중일 때 text 일치하는 부분 색상, 폰트 변경하는 방법. (Text When searching,

현재 저는 맥덕 iOS앱을 개발중입니다. 맥덕 앱에 대해선 추후에 런칭이 끝난 뒤 제대로 소개하겠습니다. 맥덕 앱의 검색기능을 구현 중인데, 검색 시 일치하는 키워드는 색상이 변경하는 걸 많

developer-p.tistory.com


 

컬렉션뷰에서 무한스크롤을 사용하는 이유?

: (이미지가 적을 땐 문제가 되지 않지만)

예를 들어 요기요, 배민과 같은 대형 앱에선 수십만장의 이미지 데이터가 서버DB에 있을겁니다.

 

컬렉션뷰에서 이미지를 불러올 때, 서버에 있는 모든 이미지 데이터를 불러와 보여주게 된다면

사용자는 아이폰이 모든 이미지처리를 하는 동안 로딩이 진행되는, 안좋은 경험을 겪게 됩니다.

즉, 사용자의 경험을 방해하기 때문입니다.

 

이를 해결하기 위해 사용자가

컬렉션뷰의 최하단까지 스크롤을 했을 때 -> 그 이미지의 마지막 rowNumber데이터를 포함한 api를 호출 -> 응답 데이터를 컬렉션뷰에 이어서 보여줌. -> 컬렉션뷰의 최하단까지 스크롤을 했을 때 ... 

이런 방식으로 반복되며 무한스크롤을 구현합니다.

 


이번엔 UICollectionView무한스크롤 구현하면서 막혔던 점을 함께 설명드리겠습니다.

 

 

막혔던 점 - 무한 로딩

(오류 해결 방법은 중 하단에 적혀있습니다.)

아래는 오류 상황에 대한 기록입니다.

 

무한 스크롤 기능 구현 중 - 무한 로딩 오류 발생.

 

이 글은 컬렉션뷰를 이미 구현했다는 가정 하에 설명드립니다.

 


무한 스크롤 구현 방법

1. UICollectionReusableView 파일을 만들어줍니다.

(Also create XIB file도 체크합니다.)

 


 

2. UIActivityIndicatorView를 연결해주고, Identifier를 입력해줍니다.

 


3. 컬렉션뷰가 있는 뷰컨트롤러아래 코드들을 추가합니다.

 

viewDidLoad함수 밖에 추가합니다.

var isLoading: Bool = false // 컬렉션뷰 인디케이터에 쓰임.
var loadingView: LoadingReusableView? // 인디케이터 있는 뷰.(CollectionReusableView)

 

viewDidLoad함수 안에 추가합니다.

// Register Loading Reusable View
let loadingReusableNib = UINib(nibName: "LoadingReusableView", bundle: nil)
imageCollectionView.register(loadingReusableNib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "loadingreusableviewid")

 

extension UICollectionViewDelegate, UICollectionViewDataSource 부분에 아래 코드를 추가합니다.

각 메서드에 대한 설명은 주석에 적어놨습니다.

(인디케이터가 눈에 잘 띄도록 노란색으로 설정했습니다. - UIColor.clear로 설정하시면 됩니다.)

 

// 컬렉션뷰 footer(인디케이터) 사이즈 설정.
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
        if self.isLoading == true || imageResult.count == BeerData.details.seeReviewMoreImageCount { // 로딩중이거나 || 서버의 모든 이미지 다 불러왔다면,
            return CGSize.zero // 로딩하면 안됨.
        } else {
            return CGSize(width: collectionView.bounds.size.width, height: 55)
        }
    }
    // footer(인디케이터) 배경색 등 상세 설정.
    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        if kind == UICollectionView.elementKindSectionFooter {
            let aFooterView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "loadingreusableviewid", for: indexPath) as! LoadingReusableView
            loadingView = aFooterView
//            loadingView?.backgroundColor = UIColor.clear
            loadingView?.backgroundColor = UIColor.mainYellow
            return aFooterView
        }
        return UICollectionReusableView()
    }
    
    // 인디케이터 로딩 애니메이션 시작. (footer appears)
    func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) {
        if elementKind == UICollectionView.elementKindSectionFooter {
            if self.isLoading {
                self.loadingView?.activityIndicator.startAnimating()
            } else {
                self.loadingView?.activityIndicator.stopAnimating()
            }
        }
    }
    // 인디케이터 로딩 애니메이션 끝. (footer disappears)
    func collectionView(_ collectionView: UICollectionView, didEndDisplayingSupplementaryView view: UICollectionReusableView, forElementOfKind elementKind: String, at indexPath: IndexPath) {
        if elementKind == UICollectionView.elementKindSectionFooter {
            self.loadingView?.activityIndicator.stopAnimating()
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        if imageResult.count == BeerData.details.seeReviewMoreImageCount { // 서버의 모든 이미지 다 불러왔다면,
            print("더 이상 로딩하면 X.") //  더이상 로딩하면 안됨.
        }
        else { // 서버의 모든 이미지를 다 불러온 게 아닌 상황이고,
            if indexPath.row == imageResult.count-1 && self.isLoading == false { // 사용자 스크롤이 마지막 index면서 로딩중이 아닐 때,
                loadMoreData()
                print("로딩 more Data.")
            }
        }
        
    }

    func loadMoreData() {
        if !self.isLoading {
            self.isLoading = true
            DispatchQueue.global().async {
                // Fake background loading task for 2 seconds
                sleep(2)
                // Download more data here
                DispatchQueue.main.async {
                    self.seeReviewMoreImageDataManager.getBeerReviewImageInfo(rowNumber: BeerData.details.rowNumber, beerId: BeerData.details.beerId, delegate: self) // 모든 이미지정보 가져오는 api 호출.
                    
//                    self.imageCollectionView.reloadData()
                    self.isLoading = false
                }
            }
        }
    }

위 코드는 제가 처한 무한 로딩 오류를 해결하기 위해, 조건문이 일부 수정되어 있습니다.

하단에 해결 방법을 참고하셔서 필요 없는 코드 부분을 빼시면 될 거 같습니다.

 

그냥 복붙 하시면 돌아가지 않습니다. 어디에든 적용시키려면 하단 링크에 있는 코드를 참고하시기 바랍니다.

아래 링크의 글은 꼭 확인해보시길 권장드립니다.

 

https://johncodeos.com/how-to-add-load-more-infinite-scrolling-in-ios-using-swift/

 

How to add Load More / Infinite Scrolling in iOS using Swift | John Codeos - Blog with Free iOS & Android Development Tutorials

Adding Load More / Infinite Scrolling feature in TableView and CollectionView

johncodeos.com

 


무한 로딩 해결 방법

: 모든 데이터(데이터의 마지막)에 도달하게 되면 더 이상 로딩하면 안 되지만, 그 부분에 대한 조건식이 없어서 로딩이 계속 시도됨.

 

해결방법

 

1. 뷰에 가장 처음 입장 시 전체 데이터의 수를 따로 저장해놓고,

무한 로딩 해결방법 1

 

2. (현재 보여주고 있는 이미지 배열의 수(imageResult) = 전체 데이터의 수(BeerData.details.seeReviewMoreImageCount))가 같다면,

(≠ 단순히 이미지를 담고 있는 배열의 인덱스 끝에 도달한 것이 아니라, 서버의 모든 데이터의 끝에 도달한 상황.)

무한 로딩 해결방법 2

 

3. 로딩 사이즈를 0으로 함으로써 인디케이터를 없애서 해결했습니다.

무한 로딩 해결방법 3

 


 

[무한 로딩 오류 해결 완료 영상]

무한 로딩 오류 해결 완료.

 


UICollectionView 무한 스크롤(infinite scroll)을 UICollectionReusableView을 활용해 구현하면서,

무한 로딩이 되는 문제를 해결하는 방법에 대해서 알아봤습니다.

 

이 글이 정답은 아니며, 코드의 기본 틀을 잡은 뒤

제가 해결한 것처럼 각자의 프로젝트에 맞게 조건식을 수정하면 될 것 같습니다.

 

작업 중 막히시거나, 질문이 있으시다면 댓글을 통해 편하게 질문 주세요.

도움이 되셨으면 좋겠습니다!

 


 

 

 

아래는 컬렉션뷰가 있는 뷰컨트롤러전체 코드입니다.

컬렉션뷰 외 다른 것도 섞여 있으니, 이 중 필요하신 부분만 참고하시면 될 것 같습니다.

전체 코드

//
//  SeeReviewMoreImageViewController.swift
//  MackDuck
//
//  Created by sumin on 2022/01/17.
//

import UIKit

class SeeReviewMoreImageViewController: UIViewController {

    var seeReviewMoreImageDataManager: SeeReviewMoreImageDataManager = SeeReviewMoreImageDataManager() // 더보기버튼 - 모든 이미지정보 가져오는 dataManager
    var imageResult: [SeeReviewMoreImageResult] = [] // 이미지 string 저장해놓을 배열 선언.
    var isFirstTime: Bool = true // 첫번째일때만 전체count 저장할 변수 선언.
    var isLoading: Bool = false // 컬렉션뷰 인디케이터에 쓰임.
    var loadingView: LoadingReusableView? // 인디케이터 있는 뷰.(CollectionReusableView)
    
    @IBOutlet weak var navigationBarItem: UINavigationItem! // 상단 네비게이션바 아이템
    @IBOutlet weak var imageCollectionView: UICollectionView! // 이미지 컬렉션뷰
    
    override func viewDidLoad() {
        super.viewDidLoad()

        navigationBarItem.title = "사진 모아보기"
        navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.font: UIFont(name: "NotoSansKR-Regular", size: 14)!, NSAttributedString.Key.foregroundColor: UIColor.mainWhite]

        
        self.view.backgroundColor = .mainBlack
        self.navigationController?.navigationBar.backgroundColor = .mainBlack // 이걸 해줘야 네비게이션바 색 바뀌는듯.
        self.navigationController?.navigationBar.barTintColor = .mainBlack // 상단 네비게이션 바 색상 변경
        self.navigationController?.navigationBar.isTranslucent = false // 상단 네비게이션 바 반투명 제거
        
        // Register Loading Reuseable View
        let loadingReusableNib = UINib(nibName: "LoadingReusableView", bundle: nil)
        imageCollectionView.register(loadingReusableNib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "loadingreusableviewid")

        
        imageCollectionView.register(SeeReviewMoreImageCollectionViewCell.nib(), forCellWithReuseIdentifier: SeeReviewMoreImageCollectionViewCell.identifier) // 이미지 컬렉션뷰
        imageCollectionView.delegate = self
        imageCollectionView.dataSource = self
        
        imageCollectionView.showsHorizontalScrollIndicator = false // 컬렉션뷰 스크롤바 숨김
        imageCollectionView.showsVerticalScrollIndicator = false
        
        
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        imageResult = [] // 콜렉션 뷰 초기화.
        BeerData.details.rowNumber = "0" // 페이지 입장시 rowNumber값 초기화.
        self.seeReviewMoreImageDataManager.getBeerReviewImageInfo(rowNumber: BeerData.details.rowNumber, beerId: BeerData.details.beerId, delegate: self) // 모든 이미지정보 가져오는 api 호출.
    }
    
    @IBAction func clickBackButton(_ sender: UIBarButtonItem) {
        self.navigationController?.popViewController(animated: true)
    }


}


// MARK: - 맥주 리뷰 모든 이미지 GET Api
extension SeeReviewMoreImageViewController {
    
    func didSuccessGetBeerReviewImageInfo(_ result: SeeReviewMoreImageResponse) { // beerId가 서버에 제대로 보내졌다면 -> 화면(HomeStoryboard)에서 모든 이미지 ui 작업.
        print("서버로부터 맥주 리뷰 - 모든 이미지 GET 성공!")
        print("response 내용 : \(result)")
        
        // api 데이터 가져온거로 모든 이미지(컬렉션뷰) ui 구성.
        imageCollectionView?.reloadData()
//        imageResult = result.result // 모든 이미지 배열에 저장.
        
        if isFirstTime == true { // 이 화면에 들어온지 처음이라면,
            BeerData.details.seeReviewMoreImageCount = result.result.count // 서버에 있는 모든 리뷰 이미지의 개수를 저장해 놓음.
            isFirstTime = false // 이유: 맨 마지막 이미지에 도달했을 때, (imageResult데이터 개수 = 전체 개수)와 같다면 더이상 로딩할 필요 없으니깐.
        }
        
        var count: Int = 0
        for data in result.result { // 모든 이미지 배열 중 10개씩만 저장.
            imageResult.append(data)
            count = count + 1
            if count == 10 { // 10개 저장하면 break 하고, 스크롤 다 하면 또 불러와서 보여줌.
                count = 0
                break
            }
        }
        
        print("################# 값 변경 전 : \(BeerData.details.rowNumber)#####################")
//        BeerData.details.rowNumber = result.result[0].rowNumber
//        BeerData.details.rowNumber = result.result.last!.rowNumber
        BeerData.details.rowNumber = imageResult.last!.rowNumber
        print("################# 값 변경 후 : \(BeerData.details.rowNumber)#####################")
        
        
    }

    func failedToGetBeerReviewImageInfo(message: String, code: Int) { // 오류메시지 & code번호 몇인지
        print("리뷰 정보 GET 실패...")
        print("실패 이유 : \(message)")
        print("오류 코드 : \(code)")
        
        if code == 2000 { // 실패 이유 : "JWT 토큰을 입력해주세요."
//            showAlert(title: message, message: "")
        }
        else if code == 3010 { // 실패 이유 : "해당 상품에 관한 리뷰가 없습니다."
            
        }
    }
    
}

// MARK: - CollectionView
extension SeeReviewMoreImageViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    
    // CollectionView
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return imageResult.count // 이미지 컬렉션뷰
    }
        
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SeeReviewMoreImageCollectionViewCell.identifier, for: indexPath) as! SeeReviewMoreImageCollectionViewCell
        let imageUrlString = imageResult[indexPath.row].reviewImgUrl
        
        cell.configure(with: imageUrlString)
        
        return cell
    }
    
    // 사진 클릭시 (확대된) 다음 페이지로 넘어가는 함수.
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        print("Cell \(indexPath.row) 클릭.")
        let imageUrlString = imageResult[indexPath.row].reviewImgUrl // 컬렉션뷰 cell의 데이터 → 새로운 VC에 전달하는 방법. -  https://stackoverflow.com/questions/41831994/how-to-pass-collection-view-data-to-a-new-view-controller

        let imageDetailVC = self.storyboard?.instantiateViewController(withIdentifier: "ImageDetailVC") as? ImageDetailViewController
        imageDetailVC?.navTitle = "\(indexPath.row + 1) / \(imageResult.count)"
        imageDetailVC!.imageUrlString = imageUrlString
        self.navigationController?.pushViewController(imageDetailVC!, animated: true)
        
    }
    
    // 컬렉션뷰 사이즈 설정
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        // 컬렉션뷰 클릭 -> inspector에서 Min Spacing을 0으로 바꿔주면 됨. -  https://stackoverflow.com/questions/28325277/how-to-set-cell-spacing-and-uicollectionview-uicollectionviewflowlayout-size-r
        return CGSize(width: UIScreen.main.bounds.width/3-3, height: UIScreen.main.bounds.width/3) // 각 cell 3만큼 공백 줌.
    }
    

    // 컬렉션뷰 footer(인디케이터) 사이즈 설정.
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
        if self.isLoading == true || imageResult.count == BeerData.details.seeReviewMoreImageCount { // 로딩중이거나 || 서버의 모든 이미지 다 불러왔다면,
            return CGSize.zero // 로딩하면 안됨.
        } else {
            return CGSize(width: collectionView.bounds.size.width, height: 55)
        }
    }
    // footer(인디케이터) 배경색 등 상세 설정.
    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        if kind == UICollectionView.elementKindSectionFooter {
            let aFooterView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "loadingreusableviewid", for: indexPath) as! LoadingReusableView
            loadingView = aFooterView
//            loadingView?.backgroundColor = UIColor.clear
            loadingView?.backgroundColor = UIColor.mainYellow
            return aFooterView
        }
        return UICollectionReusableView()
    }
    
    // 인디케이터 로딩 애니메이션 시작. (footer appears)
    func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) {
        if elementKind == UICollectionView.elementKindSectionFooter {
            if self.isLoading {
                self.loadingView?.activityIndicator.startAnimating()
            } else {
                self.loadingView?.activityIndicator.stopAnimating()
            }
        }
    }
    // 인디케이터 로딩 애니메이션 끝. (footer disappears)
    func collectionView(_ collectionView: UICollectionView, didEndDisplayingSupplementaryView view: UICollectionReusableView, forElementOfKind elementKind: String, at indexPath: IndexPath) {
        if elementKind == UICollectionView.elementKindSectionFooter {
            self.loadingView?.activityIndicator.stopAnimating()
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        if imageResult.count == BeerData.details.seeReviewMoreImageCount { // 서버의 모든 이미지 다 불러왔다면,
            print("더 이상 로딩하면 X.") //  더이상 로딩하면 안됨.
        }
        else { // 서버의 모든 이미지를 다 불러온 게 아닌 상황이고,
            if indexPath.row == imageResult.count-1 && self.isLoading == false { // 사용자 스크롤이 마지막 index면서 로딩중이 아닐 때,
                loadMoreData()
                print("로딩 more Data.")
            }
        }
        
    }

    func loadMoreData() {
        if !self.isLoading {
            self.isLoading = true
            DispatchQueue.global().async {
                // Fake background loading task for 2 seconds
                sleep(2)
                // Download more data here
                DispatchQueue.main.async {
                    self.seeReviewMoreImageDataManager.getBeerReviewImageInfo(rowNumber: BeerData.details.rowNumber, beerId: BeerData.details.beerId, delegate: self) // 모든 이미지정보 가져오는 api 호출.
                    
//                    self.imageCollectionView.reloadData()
                    self.isLoading = false
                }
            }
        }
    }
    
}

 


오류 해결할 때 참고한 링크

https://zeddios.tistory.com/998 | https://en.proft.me/2020/07/7/implementing-pagination-uitableviewuicollection/

 

 

+ 관련있는 링크

https://blog.daymore.com/?p=336 | https://medium.nextlevelswift.com/how-to-add-loading-indicator-in-footer-of-uicollectionview-in-swift-bbd235cc6b04

반응형