iOS ) Sticky Header 구현하기

영모·2022년 4월 27일
5

UI 미리보기

💻 개발 정리

# 0 구현 방법

  • ScrollView를 ⬆️ 방향으로 스크롤했을 때
    1. stickyHeaderView를 위로 올라가면서 && 투명하게 만듭니다.
    2. scrollView도 위로 올라가게 합니다.
    3. headerView를 보이게 합니다.
  • ScrollView를 ⬇️ 방향으로 스크롤 했을때
    1. stickyHeaderView를 아래로 내리면서 && 보이게 만듭니다.
    2. scrollView도 아래로 내려가게 합니다.
    3. headerView를 투명하게 만듭니다.

기본적인 구현 방식은 다음과 같습니다.

  • 위, 아래로 올라가게 만들기
    1. NSLayoutConstraint로 topAnchor를 저장합니다.
    2. 스크롤 되었을 때 constant 값의 변화를 줍니다.
  • 투명하게, 보이게 만들기
    1. alpha 값에 변화를 줍니다.

고민을 많이 했던 부분은
스티키 헤더뷰가 올라갈때 스크롤 뷰도 같이 올라가는 것을 구현하는 부분이었습니다. 둘의 올라가는 속도가 달랐을때 이를 스케일링하여 구현하였습니다.

# 1 Delegate상속 그리고 View와 Constraint 생성

UIScrollViewDelegate 상속 받아서 scrollView의 스크롤 이벤트 처리를 위임받을 수 있도록 합니다.
사용된 UIView는 postStickyHeaderView, postHeaderView, scrollView, postTableView 입니다.


class PostViewController: UIViewController, UIScrollViewDelegate {
	
	...
    
    var postStickyHeaderView = PostStickyHeaderView()
    var postHeaderView = PostHeaderView()
    var scrollView = UIScrollView()
    var postTableView = UITableView()
        .then {
            $0.register(PostTableViewCell.self, forCellReuseIdentifier: PostTableViewCell.identifier)
            $0.separatorStyle = .none
            $0.isScrollEnabled = false
            $0.showsVerticalScrollIndicator = false
        }
        
    var postStickyHeaderViewTopConstraint = NSLayoutConstraint()
    var scrollViewTopConstraint = NSLayoutConstraint()
    var postTableViewHeightConstraint = NSLayoutConstraint()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        scrollView.delegate = self
    }
    
    ...
}

# 2 ScrollView 안에 TableView 넣기

UITableView의 스크롤 방지는 #1 에서 구현 했습니다.
UITableView의 높이를 컨텐츠 사이즈로 설정하여 높이가 화면밖으로 튀어나갈 수 있도록 설정해 주어야 합니다.

func setView() {
	// ... 생략
	scrollView.addSubview(postTableView)

	postTableView.translatesAutoresizingMaskIntoConstraints = false
    postTableViewHeightConstraint = postTableView.heightAnchor.constraint(equalToConstant: 0)
	
    NSLayoutConstraint.activate([
    	scrollViewTopConstraint,
		scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
		scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
		scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),

		postTableView.topAnchor.constraint(equalTo: scrollView.topAnchor),
		postTableView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
		postTableView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
		postTableViewHeightConstraint,
	])
}

override func viewWillLayoutSubviews() {
	super.viewWillLayoutSubviews()
	postTableViewHeightConstraint.constant = postTableView.contentSize.height
}

viewWillLayoutSubviews를 오버라이딩 하여 TableView의 높이를 직접 설정해주었습니다. 이렇게 하면 ScrollView안에 TableView가 들어갑니다.

[?] UITableView는 내부적으로 UIScrollView를 상속 받고, 위 과정은 UITableView의 Scroll 기능을 해제하는 과정입니다.

# 3 AutoLayout 설정

스티키 헤더 뷰의 TopAnchor을 잡아주고 스크롤 뷰의 TopAnchor를 잡아줍니다.
2개의 뷰의 변화만 있을 예정입니다.

