스크롤뷰가 무엇인지, 애플 문서부터 보고 시작하자.
Scroll View
는 content를 스크롤 가능한 영역 안에 보여주는 기능을 한다. 사용자가 플랫폼에 적합한 스크롤 제스쳐를 하면, Scroll View는 기본 content의 일부분을 보여주기위해 표시될 부분을 조정한다. 가로, 세로, 양방향으로 모두 스크롤 가능하지만 줌 기능은 제공하지 않는다.
progamaatic 스크롤을 위해서 ScrollViewReader
안에 한개 이상의 Scroll View를 넣을 수 있다.
Scroll View의 구성 요소는 3가지다.
var content: Content
var axes: Axis.Set
var showIndicatios: Bool
ScrollView를 다루면서 처음 시도해본 부분이다. 화면의 최 상단에는 이미지가 있는 UI를 구성해야하는데 동작해보니까 상단으로 스크롤했을 때 Bounce되는 것이 거슬렸다. 다른 어플들을 이용해보니 이미지 위로는 Bounce되지 않게 하는게 일반적인 것 같았다.
struct BounceControllScrollView: View {
init() {
UIScrollView.appearance().bounces = false
}
var body: some View {
ScrollView {
ForEach(0..<5, id: \.self) { _ in
Image(systemName: "applelogo")
.resizable()
.frame(width: 300, height: 300)
}
}
}
}
Bounce를 모두 막으려면 위처럼 해주면 되는데, 이렇게 해준다면 다른 뷰에 있는 모든 View의 스크롤에도 Bounce가 동작하지 않는다.
이걸 해결하기 위한 방법으로 2가지를 이용했다.
struct BounceControllScrollView: View {
var body: some View {
ScrollView {
ForEach(0..<5, id: \.self) { _ in
Image(systemName: "applelogo")
.resizable()
.frame(width: 300, height: 300)
}
}
.onAppear {
UIScrollView.appearance().bounces = false
}
.onDisappear {
UIScrollView.appearance().bounces = true
}
}
}
View가 종료될 때 돌아가고 싶은 기본값을 설정해준다.
https://github.com/siteline/SwiftUI-Introspect
위에 링크의 패키지를 이용했다.
import SwiftUI
import Introspect
struct BounceControllScrollView: View {
var body: some View {
ScrollView {
ForEach(0..<5, id: \.self) { _ in
Image(systemName: "applelogo")
.resizable()
.frame(width: 300, height: 300)
}
}
.introspectScrollView { uiScrollView in
uiScrollView.bounces = false
}
}
}
이렇게 해주면 해당 뷰에서만 스크롤뷰가 bounce되지 않도록 할 수 있다.
이제 이 뷰에서만 설정이 적용되도록 했으니 원래 하려던 bounce 효과 제어를 해보자.
import SwiftUI
import Introspect
struct BounceControllScrollView: View {
@State private var contentMinY: CGFloat = 0
@State private var translationWidth: CGFloat = 0
var body: some View {
ScrollView(.vertical) {
VStack {
ForEach(0..<6, id: \.self) { _ in
Image(systemName: "applelogo")
.resizable()
.frame(width: 300, height: 300)
// 아래 코드가 없으면 정상 동작하지 않음.
.foregroundColor((contentMinY < 0) ? Color.green : Color.yellow)
}
}
.background(GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self, value: $0.frame(in: .global).minY)
})
.onPreferenceChange(ViewOffsetKey.self) {
contentMinY = $0
}
}
.introspectScrollView { uiScrollView in
uiScrollView.bounces = contentMinY < 0
}
}
}
PreferenceKey를 이용해서 ScrollView 내부에 있는 content의 offset을 구한다. 이 값이 0이하일 때만 bounce를 해줄 것이다. PreferenceKey 값은 contentMinY
에 저장해주고 이 값으로 bounce를 제어한다.
그런데 전혀 무관한 것 같은 코드를 추가해주고서야 정상적으로 동작했다. 원인은 못 찾았다.
실제로는 조금 바꾼 방식으로 이용하는데 거기서는 문제가 없었다.
Bounce를 제어하려는 모든 Srollview에 위 코드를 모두 적용하기는 귀찮으니 조금 더 수정해보자.
struct BounceControllScrollView<Content: View>: View {
var content: () -> Content
@State private var contentMinY: CGFloat = 0
var body: some View {
ScrollView(.vertical) {
content()
.foregroundColor(contentMinY > 0 ? Color.black : Color.black)
.background(GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self, value: $0.frame(in: .global).minY)
})
.onPreferenceChange(ViewOffsetKey.self) {
contentMinY = $0
}
}
.introspectScrollView { uiScrollView in
uiScrollView.bounces = contentMinY < 0
}
}
}
struct BounceControllScrollViewTester: View {
var body: some View {
BounceControllScrollView {
VStack {
ForEach(0..<6, id : \.self) { _ in
Image(systemName: "applelogo")
.resizable()
.frame(width: 300, height: 300)
}
}
}
}
}
구현은 완전히 동일한데 BounceControllScrollView
를 따로 분리해줬다.