SwiftUI에서 스크롤 뷰 내 컨텐츠 속도 제어하기
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 SwiftUI에서 스크롤 뷰 내 컨텐츠 속도를 상이하게 제어하는 방법에 대해 구현해보겠습니다 🙋🏻
먼저, 이와 관련하여 이전에 아래 포스팅에서 ScrollOffset을 감지하여 컨텐츠 별 상이한 스크롤 속도를 가지는것처럼 보이도록 offset을 조정하는 방법에 대해 알아본적이 있어요!
이번 포스팅은 해당 이전 포스팅에서 디벨롭된 것으로 이전 포스팅을 보지 않았다면 꼭 보고 이해하기가 수월합니다 🙋🏻
기존 포스팅에서 알아본것에는 어떤 문제가 있었는가!?
기존 학습에서는 OffsetObservableScrollView를 커스텀하게 만들고 하나의 스크롤 뷰에서 위아래 수평 스크롤이 되도록 HStack으로 컨텐츠 두개를 넣어줬어요.
그리고, 위아래 컨텐츠가 다른 width를 가질때, 스크롤 시 scrollOffset을 이용해 해당 컨텐츠의 offset을 비율로 곱하여 강제로 조정해줬습니다.
그러다보니 아래와 같은 코드가 만들어졌어요.
OffsetObservableScrollView(.horizontal, scrollOffset: $scrollOffset) { _ in
VStack(alignment: .leading, spacing: 30) {
SubView(colors: evenIndexColors)
.onReadSize { size in
evenViewWidth = size.width
}
.if(evenViewWidth < oddViewWidth) {
$0.offset(x: scrollOffset.x * ((1 - (evenViewWidth / oddViewWidth) + 0.14)))
}
SubView(colors: oddIndexColors)
.onReadSize { size in
oddViewWidth = size.width
}
}
}
.padding()
}
이 코드에서 가장 큰 문제점은 저 offset을 적용할때 저 0.14 수치를 넣어준것입니다.
즉, 매직 넘버처럼 임의적으로 알맞는 수를 넣어준거에요.
그런데, 해당 데이터들은 반복을 돌려서 표현하고 있기에 늘어날 수도 줄어들 수도 있죠.
즉, 가변적인 데이터라는 소리입니다.
그렇기에, 만약 데이터가 더 늘어나게되면 저 수치도 계산하여 변경해줘야하는 문제점이 있었습니다.
그래서, 이 수치를 강제로 넣어주는것이 아닌 계산하여 어떤 데이터가 들어와도 자연스럽게 스크롤 속도를 개선된것처럼하여 끝지점까지 맞춰보는것이 필요했습니다 🙋🏻
그걸 이번 포스팅에서 해볼꺼에요!
리팩토링 👨🏻🔧
우선 코드로 먼저 볼까요?
struct ContentView: View {
let evenIndexColors: [Color] = [.green, .yellow, .red, .blue]
let oddIndexColors: [Color] = [.red, .brown, .blue, .orange, .gray]
@State var scrollOffset: CGPoint = .zero
@State private var evenIndexViewSize: CGFloat = .zero
@State private var oddIndexViewSize: CGFloat = .zero
@State private var evenXOffset: CGFloat = .zero
@State private var oddXOffset: CGFloat = .zero
var body: some View {
OffsetObservableScrollView(.horizontal, scrollOffset: $scrollOffset) { _ in
VStack(alignment: .leading, spacing: 30) {
SubView(colors: evenIndexColors)
.onReadSize { size in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
evenIndexViewSize = size.width
}
}
.offset(x: evenXOffset)
SubView(colors: oddIndexColors)
.onReadSize { size in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
oddIndexViewSize = size.width
}
}
.offset(x: oddXOffset)
}
}
.padding()
.onChange(of: scrollOffset) { scrollOffset in
let scrollableWidthEven = evenIndexViewSize - UIScreen.main.bounds.width
let scrollableWidthOdd = oddIndexViewSize - UIScreen.main.bounds.width
if evenIndexViewSize < oddIndexViewSize {
let scrollableRate = scrollableWidthEven / scrollableWidthOdd
evenXOffset = scrollOffset.x * (1 - scrollableRate)
} else if oddIndexViewSize > evenIndexViewSize {
let scrollableRate = scrollableWidthOdd / scrollableWidthEven
oddXOffset = scrollOffset.x * (1 - scrollableRate)
}
}
}
}
여기서 핵심이 되는 코드는 onChange를 이용하는것이에요.
하나의 스크롤 뷰에 HStack 두개가 있고 해당 스택은 데이터 양에 따라서 서로 상대적으로 한쪽은 빠르게 한쪽은 느리게 offset이 변화해야 되는것입니다.
그렇기에 scrollOffset 값을 감지하여 로직을 구성해야하죠.
여기서 해당 HStack들의 width 사이즈를 구한 후 스크린의 width만큼 빼주고 있어요.
만약 이 수치를 빼주지 않으면 어떻게 될까요?
보시는것처럼 스크롤 속도는 차이가 나지만, 끝나는 지점이 맞아떨어지지 않습니다.
그렇기에 스크롤 가능한 너비를 계산하여 주기 위해 컨텐츠의 너비에서 화면의 가로 너비를 제외한 부분을 고려해 값을 도출해야 하는것입니다.
그리고 그 너비를 큰쪽을 기준으로 비율로 나누고 scrollOffset의 x 좌표에 따라 수치를 곱해줍니다.
이때, 곱해주는 비율에 1을 빼주는것은 수치가 클수록 느리게 이동하기에 역으로 계산해주는것입니다.
왜 그런지는 확실하게 밝혀내지 못했지만, 곱한 비율이 클수록 사실 빠르게 offset이 더 크게 넘어가야되는데 반대더라구요.
심지어, 곱해주는 비율이 1을 넘어가면 역 스크롤링 현상이 발생합니다.
이런식으로 말이죠 🥲
그렇기에, 여러번의 테스트 결과 이 수치로 계산한다면 어떤 데이터가 들어오고 너비가 계산되더라도 정상적입니다.
위 아래 컨텐츠의 데이터를 예측할 수 없기에 둘 다 큰쪽이라고 가정할때를 고려해 분기 처리하여 offSet을 계산해주는 로직입니다.
이렇게 코드를 구성한다면 원하던 바를 얻어낼 수 있습니다!
한번 돌려볼까요?
컬러가 몇개 없어서 확실히 차이를 모를 수 있으니 많이 넣어볼께요!
이제 확실히 차이가 느껴지죠?
원하는 하나의 스크롤에서 서로 다른 컨텐츠의 offset을 조정해 스크롤 속도가 상이한것처럼 나오고 또 스크롤 가능한 영역까지만 컨트롤해 컨텐츠의 끝나는 지점을 맞출 수 있습니다!
이전에 방정식이니 뭐니 이 수치를 구하려고 삽질을 많이 했었는데.....
스크롤 가능한 영역의 수치만 구하면 되는 구현이였습니다.
덕분에, 5차 방정식까지 한번 공부해본 😇
마무리
진짜 이런거 편하게 사용되게 누가 안만들어주나..?ㅎㅎ
오픈소스로 만들어볼까 생각중입니다.