func setView() {
	// ... 생략
	view.addSubview(postStickyHeaderView)
    view.addSubview(postHeaderView)
    view.addSubview(scrollView)

	postStickyHeaderView.translatesAutoresizingMaskIntoConstraints = false
	postHeaderView.translatesAutoresizingMaskIntoConstraints = false
    
    postStickyHeaderViewTopConstraint = postStickyHeaderView.topAnchor.constraint(equalTo: view.topAnchor)
    scrollViewTopConstraint = scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: Const.Size.postHeaderMaxHeight)
	
    NSLayoutConstraint.activate([
    	postStickyHeaderViewTopConstraint,
        postStickyHeaderView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
		postStickyHeaderView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
		postStickyHeaderView.heightAnchor.constraint(equalToConstant: Const.Size.postHeaderMaxHeight),
            
		postHeaderView.topAnchor.constraint(equalTo: view.topAnchor),
		postHeaderView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
		postHeaderView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
		postHeaderView.heightAnchor.constraint(equalToConstant: Const.Size.postHeaderMinHeight),
            
		scrollViewTopConstraint,
		scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
		scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
		scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
	])
}

# 4 스티키 헤더 뷰 구현

delegate로 상속받은 후 scrollViewDidScroll를 구현해 줍니다.

func scrollViewDidScroll(_ scrollView: UIScrollView) {
	var headerConstant = scrollView.contentOffset.y
	var tableConstant = CGFloat()
        
	headerConstant = headerConstant < 0 ? 0 : headerConstant
	headerConstant = headerConstant > Const.Size.postHeaderMinHeight ? Const.Size.postHeaderMinHeight : headerConstant
	tableConstant = Const.Size.postHeaderMaxHeight - ((headerConstant - 0) / (Const.Size.postHeaderMinHeight)) * (Const.Size.postHeaderMaxHeight - Const.Size.postHeaderMinHeight)
        
	postStickyHeaderViewTopConstraint.constant = -headerConstant
	scrollViewTopConstraint.constant = tableConstant
	postStickyHeaderView.alpha = 1 - headerConstant / Const.Size.postHeaderMinHeight
	postHeaderView.titleView.alpha = headerConstant / Const.Size.postHeaderMinHeight
	postHeaderView.rightLabel.alpha = 1
}
  • 스티키 헤더 뷰
    1. scrollView.contentOffset.y 를 받아서 headerConstant 에 저장하고 0보다 작으면 (처음부터 ⬇️ 방향으로 스크롤) 0으로 바꿉니다. 왜냐하면 ⬇️ 방향으로 스크롤 하더라도 고정된 TopAnchor을 갖도록 하기 위함입니다.
    2. postHeaderMinHeight 보다 크면 postHeaderMinHeight으로 바꿉니다. 왜냐하면 ⬆️ 방향으로 스크롤 할때 너무 많이 스크롤 해서 y값이 엄청 커져도 고정된 TopAnchor을 갖도록 하기 위함입니다.
    3. postStickyHeaderViewTopConstraint의 constant 값을 headerConstant 값에 -을 붙인 값으로 수정합니다.
    4. 이렇게 하면 postHeaderView는 y값에 따라서 스크롤이 구현됩니다.
  • 스크롤 뷰
    • 스티키 헤더뷰의 constant값은 0 ↔️ (-1 * postHeaderMinHeight) 사이 입니다.
    • 스크롤 뷰는 postHeaderMaxHeight ↔️ postHeaderMinHeight 사이 입니다.
    1. headerConstant을 0 ~ 1 사이로 변환시키고 이를 다시 0 ~ (postHeaderMaxHeight - postHeaderMinHeight)로 변환해줍니다.
    2. 그리고 postHeaderMaxHeight(초기값)에서 빼줍니다.

[?] 스티키 헤더 뷰가 올라가면 스크롤 뷰도 따라서 올라가게 해야합니다. 하지만, 완전 끝까지 올라가면 안되고 postHeaderMinHeight 만큼 상단과의 여유를 두어야 합니다. 스티키 헤더뷰는 끝까지 다 올라가지만, 스크롤 뷰는 최소 여유가 존재하므로 둘의 속도가 다르고 이를 스케일링 하는 과정을 해주었습니다. 검색키워드는 정규화 Scaling 입니다.

스티키 헤더 뷰가 10 올라가면 스크롤 뷰는 7정도 올라간다고 생각하면 됩니다.

profile
iOS Developer

1개의 댓글

comment-user-thumbnail
2023년 1월 12일

GitHub link for the finished project?

답글 달기