SwiftUI

Gesture

Phililip
728x90

안녕하세요.

 

이번에는 SwiftUI의 Gesture에 대해 알아볼게요.

 


# 1. 제스처 추가

특정 뷰에 제스처를 추가하려면, Gesture를 생성 및 설정한 뒤에, gesture(_:including:) 수식어를 사용해줍니다.

 

struct ShapeTapView: View {
    var body: some View {
        let tap = TapGesture()		// 제스처 생성
            .onEnded { _ in		// 제스처 설정
                print("View tapped!")
            }
        
        return Circle()
            .fill(Color.blue)
            .frame(width: 100, height: 100, alignment: .center)
            .gesture(tap)			// 제스처 추가
    }
}

 

 

 

 

 

만약 아래처럼 VStack에는 gesture(_:including:) 수식어를 사용해서 gesture 동작을 정의하고, VStack 안에 있는 Image 뷰에는 onTapGesture handler를 추가했을 경우 어떻게 동작할까요?

 

struct ContentView: View {
    let newGesture = TapGesture().onEnded {
        print("Tap on VStack.")
    }

    var body: some View {
        VStack(spacing:25) {
            Image(systemName: "heart.fill")
                .resizable()
                .frame(width: 75, height: 75)
                .padding()
                .foregroundColor(.red)
                .onTapGesture {
                    print("Tap on image.")
                }
            Rectangle()
                .fill(Color.blue)
        }
        .gesture(newGesture)
        .frame(width: 200, height: 200)
        .border(Color.purple)
    }
}

 

 

VStack 안에 있는 Rectangle 뷰는 newGesture 동작을 따라가지만, Image 뷰는 자체 정의한 onTapGesture 동작을 따라가게 됩니다.

 

onTapGesture가 우선순위가 높다는 의미가 아니라,

 

자기 자신한테 gesture가 정의되어 있지 않으면 부모 뷰에 정의된 gesture를 따르게 되어있지만, 자신한테 gesture가 정의되어 있으면 자신의 gesture가 부모 뷰의 gesture보다 우선순위가 더 높다는 것을 의미합니다.

 

 

 

# 2. Gesture callback

SwiftUI에서는 Gesture의 상태가 변할 때마다 callback이 호출해주는데, 종류는 총 3가지가 있어요.

 

- updating(_:body:)

- onChanged(_:)

- onEnded(_:)

 

 

 

# 3. 일시적인 UI 상태 업데이트

Gesture 변화로 UI 상태를 일시적으로 변화시키려면 GestureState 프로퍼티를 추가하고, 이를 사용해서 updating(_body:) callback을 사용합니다.

 

Gesture가 시작되고 Gesture가 끝나기 직전까지 계속해서 updating callback이 호출되며, 사용자가 gesture를 종료하거나 취소할 경우에는 updating callback이 호출되지 않아요.

 

그리고, gesture가 끝나면 GestureState 프로퍼티의 값은 초기값으로 reset 됩니다.

 

 

예를 들어, 사용자가 길게 누를 때 색상이 변경되게 하고 싶다면, updating callback을 구현하고 그 callback 안에서 상태를 업데이트해줍니다.

 

struct CounterView: View {
    @GestureState var isDetectingLongPress = false
    
    var body: some View {
        let press = LongPressGesture(minimumDuration: 1)
            .updating($isDetectingLongPress) { currentState, gestureState, transaction in
                gestureState = currentState
            }
        
        return Circle()
            .fill(isDetectingLongPress ? Color.yellow : Color.green)
            .frame(width: 100, height: 100, alignment: .center)
            .gesture(press)
    }
}

 

 

 

 

callback 파라미터의 transaction을 사용하면 애니메이션 처리도 가능해요!!

 

struct ContentView: View {
    @GestureState var isDetectingLongPress = false
    
    var body: some View {
        let press = LongPressGesture(minimumDuration: 1)
            .updating($isDetectingLongPress) { currentState, gestureState, transaction in
                gestureState = currentState
                transaction.animation = .easeInOut    ✅
            }
        
        return Circle()
            .fill(isDetectingLongPress ? Color.yellow : Color.green)
            .frame(width: 100, height: 100, alignment: .center)
            .gesture(press)
    }
}

 

 

 

 

 

# 3. Gesture 도중 상태 영구적으로 업데이트

gesture가 끝나도 상태가 reset 되지 않고 유지되었으면 하는 동작이 있다면, onChange(_:) callback을 사용해야 합니다.

 

struct CounterView: View {
    @GestureState var isDetectingLongPress = false
    @State var totalNumberOfTaps = 0
    
    var body: some View {
        let press = LongPressGesture(minimumDuration: 1)
            .updating($isDetectingLongPress) { currentState, gestureState, transaction in
                gestureState = currentState
            }.onChanged { _ in
                self.totalNumberOfTaps += 1
            }
        
        return VStack {
            Text("\(totalNumberOfTaps)")
                .font(.largeTitle)
            
            Circle()
                .fill(isDetectingLongPress ? Color.yellow : Color.green)
                .frame(width: 100, height: 100, alignment: .center)
                .gesture(press)
        }
    }
}

 

 

 

 

 

# 4. Gesture가 끝났을 때 상태 영구적으로 업데이트

Gesture가 성공적으로 끝났을 때, 상태 값을 최종적으로 할당하고 싶을 때 onEnded(_:) callback을 사용합니다.

 

Gesture가 성공적으로 끝났을 때만 callback이 호출되고, Gesture 도중 멈출 경우에는 onEnded(_:) callback이 호출되지 않는답니다.

 

 

예를 들어, 아래처럼 onEnded callback이 호출될 때, 원의 색깔을 빨간색으로 바꿔준다고 했을 때

 

struct CounterView: View {
    @GestureState var isDetectingLongPress = false
    @State var totalNumberOfTaps = 0
    @State var doneCounting = false
    
    var body: some View {
        let press = LongPressGesture(minimumDuration: 1)
            .updating($isDetectingLongPress) { currentState, gestureState, transaction in
                gestureState = currentState
            }.onChanged { _ in
                self.totalNumberOfTaps += 1
            }
            .onEnded { _ in
                self.doneCounting = true
            }
        
        return VStack {
            Text("\(totalNumberOfTaps)")
                .font(.largeTitle)
            
            Circle()
                .fill(doneCounting ? Color.red : isDetectingLongPress ? Color.yellow : Color.green)
                .frame(width: 100, height: 100, alignment: .center)
                .gesture(doneCounting ? nil : press)
        }
    }
}

 

 

Tap을 누르고 있다가 이동시켰을 땐 onEnded가 호출되지 않는 것을 볼 수 있어요.

 

 

 

 

# 5. 기본 Gesture 종류

기본 Gesture는 총 5가지가 제공되고 있으니 한번 살펴보시는 것을 추천드려요!

 

 

 

## 참고

- https://developer.apple.com/documentation/swiftui/adding-interactivity-with-gestures

 

Apple Developer Documentation

 

developer.apple.com

 

 

 


 

이번 글은 여기서 마무리.

 

 

 

반응형

'SwiftUI' 카테고리의 다른 글

Custom Adaptive StackView (feat. ViewBuilder)  (0) 2022.04.17
horizontalSizeClass, dynamicTypeSize  (0) 2022.04.17
__printChanges  (0) 2022.03.23
SceneStorage  (0) 2022.03.20
Canvas 뷰를 통한 성능 향상  (0) 2022.03.14