DragGesture를 사용하면 드래그 기능을 구현할 수 있습니다
DragGesture를 사용하기 전에 알아둘 것
DragGesture를 사용하기 전에 미리 알아둬야 하는 것들이 있습니다.
첫번째로 offset 입니다.
offset 을 사용한다면 x축과 y축을 통해 현재 위치가 뷰의 어디에 위치하는지 알 수 있습니다.
https://apple-document.tistory.com/222
아래 코드처럼 offset을 사용하게 된다면 컨텐츠의 위치를 이동시켜 원하는 뷰를 볼 수 있도록 해줍니다.
GeometryReader { geometry in
VStack {
HStack(spacing: 0) {
ForEach(0..<3) { index in
getViewFor(index: index)
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
.frame(width: geometry.size.width * 3, height: geometry.size.height, alignment: .leading)
.offset(x: -CGFloat(selectedTab) * geometry.size.width + verticalOffset)
offset 변수의 값을 드래그 했을 때 변화시켜 다이나믹하게 다음에 이동할 뷰를 미리 보여줄 수 있습니다.
.offset(x: -CGFloat(selectedTab) * geometry.size.width + offset)
offset을 적용하여 뷰의 위치를 원하는 곳으로 이동할 수 있게 됩니다.
GeometryReader
offset을 알게 되었다면 이제 GeometryReader 입니다. 화면의 뷰를 드래그하여 원하는 뷰로 이동하기 위해서는 GeometryReader를 사용해야 합니다. GemetryReader를 사용하게 된다면 부모 레이아웃의 크기를 알아내서 컨텐츠의 위치 및 사이즈를 조절할 수 있습니다.
아래 코드를 보면서 먼저 어떻게 사용되는지 설명하겠습니다.
GeometryReader { geometry in
HStack(spacing: 0) {
ForEach(0..<tabs.count, id: \.self) { index in
getViewFor(index: index)
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
.offset(x: -CGFloat(selectedTab) * geometry.size.width + verticalOffset)
.animation(.easeInOut, value: selectedTab)
.animation(.easeInOut, value: verticalOffset)
.gesture(
DragGesture()
.onChanged { value in
verticalOffset = value.translation.width
}
.onEnded { value in
let threshold = geometry.size.width / 2
if -value.predictedEndTranslation.width > threshold && selectedTab < tabs.count - 1 {
selectedTab += 1
} else if value.predictedEndTranslation.width > threshold && selectedTab > 0 {
selectedTab -= 1
}
verticalOffset = 0
}
)
}
GeometryReader { geometry in
VStack {
GeometryReader 는 VStack 으로 구성된 컨텐츠들을 모두 클로저 안에 담아내고 있습니다. 그리고 GeometryReader 는 클로저에 값을 반환하는데 이를 geometry 로 이름을 지었습니다.
HStack(spacing: 0) {
ForEach(0..<3) { index in
getViewFor(index: index)
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
해당 geometry 를 사용하여 GeometryReader 에서 가장 최상위 뷰인 VStack의 크기(높이,너비)에 접근할 수 있습니다.
접근한 이후 화면에 나타낼 뷰의 크기를 frame의 width와 height로 지정합니다.
.offset(x: -CGFloat(selectedTab) * geometry.size.width + offset)
geometry는 offset에도 적용됩니다. HStack의 너비는 iPhone 15 Pro 기준으로 393인 것으로 확인하였습니다.
이후 중요한 gesture 부분입니다.
.gesture(
DragGesture()
.onChanged { value in
verticalOffset = value.translation.width
}
.onEnded { value in
let threshold = geometry.size.width / 2
if -value.predictedEndTranslation.width > threshold && selectedTab < tabs.count - 1 {
selectedTab += 1
} else if value.predictedEndTranslation.width > threshold && selectedTab > 0 {
selectedTab -= 1
}
verticalOffset = 0
}
)
gesture 에는 화면을 드래그 하는 것을 만들기 위해서 DragGesture()를 사용합니다.
.onChanged { value in
verticalOffset = value.translation.width
}
onChanged 에서는 화면을 드래그하기 시작했을 때 verticalOffset의 값을 translation.width 를 사용하여 현재 드래그된 너비의 거리를 저장합니다.
translation 이란
translation 은 현재 드래그된 거리를 나타냅니다. 이는 제스처가 시작된 지점에서 현재 위치까지의 이동 거리를 나타냅니다.
translation 에는 width 와 height 등이 존재하여 원하는 값을 사용하여 드래그되는 위치를 추적할 수 있습니다.
즉 translation.width 는 가로로 얼만큼 드래그 되었는지를 나타내는 값을 의미하고
translation.height는 세로로 얼만큼 드래그 되었는지를 나타내는 값을 의미합니다.
.onEnded { value in
let threshold = geometry.size.width / 2
if -value.predictedEndTranslation.width > threshold && selectedTab < tabs.count - 1 {
selectedTab += 1
} else if value.predictedEndTranslation.width > threshold && selectedTab > 0 {
selectedTab -= 1
}
verticalOffset = 0
}
onEnded 에서는 화면 드래그가 끝난 시점에서 특정 동작을 수행하는 코드입니다.
threshold 즉 임곗값을 geometry의 너비의 절반으로 설정합니다. (이를 통해 화면을 절반만큼 드래그 했을 때 뷰를 옮길 수 있습니다)
predictedEndTranslation.width 를 사용하여 제스처가 끝날 시점에 위치의 값이 임곗값보다 크면서 동시에 selectedTab 의 값이 뷰의 개수를 넘지 않는다면 뷰를 이동하도록 selectedTab 의 값을 1과 -1을 증가시킵니다.
- 화면을 오른쪽에서 왼쪽으로 드래그하면 predicatedEndTranslation.width 는 음의 값으로 가까워집니다.
- predicatedEndTranslation.width의 값이 -196.5 보다 값이 작아진다면 화면을 오른쪽 뷰로 넘어가게 됩니다 (iPhone15 Pro 기준)
- 화면을 왼쪽에서 오른쪽으로 드래그하면 predicatedEndTranslation.width 는 양의 값으로 가까워집니다.
- predicatedEndTranslation.width의 값이 196.5 보다 값이 커지게 된다면 화면을 왼쪽 뷰로 넘어가게 됩니다 (iPhone15 Pro 기준)
결과물
전체코드
import SwiftUI
struct MainContainer: View {
@State private var selectedTab = 1
@State private var verticalOffset: CGFloat = 0
private let tabs: [String] = ["자산", "소비﹒수입", "연말정산"]
var body: some View {
VStack {
HStack {
ForEach(Array(tabs.enumerated()), id: \.offset) { obj in
Button {
selectedTab = obj.offset
print(selectedTab)
} label: {
Text(obj.element)
.foregroundStyle(obj.offset == selectedTab ? Color.accentColor : Color.gray)
}
.frame(maxWidth: .infinity)
}
}
Divider()
GeometryReader { geometry in
HStack(spacing: 0) {
ForEach(0..<tabs.count, id: \.self) { index in
getViewFor(index: index)
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
.offset(x: -CGFloat(selectedTab) * geometry.size.width + verticalOffset)
.onAppear {
print(geometry.size.width)
}
.animation(.easeInOut, value: selectedTab)
.animation(.easeInOut, value: verticalOffset)
.gesture(
DragGesture()
.onChanged { value in
verticalOffset = value.translation.width
}
.onEnded { value in
let threshold = geometry.size.width / 2
if -value.predictedEndTranslation.width > threshold && selectedTab < tabs.count - 1 {
selectedTab += 1
} else if value.predictedEndTranslation.width > threshold && selectedTab > 0 {
selectedTab -= 1
}
verticalOffset = 0
}
)
}
}
}
@ViewBuilder
private func getViewFor(index: Int) -> some View {
switch index {
case 0:
Text("First View")
case 1:
Text("Second View")
case 2:
Text("Third View")
default:
EmptyView()
}
}
}
#Preview {
MainContainer()
}
'SwiftUI' 카테고리의 다른 글
SwiftUI - calendar (0) | 2024.08.15 |
---|---|
SwiftUI - 나만의 SPM 만들어 사용해보기 (0) | 2024.08.08 |
SwiftUI - 컨텐츠를 원하는 위치에 배치하기 (0) | 2024.08.06 |
Swift - Sheet, presentation 뒤에 배경하고 상호작용 presentationBackgroundInteraction (0) | 2024.07.18 |
DisclosureGroup 로 접는 뷰 만들기 (0) | 2024.07.13 |