개발을 하다보면 아래와 같이 스크롤의 상태, 스크롤된 정도를 추적하고 싶을 때가 있다. Swift 개발을 하다보면 NavigationTitle 의 경우가 이렇게 스크롤을 추적하여 상단 툴바에 Title 을 표시해준다.
다양한 인터렉션을 위해서 스크롤의 정도를 추적하고 싶다면... 세가지 방법을 소개하겠다 :)
1. PreferenceKey 와 View extension 활용
아래와 같은 코드를 작성해주면 된다. PreferenceKey 는 하위뷰에서 상위뷰로 값을 전달해주고 싶을 때 사용하게 된다.
import SwiftUI
struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGPoint = .zero
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {
value = nextValue()
}
}
extension View {
func onScrollOffsetChanged(action: @escaping (_ offset: CGPoint) -> Void) -> some View {
self
.background(GeometryReader { geometry in
Color.clear
.preference (
key: ScrollOffsetPreferenceKey.self,
value: geometry.frame(in: .global).origin
)
})
.onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: action)
}
}
뷰에서 사용방법은 아래와 같이 사용 할 수 있다.
import SwiftUI
struct ContentView: View {
@State private var offset: CGPoint = .zero
var body: some View {
ScrollView {
Color(.systemGray3)
.frame(height: 200)
.onScrollOffsetChanged { offset in
self.offset = offset
}
.overlay {
Text("\(offset.y)")
}
}
}
}
이슈
단점이라고 볼 수 있는데... 59.0 부터 값이 시작한다는 것이다. 상단의 SafeArea 의 높이가 59 이기 때문에 발생한다. 하지만 모든 기기에서 동일하게 59.0 부터 시작되기 때문에 -59 를 해줘서 보정을 해주면 될 듯하다.
해결방법
coordinateSpaceName 을 지정해준다면 0부터 시작이 된다.
import SwiftUI
struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGPoint = .zero
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {
value = nextValue()
}
}
extension View {
func onScrollOffsetChanged(name: UUID, action: @escaping (_ offset: CGPoint) -> Void) -> some View {
self
.background(GeometryReader { geometry in
Color.clear
.preference (
key: ScrollOffsetPreferenceKey.self,
value: geometry.frame(in: .named(name)).origin
)
})
.onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: action)
}
}
struct ContentView: View {
@State private var offset: CGPoint = .zero
private let coordinateSpaceName = UUID()
var body: some View {
ScrollView {
Color(.systemGray3)
.frame(height: 200)
.onScrollOffsetChanged(name: coordinateSpaceName) { offset in
self.offset = offset
}
.overlay {
Text("\(offset.y)")
}
}
.coordinateSpace(name: coordinateSpaceName)
}
}
2. PreferenceKey 활용하여 CustomView 만들기
위에서 사용한 것을 응용해서 아래와 같은 나만의 OffsetObservingScrollView 를 만들 수 있다.
import SwiftUI
struct PositionObservingView<Content: View>: View {
var coordinateSpace: CoordinateSpace
@Binding var position: CGPoint
@ViewBuilder var content: () -> Content
var body: some View {
content()
.background(GeometryReader { geometry in
Color.clear.preference(
key: PreferenceKey.self,
value: geometry.frame(in: coordinateSpace).origin
)
})
.onPreferenceChange(PreferenceKey.self) { position in
self.position = position
}
}
}
private extension PositionObservingView {
struct PreferenceKey: SwiftUI.PreferenceKey {
static var defaultValue: CGPoint { .zero }
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) { }
}
}
struct OffsetObservingScrollView<Content: View>: View {
var axes: Axis.Set = [.vertical]
var showsIndicators = true
@Binding var offset: CGPoint
@ViewBuilder var content: () -> Content
private let coordinateSpaceName = UUID()
var body: some View {
ScrollView(axes, showsIndicators: showsIndicators) {
PositionObservingView(
coordinateSpace: .named(coordinateSpaceName),
position: $offset,
content: content
)
}
.coordinateSpace(name: coordinateSpaceName)
}
}
사용하는 방법은 좀더 간단하다. 아래와 같이 ScrollView 처럼 써주고 offset 만 전달해주면 된다.
import SwiftUI
struct ContentView: View {
@State private var offset: CGPoint = .zero
private let coordinateSpaceName = UUID()
var body: some View {
OffsetObservingScrollView(offset: $offset) {
Color(.systemGray3)
.frame(height: 200)
.overlay {
Text("\(offset.y)")
}
}
}
}
3. Binding 만 사용하여 CustomView 만들기
사실 의문점이 생겼다. 굳이 PreferenceKey 를 만들어야 할까? @Binding 만으로 전달하여 offset 을 추적할 수 있지 않을까? 아래와 같이 만들어 주면 된다.
import SwiftUI
struct ObservingScrollView<Content: View>: View {
var axes: Axis.Set = [.vertical]
var showsIndicators = true
@Binding var offset: CGPoint
var content: () -> Content
private let coordinateSpaceName = UUID()
var body: some View {
ScrollView(axes, showsIndicators: showsIndicators) {
content()
.background(GeometryReader { geometry in
Color.clear
.onAppear {
calculateOffset(in: geometry)
}
.onChange(of: geometry.frame(in: .named(coordinateSpaceName)).origin) {
calculateOffset(in: geometry)
}
})
}
.coordinateSpace(name: coordinateSpaceName)
}
private func calculateOffset(in geometry: GeometryProxy) {
let newOffset = geometry.frame(in: .named(coordinateSpaceName)).origin
DispatchQueue.main.async {
self.offset = newOffset
}
}
}
사용방법은 2번과 같다. ScrollView 처럼 사용하면 될 것 같다.
import SwiftUI
struct ContentView: View {
@State private var offset: CGPoint = .zero
private let coordinateSpaceName = UUID()
var body: some View {
ObservingScrollView(offset: $offset) {
Color(.systemGray3)
.frame(height: 200)
.overlay {
Text("\(offset.y)")
}
}
}
}
가로로 스크롤 할 경우
이 경우도 고려하여 만들었다. offset.x 를 가져오면 된다. 물론 axis 는 .horizontal 이 되면 된다.
마치며
세개의 코드중 어떤 것이 제일 좋고, 최적화가 된 코드인지는 아직 모르겠다. 하지만 장단점은 분명히 있다. 첫번째는 정말 내가 원하는 여러가지 요소의 위치를 추적하고 싶을 때 자유롭게 사용이 가능하지만, 두번째 세번째는 스크롤뷰 그 자체만 추적이 가능하여 좀더 편하게 사용가능하긴 하지만 제한적이라는 생각이 든다.
'→ Swift Archive' 카테고리의 다른 글
[Swift] 정규표현식 사용하여 문자열 검사하기 (0) | 2024.04.15 |
---|---|
[SwiftUI] 커스텀 달력 만들기 (1) | 2024.02.06 |
[SwiftUI] TabView 이상한 현상 발견?! (0) | 2024.02.03 |
[SwiftUI] 앱의 실행, 종료 알기 (1) | 2024.01.27 |
[SwiftUI] 앱이 background 에서 다시 돌아왔을 때 알기 (0) | 2024.01.27 |