Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- uiscrollview
- swift documentation
- 스위프트
- combine
- UICollectionView
- ios
- clean architecture
- Observable
- map
- Human interface guide
- 리팩토링
- tableView
- Clean Code
- collectionview
- 리펙토링
- 리펙터링
- Xcode
- MVVM
- ribs
- Protocol
- 애니메이션
- rxswift
- uitableview
- 클린 코드
- HIG
- UITextView
- swiftUI
- SWIFT
- RxCocoa
- Refactoring
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] 2. 상단 탭과 하단 수평 스크롤 UI 구현 방법 - 하단 수평 스크롤 콘텐츠 구현 (UIPageViewController) 본문
UI 컴포넌트 (swift)
[iOS - swift] 2. 상단 탭과 하단 수평 스크롤 UI 구현 방법 - 하단 수평 스크롤 콘텐츠 구현 (UIPageViewController)
jake-kim 2023. 1. 17. 23:151. 상단 탭과 하단 수평 스크롤 UI 구현 방법 - 상단 TabHeaderView 구현 (UICollectionView)
2. 상단 탭과 하단 수평 스크롤 UI 구현 방법 - 하단 콘텐츠 구현 (UIPageViewController) <
구현 아이디어
* 1편 포스팅 글(상단 TabHeaderView 구현)에서 설명한 내용과 동일
- 상단 스크롤은 collectionView를 사용하여 구현 (scrollView의 scrollDirection = .horizontal 사용)
- UIScrollView도 사용할 수 있지만, 동일한 화면이 반복적으로 필요하고, 어떤 셀이 선택되었는지 쉽게 구하려면 UICollectionView를 사용하는 것이 편리
- 하단 콘텐츠는 pageViewController를 사용하여 왼쪽, 오른쪽으로 ViewController들을 넘길 수 있도록 구현
- 상단 스크롤, 하단 콘텐츠 모두 클라이언트 코드 쪽 한 곳에서 관리하도록 구현
- 상태 관리를 여러곳에서 하지 않고 한 곳에서 해야, 상태가 변경되었을때 어디서 변경되는지 쉽게 확인할 수 있고 유지보수에 용이히기 때문
- 뷰 상태 관리(isSelected 등)도 모두 클라이언트 코드에게 위임하도록 구현
PageViewContoller 사용하여 구현
* 구현 내용 코드 다운로드 링크
- PageViewController를 사용하여 구현할것인데, PageViewControllers는 상단 tabHeader의 하단 콘텐츠이므로, 이 곳에는 다른 콘텐츠도 추가되고 스크롤 될 가능성이 높으므로 UIScrollView로 wrapping하여 구현
// ViewController.swift
// MARK: UI
private let tabHeaderView = TabHeaderView()
private let scrollView = UIScrollView()
private let stackView = UIStackView().then {
$0.axis = .vertical
}
private let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
private func setUpLayouts() {
view.addSubviews(
tabHeaderView,
scrollView
)
scrollView.addSubviews(
stackView
)
stackView.addArrangedSubviews(
pageViewController.view
)
tabHeaderView.snp.makeConstraints {
$0.top.equalTo(view.safeAreaLayoutGuide)
$0.left.right.equalToSuperview().inset(Metric.headerViewHorizontalInset)
$0.height.equalTo(Metric.headerHeight)
}
scrollView.snp.makeConstraints {
$0.top.equalTo(tabHeaderView.snp.bottom)
$0.left.right.equalToSuperview().inset(Metric.horizontalInset)
$0.bottom.equalToSuperview()
}
stackView.snp.makeConstraints {
$0.edges.width.equalToSuperview()
}
// height 지정 필수
pageViewController.view.snp.makeConstraints {
$0.height.equalTo(Metric.pageHeight)
}
}
- 페이지 뷰 컨트롤러에 들어갈 프로퍼티 선언
- 따로 전역으로 빼놓는 이유는 pageViewController가 스크롤 직전에 index값을 델리게이트에서 얻을 수 있는데, 이 값을 가지고 어떤 ViewController를 리턴해줄것인지 필요하기 때문
fileprivate var contentViewControllers = [UIViewController]()
- 이 프로퍼티에 샘플로 사용할 ViewController 셋팅
private var items = ["1", "jake", "iOS 앱 개발 알아가기", "2", "jake123"]
.enumerated()
.map { index, str in HeaderItemType(title: str, isSelected: index == 0) }
private func setViewControllers() {
items
.map(\.title)
.forEach { title in
let vc = LabelViewController() // 단순히 UILabel을 가지고 있는 VC
vc.titleText = title
contentViewControllers.append(vc)
}
}
- pageViewController 델리게이트, 데이터소스, 뷰 셋팅
private func setUpViews() {
pageViewController.dataSource = self
pageViewController.delegate = self
addChild(pageViewController)
pageViewController.didMove(toParent: self)
pageViewController.setViewControllers([contentViewControllers[0]], direction: .forward, animated: false)
}
- 페이지 dataSource
- 스크롤 직전에 호출되는 메소드에서 어떤 ViewController를 보여줄지 결정
extension ViewController: UIPageViewControllerDataSource {
// left -> right 스와이프 하기 직전 호출 (다음 화면은 무엇인지 리턴)
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController
) -> UIViewController? {
guard let index = contentViewControllers.firstIndex(of: viewController) else { return nil }
let previousIndex = index - 1
guard previousIndex >= 0 else { return nil }
return contentViewControllers[previousIndex]
}
// right -> left 스와이프 하기 직전 호출 (이전 화면은 무엇인지 리턴)
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController
) -> UIViewController? {
guard let index = contentViewControllers.firstIndex(of: viewController) else { return nil }
let nextIndex = index + 1
guard nextIndex < contentViewControllers.count else { return nil }
return contentViewControllers[nextIndex]
}
}
- 페이지 delegate
- 페이지가 변하면 상단의 tabHeaderCell도 업데이트 되어야 하므로 싱크 작업
extension ViewController: UIPageViewControllerDelegate {
func pageViewController(
_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool
) {
guard completed else { return }
updateTabIndex()
}
private func updateTabIndex() {
guard
let vc = (pageViewController.viewControllers?.first as? LabelViewController),
let id = vc.id,
let currentIndex = items.firstIndex(where: { id == $0.title })
else { return }
updateTapHeaderCell(currentIndex)
}
}
(완료)
'UI 컴포넌트 (swift)' 카테고리의 다른 글
Comments