iOS 프로젝트/클론

[iOS] Jetflix 9. 리팩토링 - 무한 스크롤과 TabBar 클릭

유정주 2023. 5. 27. 17:22
반응형

* 진행 코드는 https://github.com/jeongju9216/Jetflix에서 볼 수 있고, PR에서 에피소드 단위로 코드를 확인할 수 있습니다.
 

서론

이번 Jetflix 9 리팩토링은 무한 스크롤과 Tab 이벤트를 구현했습니다.
상용 앱처럼 스크롤 맨 아래까지 내려오면 다음 컨텐츠를 load하는 기능과 탭바를 누르면 맨 위로 스크롤되는 기능입니다.
 
(이번 포스팅을 마지막으로 iOS 공부는 줄이고 코딩 테스트 연습에 주력하려고 합니다.
작년부터 코딩 테스트를 보기 시작했는데 초조함, 부담감때문인지 어째 작년보다 통과율이 더 낮네요 ㅎㅎ;
그래서 iOS 공부보다는 코딩테스트 공부에 주력하려고 합니다. 
iOS 취업을 하려면 iOS 공부를 줄여야 하다니.. 아이러니하면서 속상함이 크네요.
(코테만을 위해 파이썬도 새로 시작함.. iOS 직무 코테인데 Swift 지원 안 하는 곳도 꽤 있어서...)
하고 싶은게 많았는데 잠시 미루고 코테에 집중하려고 합니다 ㅎㅎ)
 
아무튼, 이번 기능 구현은 아래와 같습니다.

  1. CollectionView 무한 스크롤
  2. TabBar Item 클릭 시 최상단으로 이동

 

1.

CollectionView의 맨 아래까지 스크롤했을 때 서버로부터 다음 컨텐츠를 load하는 기능을 구현했습니다.
UIScrollViewDelegate의 UISCrollscrollViewDidScroll(_ scrollView:)를 이용해 구현할 수 있었습니다.
스크롤뷰를 스크롤하면 호출이 되는 메서드입니다.
스크롤을 할 때 pos를 계산하여 최하단까지 스크롤했는지 판단하는 원리입니다.
 

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let pos = scrollView.contentOffset.y

    if pos > (collectionView.contentSize.height - scrollView.frame.size.height) {
        //서버 요청하기
        if !isFetchingContents {
            isFetchingContents = true
            fetchUpcomingContents()
        }
    }
}

CollectionView는 contentSize의 height이고, ScrollView는 frame size의 height를 이용합니다.
contentSize는 CollectionView에 들어가 있는 컨텐츠 길이(실제 스크롤할 수 있는 Height)를 의미합니다.
 
현재 scroll contentOffset의 y 좌표가 CollectionView의 맨 마지막 아이템이 보이는 위치일 때 새로운 아이템을 불러옵니다.
isFetchingContents는 Bool 타입 변수이고 위 로직이 한 번만 수행하도록 하는 역할입니다.
 
컨텐츠를 가져오면 ViewModel의 데이터를 바인딩해서 CollectionView에 설정합니다.

viewModel.$contents
    .receive(on: DispatchQueue.main)
    .sink { [weak self] contents in
        guard let self = self else { return }

        if !contents.isEmpty {
            self.isFetchingContents = false

            var snapshot = SnapShot()
            snapshot.appendSections([.main])
            snapshot.appendItems(contents, toSection: .main)
            dataSource.apply(snapshot, animatingDifferences: false)
        }
    }
    .store(in: &cancellables)

바인딩 위치에서 isFetchingContent를 false로 만들어줘서 로직이 다시 실행될 수 있게 합니다.
 

2.

두 번째 기능은 탭을 클릭했을 때 맨 위로 스크롤되는 기능입니다.
이 기능도 상용 앱에서 많이 봤을 것 같습니다.
저는 이게 TabBar의 기본 기능인줄 알았는데 아니라는 것에 놀랐습니다 ㅎㅎ;;
 
UITabBarController에서 ScrollView(혹은 CollectionView)의 좌표를 수정하는 원리로,
tabBarController(_:, shouldSelect:) 메서드를 이용합니다.

typealias TabItem = (vc: UINavigationController, title: String, icon: String)

private let tabItems: [TabItem] = [
    (UINavigationController(rootViewController: HomeViewController()), "Home", "house"),
    (UINavigationController(rootViewController: UpcomingViewController()), "Upcoming", "play.circle"),
    (UINavigationController(rootViewController: SearchViewController()), "Top Search", "magnifyingglass"),
    (UINavigationController(rootViewController: DownloadsViewController()), "Downloads", "arrow.down.to.line")
]
//true: 화면 전환, false: 화면 전환 필요 없음
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
    let currentIndex = tabBarController.selectedIndex
    let currentVC = tabBarController.viewControllers?[currentIndex]

    guard currentVC == viewController else {
        //현재 VC와 선택한 탭의 VC가 다르면 화면을 전환
        return true
    }

    //현재 탭을 선택하면 맨 위 Rect 위치로 scroll함
    let rootVC = tabItems[currentIndex].vc.viewControllers.last
    let scrollView = rootVC?.view.subviews.first { $0 is UIScrollView } as? UIScrollView
    scrollView?.scrollRectToVisible(CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: true)

    return false
}

먼저 클릭한 탭이 현재 탭과 동일한지 확인합니다.
현재 탭과 다르다면 클릭한 탭으로 이동합니다.
 
현재 탭과 동일하다면 맨 위로 스크롤을 해줍니다.
저는 UITabBarController에서 탭의 VC를 배열로 관리하고 있어서 그 배열을 이용해 현재 VC를 구했습니다.
그 VC에서 UIScrollView를 찾습니다.

@MainActor class UICollectionView : UIScrollView

이때 UICollectionView는 UIScrollView를 상속하고 있기 때문에 UIScrollView로 체크가 됩니다.
(https://developer.apple.com/documentation/uikit/uicollectionview)
 
scrollRectToVisible 메서드를 이용해 최상위로 스크롤해주는데요.
매개변수로 전달하는 Rect가 보이는 위치로 스크롤을 시키는 메서드입니다.
width와 height가 1인 Rect를 전달하면 좌표 (1, 1)가 보이는 위치 즉, 맨 위로 이동하게 됩니다.
 

let collectionView = rootVC?.view.subviews.first { $0 is UICollectionView } as? UICollectionView
collectionView?.selectItem(at: IndexPath(row: 0, section: 0), animated: true, scrollPosition: .top)

 
위 코드처럼 UICollectionView의 scrollToItem(at:at:animated:)를 이용하는 방법도 있는데요.
이건 최상위가 아니라 1번 아이템으로 이동하는 방법이라 상황에 맞춰 선택하시면 될 듯 합니다.
저는 화면 최상위로 올라가는 것을 원했기 때문에 UIScrollView를 이용해서 구현했어요.
 
감사합니다.


아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감 댓글 부탁드립니다.

 
 
 

반응형