전체적인 뷰 설계하기
CollectionView & CollectionView ?? 🤔
- 상단 탭바만 보면 옆으로 스와이프를 하면 화면이 넘어가는 것을 볼 수 있다. 그리고 횡스크롤이 가능해야한다.
- 탭 이동시에 indicatorbar의 넓이 및 위치가 글자에 맞게 조정된다.
여기서 처음에 든 생각은 맨아래에 CollectionView를 깔고 TabBar도 CollectionView로 만들고 TabBar를 움직일 때 Cell을 넘기는 방식으로 생각했다. 인디케이터바의 넓이 및 위치를 구현하는데도 용이하다고 판단했다. 그런데 구현 중에 모두 동일한 콜백으로 이벤트가 들어오는데 일일히 분기처리하는 것이 깔끔하지 않다고 생각했다. 아니면 더 나은 방법이 있었을지는 음
CollectionView & PageViewController !! 🙂
아래 글을 보고 아이디어를 얻었다. PageViewController를 예전에 딱 한번 쓴적은 있었지만 당시에는 뭣 모르고 썼던지라 어느새 기억에서 지워져있었는데 다시금 떠올랐다. PageViewController를 사용하면 ViewController안에 여러개의 ViewController를 Paging으로 넘길 수 있었다.
PageViewController
- 전체 뷰에서의 scroll에 대한 작업을 처리
- 이 때 탭도 이동되어야함. (상단 왼쪽 그림)
- 이동하다가 돌아왔을 때는 선택된 탭도 다시 원래대로 돌아와야함. (하단 오른쪽 그림)
CollectionView
- 탭(cell)을 눌렀을 때 탭 이동
- 이 때 Page도 이동해야함. (상단 오른쪽 그림)
- 탭에서의 횡스크롤 탐색. (하단 왼쪽 그림)
- 탭 이동시의 indicatorbar의 넓이 및 위치 조정 작업 처리 (all)
https://ios-development.tistory.com/628
로직 설계
- 가장 중요하게 가져갈 state(상태)를 targetIndex, 즉 가고자 하는 목표 탭(Page)의 index와 currentIndex 현재 있는 탭(Page)의 index 두가지로 잡았다.
- _targetIndex_는 CollectionView에, 가고자 하는 탭 액션은 컬렉션뷰에서 처리
- _currentIndex_는 PageViewController쪽에. 현재 있는 화면의 index를 가지고 있다.
- PageViewController에 세팅하는 뷰컨트롤러의 개수가 곧 탭(cell)의 개수이다. 고로 cell의 indexPath.item는 PageViewController의 index로의 효과도 가져갈 수 있다. 즉, page == cell. CollectionView와 PageViewController를 조합해서 쓰면 좋은 이점 !
처음에는 state를 cell의 isSelected를 가지고 해볼려고 했는데, 탭을 눌렀을 때 해당 탭쪽으로 탭바가 스크롤 되게끔하기 위해서 collectionView의 selectItem메서드를 사용하는데 이 것 때문에 state가 변해버려서 제대로 된 기능을 하지 않았다.
state를 정했다면 ! 특정 작업이 일어나길 원할 때 우리는 state를 바꾸고 state를 didSet을 통해 옵저빙하고 있다가 state가 바뀌
는 순간 거기에 알맞는 작업을 하게끔 구현할 계획이다.
추가로 CollectionView는 View로 따로 빼고 PageViewController가 있는 메인 뷰 위에 올릴 계획이다.
var targetIndex: Int = 0 {
didSet {
moveIndicatorbar(targetIndex: targetIndex)
}
}
해당 state가 바뀐다, 즉 무언가 우리가 액션을 해서 가고자 하는 Page의 targetIndex의 값이 새로 들어온다는 것이다.
func moveIndicatorbar(targetIndex: Int) {
let indexPath = IndexPath(item: targetIndex, section: 0)
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
guard let cell = collectionView.cellForItem(at: indexPath) as? TopTabBarCollectionViewCell else { return }
indicatorView.snp.remakeConstraints { make in
make.centerX.equalTo(cell)
make.width.equalTo(cell.getTitleFrameWidth())
make.height.equalTo(3)
make.bottom.equalTo(indicatorUnderLineView.snp.top)
}
UIView.animate(withDuration: 0.3) {
self.layoutIfNeeded()
}
}
- 현재 View로 따로 빼서 작업을 하고 있다.
탭을 눌렀을 때 해당 탭쪽으로 탭바가 스크롤 되게끔하기 위해서 selectItem메서드로 해당 탭을 선택하고 (==해당 Page로 이동) indicatorbar의 사이즈를 cell의 크기에 맞게끔 잡아주고 위치를 다시 잡아준다. 그 후 애니메이션을 통해 이동시킨다. cell의 사이즈는 delegate에서 sizeForItemAt으로 안에 있는 Label의 frame에 맞게 잡아주었다.
그렇다면 언제 state를 변경시키나?
1. 탭(cell)을 눌렀을 때
extension TopTabBarView: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let targetIndex = indexPath.item
self.targetIndex = targetIndex
delegate?.moveToTargetViewController(index: targetIndex)
}
}
- targetIndex를 눌린 cell의 index값으로 바꿔주고 delegate pattern을 통해 해당 View를 가진 ViewController로도 전달한다.
func moveToTargetViewController(index: Int) {
var direction: UIPageViewController.NavigationDirection = currentPageIndex < index
? .forward
: .reverse
pageViewController.setViewControllers([controllers[index]], direction: direction, animated: true)
self.currentPageIndex = index
}
- UIPageViewController이 있는 뷰컨에서 해당 값을 전달받아서 currentIndex와 비교한 후 가고자하는 index가 현재 index보다 크다면 앞으로(->) 스크롤, 만약 작다면 뒤로(<-) 스크롤하게끔 해준다.
- 그 뒤에 currentIndex를 새로 업데이트 해준다.
여기까지 하면 아래와 같은 결과물을 얻을 수 있다 !
이제 스크롤 시에도 동일한 효과를 얻을 수 있어야 한다.
+ 🚨수정사항 !!
위의 TVING로고를 눌렀을 때에도 홈화면의 처음으로 돌아가야했다. UIPageViewController의 setViewController를 통해서 처음 화면으로 돌아가게끔 하는 로직이 계속 반복되고 있었다. 이를 위해서 세팅해주는 함수를 따로 빼주고 currentIndex또한 didSet을 통해 값이 바뀔 때 PageVC의 페이지를 바꿔주고, 탭 바의 액션 이동은 targetIndex를 통해서 설정해준다.
private func setFirstVCinPageViewController(currIndex: Int, targetIndex: Int) {
guard targetIndex >= 0 && targetIndex < controllers.count else { return }
let direction: UIPageViewController.NavigationDirection = currIndex < targetIndex
? .forward
: .reverse
pageViewController.setViewControllers([controllers[targetIndex]], direction: direction, animated: true)
}
- PageVC의 첫번째 VC설정하는 메서드 공통화
- 기존 index와 목표 index의 크기를 비교해서 앞으로 스크롤할지 뒤로 스크롤할지 정한다.
extension MainHomeViewController: TopTabBarViewProtocol {
func moveToTargetViewController(index: Int) { // 탭바 눌렀을 때
self.currentPageIndex = index // targetIndex는 안에서 바꾸고 넘긴거라 currentIndex만
}
}
extension MainHomeViewController: NavigationBarViewProtcol {
func moveToMain() { // TVING 로고 눌렀을 때
topTabBarView.targetIndex = 0 // indicatorBar animation
self.currentPageIndex = 0 // setting first VC of PageVC
}
}
- 탭을 눌렀을 때, TVING로고를 눌렀을 때
private var currentPageIndex = 0 {
didSet {
setFirstVCinPageViewController(currIndex: oldValue, targetIndex: currentPageIndex)
}
}
- currentIndex가 바뀌면 위에서 뺀 메서드 호출
이렇게 바꾸니 각각의 state가 바뀔 때 알아서 필요한 작업을 수행하고 해당 state가 바뀌어야할 순간에 해당 state값들만 원하는 대로 바꿔주면 되는 구조로 바뀌어서 더 깔끔해진 것 같다.
2. scroll을 했을 때 !
scroll 이벤트 감지는 UIPageViewController의 delegate에서 콜백을 받을 수 있다.
우리가 활용한 메서드는 willTransitionTo와 transitionCompleted 두가지다.
willTransitionTo
- gesture가 시작된 순간 불리는 콜백 메서드이다.
- 파라미터를 보면 pendingViewControllers, transition될 예정(pending)인 view controllers들이다. 우리는 해당 배열에서 첫번째 값을 가져오면 그것이 곧 targetVC다. targetVC를 알면 배열에서 해당 값을 찾아 index를 찾을 수 있다.
- 해당 index를 가져오면 _targetIndex_를 업데이트 시켜주면 된다.
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
guard let targetVC = pendingViewControllers.first,
let targetIndex = controllers.firstIndex(of: targetVC)
else { return }
topTabBarView.targetIndex = targetIndex
}
didFinishAnimating
- gesture가 끝난 후에 불리는 메서드이다.
- 여기서는 currentIndex와 targetIndex를 비교해서 값이 다르다면 (왼쪽 그림처럼 갔다가 화면을 안넘겼다면) 탭을 다시 원래대로 돌리는 작업을 해주면 된다.
- 즉, currentIndex와 조금 전의 willTransitionTo에서 설정해주었던 targetIndex와 값이 다르다면 원래대로 돌리면 된다.
- currentIndex는 바뀌지 않았을 테고 조금 전에 다음 Index로 가기 위해 바꿨던 targetIndex를 currentIndex로 바꿔 원래대로 돌리면 된다.
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
guard let currentVC = pageViewController.viewControllers?.first,
let currentPageIndex = controllers.firstIndex(of: currentVC)
else { return }
if currentPageIndex != topTabBarView.targetIndex {
topTabBarView.targetIndex = currentPageIndex
}
}
최종 결과
'Dev > 고민과 삽질의 기록들🤔' 카테고리의 다른 글
[삽질] 클래스의 확장성 개선(protocol 의존성 주입: any) (0) | 2023.06.01 |
---|---|
[삽질] Infinite Carousel 구현해보기 + auto scrolling(Timer) (0) | 2023.05.08 |
[삽질] BottomSheet 구현 중 시행착오 + 해결과정 (PanGesture, CGAffineTransform) (1) | 2023.04.16 |
[Q&A] ViewController생성: Factory Method Pattern & DI (0) | 2023.04.15 |
[iOS] 메모리 뜯어보기 (힙할당 줄이기 및 누수 해결하기) (0) | 2023.03.11 |
댓글