-
SwiftUI - MagnifyGesture (Pinch Gesture)SwiftUI 2023. 12. 4. 19:00
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 SwiftUI에서 핀치 줌 기능 사용을 위한 MagnifyGesture에 대해 학습해보겠습니다 🙋🏻
우선 핀치 줌이라고 불리는것에 대해 먼저 알아봐야겠죠?
Pinch Gesture?
애플의 공식문서에서 Handling pich gesture라는 문서를 찾아볼 수가 있습니다!
공식문서를 토대로 한번 핀치 제스처가 어떤건지 어떻게 기본적으로 사용되는지 확인해볼께요.
핀치 제스처는 화면을 터치하는 처음 두 손가락 사이의 거리를 추적하는 연속 제스처입니다.
UIKit에서 핀치 동작을 감지하려면 UIPinchGestureRecognizer 클래스를 사용해야합니다.
해당 클래스를 이번 포스팅에서는 딥하게 파지 않습니다 🥹
즉, 핀치 제스처는 아래와 같은 기능이에요.
우리가 이미지를 확대해서 볼때 아주 빈번하게 사용하는 기능임을 알 수 있습니다.
꼭 이미지뿐만 아니라 화면상 콘텐츠로 나타낸것에 대한 크기를 변경하는데 사용되는 경우도 많습니다.
예를들면 지도와 같은것에서요!
pinch gesture recognizer는 화면을 터치하는 두 손가락 사이의 거리 변화를 가지고 판단합니다.
즉, 핀치 동작은 연속적이기에 손가락 사이의 거리가 바뀔 때마다 구현해둔 메서드가 호출되는것이죠.
손가락 사이의 거리는 축척 계수로 인식됩니다.
제스처가 시작되면 일반 기본 배율은 1입니다.
두 손가락 사이의 거리가 증가함에 따라 배율도 비례적으로 증가하죠.
마찬가지로, 손가락 사이의 거리가 감소함에 따라 축척 계수도 감소하여 배율도 비례적으로 감소하구요 🙋🏻
pinch gesture recognizer는 두 손가락 사이의 거리가 처음으로 변경된 후에만 UIGestureRecognizer.State.began 상태에 진입합니다.
초기 진입 후 변경이 일어나면 후속 변경에 대해서는 UIGestureRecognizer.State.changed 상태를 호출하여 배율을 변화시키죠.
마지막으로 두 손가락을 떼면 핀치 제스처가 종료되면서 UIGestureRecognizer.State.ended 상태를 호출합니다.
즉, began 👉🏻 changed 👉🏻 ended 상태 변화의 주기를 가집니다.
UIKit에서 pinch gesture recognize를 사용해 뷰의 크기를 업데이트하는 예시 코드를 한번 봐볼까요?
@IBAction func scalePiece(_ gestureRecognizer : UIPinchGestureRecognizer) { guard gestureRecognizer.view != nil else { return } if gestureRecognizer.state == .began || gestureRecognizer.state == .changed { gestureRecognizer.view?.transform = ( gestureRecognizer.view?.transform.scaledBy( x: gestureRecognizer.scale, y: gestureRecognizer.scale ) )! gestureRecognizer.scale = 1.0 } }
예시로 이렇게 구현될 수가 있습니다.
배수로 핀치 제스처에 따라서 컨텐츠의 스케일이 변화하죠.
핀치 제스처를 인식하기 위해 주의해야할것이 세가지 존재합니다 🙋🏻
1️⃣ 뷰의 isUserInteractionEnabled 속성이 true로 되어 있어야 합니다. (이미지 및 라벨은 기본적으로 false로 되어 있어요.)
2️⃣ 두개 이상의 손가락이 화면을 터치하고 있어야 합니다.
3️⃣ 콘텐츠에 배율 인수를 올바르게 적용하고 있어야 합니다.
만약 현재 값에 축척 비율을 연속적으로 적용하면 값이 기하급수적으로 제곱으로 늘어나게되어 의도치 않은 결과를 가져와요!
자, 그럼 이렇게 pinch zoom이 무엇인지 UIKit에서는 간단히 어떻게 사용하는지 확인해봤습니다.
이제 오늘의 주제였던 SwiftUI에서 pinch zoom 기능을 도입해볼까요?
SwiftUI에서 pinch zoom 사용하기
원래 제가 알고있었던건 SwiftUI에서 pinch zoom을 사용하려면 MagnificationGesture를 사용했어야합니다.
그래서 생각나서 공식문서를 들어가봤는데,
뜨든..!?
디프리케이트 되어버렸군요ㅎ
이제 MagnifyGesture를 사용해야 한다고 합니다.
그래서 이번 포스팅에서 제목이 MagnifyGesture인것입니다~
MagnifyGesture?
사실 공식문서를 이전 MagnificationGesture와 비교하며 살펴보아도 뭐가 더 바뀌거나 추가된지는 모르겠어요.
이름 바뀌치기 느낌..?
그러면서 MagnificationGesture는 iOS 13 ~ iOS 17.2까지는 쓸 수 있지만, MagnifyGesture는 iOS 17이상에서 사용이 가능합니다.
거의 사용은 동일하다 싶으니 MagnifyGesture만 다룰줄 알아도 MagnificationGesture는 덤으로 올 수 있으니 알아보시죠ㅎㅎ
MagnifyGesture는 확대 동작을 인식하고 확대 정도를 추적하는 제스처 구조체입니다.
struct MagnifyGesture
물론 이 친구도 Gesture를 준수하고 있습니다.
생성하는 조건으로 이니셜라이저를 구성할때 필요한 속성은 하나입니다.
minimumScaleDelta
CGFloat 타입으로 동작이 시작되기전 필요한 최소 델타 값이죠.
즉, 비율을 의미하고 해당 최소 델타 값을 이용해 확대 동작을 만들 수 있습니다.
선언도 간단한 만큼 사용도 훨씬 간단합니다.
예시 코드로 한번 보시죠!
import SwiftUI struct ContentView: View { @GestureState private var magnifyBy = 1.0 var magnification: some Gesture { MagnifyGesture() .updating($magnifyBy) { value, gestureState, transaction in gestureState = value.magnification } } var body: some View { Circle() .frame(width: 100, height: 100) .scaleEffect(magnifyBy) .gesture(magnification) } }
MagnifyGesture는 Gesture를 준수하면서 만드는것이 기본입니다.
해당 MagnifyGesture 인스턴스를 만들면서 전역적으로 선언한 magnifyBy인 제스처 상태 변수 값을 updating 메서드로 붙여 사용합니다.
즉, 핀치 제스처로 인해 비율이 변경되게 되면 해당 값은 변경된 비율로 값이 들어가게 되죠.
그리고 핀치 줌을 시켜줄 컨텐츠로 원을 간단히 두었습니다.
해당 원의 scaleEffect 모디파이어를 이용해 나타낼 비율을 핀치 줌이 일어날때 값이 변경되는 magnifyBy 값으로 바인딩 시켜줍니다.
그리고 제스처로 위에서 만든 커스텀한 제스처인 magnification을 붙여주는것이죠.
그럼 동작을 한번 봐볼까요?
의도대로 두 손가락으로 핀치함에 따라 원의 스케일이 변경됩니다ㅎㅎ
아..! 참고로, 엑코 시뮬레이터에서 핀치 줌을 해보시려면 option을 누른 상태에서 컨텐츠를 드래그 하면 됩니다!
여기서 부족한게 있죠.
스케일이 결국 핀치 줌하고 있을때만 변경되고 손을 떼는 순간 원래 크기로 돌아오게 됩니다.
물론 이는 원하는 요구에 따라 가져갈 수도 있겠지만, 이미지가 변경된 크기로 손을 떼더라도 고정해야 할 경우도 있을거에요.
왜 원래 크기로 돌아오는지는 GestureState라는 속성에 있습니다.
해당 GestureState 속성은 제스처가 활성화 되있는 동안에 임시적인 상태를 저장하는데 사용하는것입니다.
즉, 해당 속성은 제스처의 현재 상태를 추적하고 뷰에 반영하는데 사용되죠.
그렇기에, 제스처가 끝나면 자동으로 다시 초기 상태로 리셋되는것입니다.
GestureState를 통해 구현하면 제스처가 활성화되지 않으면 해당 상태가 필요하지 않다 판단하기에,
앱의 메모리 사용량을 최적화하고 제스처 상태 관리를 단순화할 수 있는 이점이 있습니다.
즉, 제스처가 끝난 후 자동으로 리셋되기에 다시 다음 제스처 이벤트를 위한 준비 상태가 되는것이죠.
그렇기에 리셋되지 않고 우리가 두번째로 원했던 변경된 크기로 가져감을 구현해보고 싶다면 State 속성을 사용해보는것입니다.
간단히 위 예제 코드를 변경해 볼까요?
import SwiftUI struct ContentView: View { @State private var magnifyBy = 1.0 var magnification: some Gesture { MagnifyGesture(minimumScaleDelta: magnifyBy) .onChanged { value in magnifyBy = value.magnification } } var body: some View { Circle() .frame(width: 100, height: 100) .scaleEffect(magnifyBy) .gesture(magnification) } }
다른점은 State 속성을 사용하면서 updating 메서드 대신 onChanged 메서드를 이용하는것입니다.
그럼 어떻게 의도대로 되는지 돌려볼까요~?
핀치가 끝난 후 손을 떼어도 최종 늘어난 비율로 값을 가져가고 리셋되지 않는걸 볼 수 있습니다 🙋🏻
결국, 구현하고자하는 스펙에 따라서 두가지 방법중 채택하면 될것 같아요ㅎㅎ
이 외에도, 핀치와 드래그를 같이 곁들여서 확대된 상태로 다른 부분을 보고 싶어 드래깅을 한다거나 하는 구현들은 더 살을 붙여서 드래그 제스쳐도 구현하거나 하는 방식으로 만들어야합니다.
🙋🏻 추가 업데이트
간혹 위 코드처럼 적용했지만, 3배수로 스케일을 늘어난 상태에서 조정하면, 핀치 시 항상 배율은 1부터 시작하기에 해당 원의 스케일이 1로 잠깐 적용되었다가 업데이트되는 버그같은 현상을 볼 수 있어요!
그럴때는 아래와 같이 코드를 구성하며 scaleEffect를 가져가는편이 좋습니다.
struct ContentView: View { @State private var scale = 3.0 @GestureState private var magnification = 1.0 var magnificationGesture: some Gesture { MagnifyGesture() .updating($magnification) { value, gestureState, transaction in gestureState = value.magnification } .onEnded { value in self.scale *= value.magnification } } var body: some View { Circle() .frame(width: 100, height: 100) .scaleEffect(scale * magnification) .gesture(magnificationGesture) } }
마무리
이렇게 오늘은 핀치 제스처에 대해 알아봤습니다!
정말 많이 사용되는 기능으로 SwiftUI에서 손쉽게 적용해보면서 또 장점인 커스텀한 구현들을 쉽게 붙여볼 수 있을것 같습니다 🙋🏻
레퍼런스
'SwiftUI' 카테고리의 다른 글
SwiftUI에서 shadow와 blur 사용하기 (79) 2024.01.08 SwiftUI - trim & mask (86) 2023.12.21 SwiftUI에서 VoiceOver 사용하기 (40) 2023.11.23 BorderlessButtonStyle의 활용 (44) 2023.10.19 allowsHitTesting을 통한 뷰 터치 이벤트 넘기기 (54) 2023.10.13