[SwiftUI] Scroll View

Page·2022년 8월 23일
2

SwiftUI

목록 보기
17/18
post-thumbnail

Apple Document

스크롤뷰가 무엇인지, 애플 문서부터 보고 시작하자.

Scroll View 는 content를 스크롤 가능한 영역 안에 보여주는 기능을 한다. 사용자가 플랫폼에 적합한 스크롤 제스쳐를 하면, Scroll View는 기본 content의 일부분을 보여주기위해 표시될 부분을 조정한다. 가로, 세로, 양방향으로 모두 스크롤 가능하지만 줌 기능은 제공하지 않는다.

progamaatic 스크롤을 위해서 ScrollViewReader 안에 한개 이상의 Scroll View를 넣을 수 있다.

Scroll View의 구성 요소는 3가지다.

var content: Content 
var axes: Axis.Set 
var showIndicatios: Bool 
  1. content : 스크롤 뷰가 보여줄 content
  2. axes: 스크롤 가능한 방향. 기본값은 Axis.vertical(세로축)
  3. showIndicators : 플랫폼에 적합한 방법으로, content의 offset의 표시 유무. 기본 값은 true

Bounce 조정하기

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가지를 이용했다.

  1. onApeear, onDisappear 이용하기
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가 종료될 때 돌아가고 싶은 기본값을 설정해준다.

  1. Introspect Package 이용하기

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를 따로 분리해줬다.

0개의 댓글