티스토리 뷰

SwiftUI에서 처음 만들었던 예제인 StickyHeader를 UIKit을 활용해서 만들어보자. 이전 포스팅에서 MVC 패턴으로 나누면서 controller 부분을 넘어갔다. 지금까지는 사용자의 입력에 따라 view가 바뀌는 부분이 없었기 때문에 view와 controller를 분리해서 생각할 부분이 없었는데 이번에 StickyHeader를 만들면서 둘을 분리해서 생각해 볼 수 있을 것이다.

 

 

 

Header constraints

지금까지 만든 화면을 다시 보면 가장 아래에 UIView부터 UIScrollView, UILabel 등을 추가해서 만들었다. 이 상태로 실행해보면 header가 scroll 상단에 붙어있기는 하지만 아래로 스크롤 할 때 같이 스크롤 된다. sticky header는 아래로 스크롤 할 때 header의 top이 화면 위에 붙어서 높이가 동적으로 조절되는 것을 말하는데, 지금은 height를 고정하고 있기 때문에 scroll의 contentView가 움직이는 것에 따라서 header도 같이 움직인다.

 

위쪽이 끈끈하게 붙어있는 것 처럼(Sticky) 보여야 한다.

 

constraint는 view의 위치와 크기를 정하는 규칙이라고 했다. 그렇다면 header의 위치와 크기가 동적으로 변해야 한다면 이 규칙을 그에 따라 바꿔주면 될 것 같다. 위 아래로 움직이는 scroll 이기 때문에 top, height 규칙을 상황에 맞게 바꿔주면 좋을 것 같다.

widthAnchor의 경우 scrollView.widthAnchor와 같게 설정하면 scrollView의 크기가 변할 때 width가 동적으로 변한다. 이걸 보고 height나 topAnchor도 처음 설정할 때 closure같은 걸 넣어서 설정하면 자동으로 레이아웃이 되도록 만들고 싶었지만, constant의 경우 처음 실행할 때 상수로 입력되고, 그 값을 계속 사용하기 때문에 동적으로 바꾸려면 실제로 값을 바꿔줘야 했다.

 

// what I wanted.
scrollHeader.topAnchor(equalTo: scrollView.topAnchor, constant: { in
	scrollView.contentOffset.y
})
// constant is CGFloat not () -> CGFloat

 

어쩔 수 없이 변수에 저장하기로 했다. 그리고 이 Layout의 constant를 실시간으로 변하도록 만들어 볼 것이다.

 

var headerTopAnchor = scrollHeader.topAnchor(equalTo: scrollView.topAnchor, constant: 0)
headerTop.isActive = true
var headerHeightAnchor = heightAnchor.constraint(equalToConstant: 100)
headerHeight.isActive

// when scrolling...
// headerTop.constant = newValue
// headerHeight.constant = newValue

 

 

 

UIScrollViewDelegate

UIKit은 event driven 방식으로 만들어져 있기 때문에 모든 view에서 발생하는 이벤트는 delegate라는 대리자를 통해 관리할 수 있다. 예를 들어 UIScrollView의 경우 스크롤이 시작했는지, 스크롤이 끝났는지, 스크롤 중인지 등에 대한 이벤트가 있다. 대리자를 사용하기 위해 현재 ViewController가 UIScrollViewDelegate를 상속 받도록 만들고, scrollView의 delegate를 ViewController로 설정한다.

 

class MainViewController : UIViewController, UIScrollViewDelegate {
	// ...

	func viewDidLoad() {
		scrollView.delegate = self

		// ...
	}
}

 

이제 UIScrollView의 이벤트를 처리할 수 있게 되었다. sticky header는 사용자가 스크롤을 할 때 header의 크기를 동적으로 변경해줘야 하기 때문에 scrollViewDidScroll 이라는 함수를 재정의한다. 이 함수는 실제로 UIScrollView 내부의 contentView가 움직일 때 매 프레임 호출되는 이벤트로 이곳에서 header의 위치와 크기를 업데이트할 수 있다.

 

 

 

contentOffset

UIScrollView는 content를 가진다고 했다. 그리고 이 내용을 스크롤하면서 이리저리 움직일 수 있는데, 이 위치가 바로 contentOffset이라고 한다. scrollViewDidScroll에서 이 값(세로로 스크롤 하기 때문에 contentOffset.y를 출력)을 출력해보면 스크롤 할 때 마다 바뀌는 것을 볼 수 있다. navigation을 추가하거나 상단에 위치한 다른 view에 따라 값이 조금씩 달라질 수 있지만 그냥 바로 view에 scrollView만 추가하면 기본값이 -20일 것이다.

 

func scrollViewDidScroll(_ scrollView: UIScrollView) {
	print(scrollView.contentOffset.y)    // -20
}

 

왜 0이 아닐까? 화면 위에 시간이나 배터리 아이콘 등이 있는 바가 있어서 자동으로 adjustedContentInset 이라는 값이 설정되기 때문이다. 여기서 contentInset이 등장한다.

 

offset은 content 안에서 scrollView의 위치를 말한다.

 

근데 contentInset을 출력해 봐도 다 0 이다. 당연히 아무 설정도 해주지 않았기 때문에 0 일 수 밖에 없는데 그러면 offset은 왜 달라진걸까?

IOS11부터 adjustedContentInset 이라는 값이 추가되었다고 한다. contentInset이 개발자가 임의로 넣을 수 있는 공백이라면, adjustedContentInset은 XCode가 넣어주는 공백이다. 그래서 수정할 수도 없고 get만 가능하다. 위쪽에 상태바가 있기 때문에 그 높이만큼 top Inset으로 20이 설정 해주는 것이다.

 

func scrollViewDidScroll(_ scrollView: UIScrollView) {
	print(scrollView.adjustedContentInset.top)    // 20
	print(scrollView.contentOffset.y)    // -20
}

 

adjustedContentInset값은 수정할 수 없기 때문에 이 값을 가지고 보정하거나 offset을 무시할 수 있는데, 무시하면 상태바와 scrollHeader가 겹치기 때문에 이 값으로 보정하는 방법을 써보자. header의 높이도 변수로 빼서 heightAnchor와 topAnchor를 계산했다.

 

func scrollViewDidScroll(_ scrollView: UIScrollView) {
	headerHeightAnchor.constant = scrollView.contentOffset.y < -scrollView.adjustedContentInset.top ? headerHeight + radius - scrollView.contentOffset.y : headerHeight + radius + scrollView.adjustedContentInset.top      
	headerTopAnchor.constant = scrollView.contentOffset.y < -scrollView.adjustedContentInset.top ? scrollView.contentOffset.y : -scrollView.adjustedContentInset.top
}

 

이제 스크롤을 할 때 마다 scrollHeader의 높이와 위치가 적절하게 조절이 되면서 화면 상단에 붙어있는 것 처럼 보이게 된다.

 

 

 

Code

드디어 코드가 어느정도 정리가 돼서 공유를 해도 될 것 같다. 처음 하다보니 이것저것 시도해보느라 포스팅 내용이랑 안맞아서 공유할 수 없었는데, 이번 포스팅을 기점으로 필요없는 코드를 대부분 쳐내고 필요한 부분만 남겼다.

 

https://github.com/eastroot1590/TodoChallenge/releases/tag/StickyHeader

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
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
글 보관함