ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SwiftUI에서 TapGesture를 통해 현재 Position 구하기
    SwiftUI 2022. 11. 7. 10:32

    안녕하세요. 그린입니다🍏

    이번 포스팅은 SwiftUI에서 TapGesture를 통해 현재 탭된 위치, Position을 구해보려합니다🙌

     

    SwiftUI를 사용하면서 아직 UIKit에서 제공해주는 기능들이 많이 미비할때가 아주 많습니다😭

    점차 발전하고 있고 추후 아주 쓰기 쉬울거라고는 확신하고 있지만 당장 지금 SwiftUI로 선언형 API들로만 구현을 하기에는 분명 무리가 있다고 느껴집니다.

     

    이번 주제인 사용자가 탭 위치를 구하는 기능도 SwiftUI에서는 기본적으로 간단하게 제공해주지 않았습니다.

    iOS 16에서 기존 onTapGesture의 기능이 확장되기 전까지는요..🕺🏻

     

    그렇기에 오늘은 iOS 16 이후 onTapGesture를 아주 간단히 알아보고 그 다음 이전 OS에서 오늘의 주제인 TapGesture를 통해 현재 Position을 구하는 커스텀한 제스쳐를 만들어보겠습니다🙌

     

    iOS 16 이후 onTapGesture

    iOS 16 이전에는 onTapGesture 메서드를 통해 탭 되었을때 현재 탭 된 위치를 알 수 있는 방법이 없었어요.

    그래서 오늘 할 주제이기도 하지만 SwiftUI의 제스쳐들을 잘 조합하여 커스텀하게 만들어주면서 아주 불편하게 사용해야 했습니다.

    더군다가 여러 제스쳐가 동시 다발적이라면 오류가 많을때도 많았구요.

    그렇기에 이번 업데이트를 통해 이 불편함을 애플에서 어느정도 해소해줬다고 생각합니다.

    선언은 이렇습니다.

    func onTapGesture(
        count: Int = 1,
        coordinateSpace: CoordinateSpace = .local,
        perform action: @escaping (CGPoint) -> Void
    ) -> some View

    coordinateSpace라는것이 생겼죠?

    이 파라미터를 통해 위치를 판단할 수 있습니다.

    CoordinateSpace 타입에는 기본적으로 정의된 local, global, named()가 존재합니다.

    여기서 local은 자신 즉 셀프뷰와 연관하여 현재 위치를 나타내며 global은 전체 디바이스에서의 위치를 판단해줍니다.

    named는 named("scroll")과 같이 개발 시 지정해준 String으로 지정된 뷰로부터의 위치를 판단합니다.

    이것만 잘 알면됩니다🥳

    그 외 count는 몇번 탭 되었을때 탭 제스쳐를 수행할것인지이고 탈출 클로저로 탭 된 후의 동작할 액션 로직을 넣어주면 됩니다.

    항상 늘 먹던 방법인데 coordinateSpace가 추가된것이죠!

     

    그럼 어떻게 사용할 수 있을까요?
    struct TapGestureExample: View {
      @State private var location: CGPoint = .zero
    
      var body: some View {
        Circle()
          .fill(self.location.y > 50 ? Color.blue : Color.red)
          .frame(width: 100, height: 100, alignment: .center)
          .onTapGesture(count:1, coordinateSpace: .global) { location in
            self.location = location
        }
      }
    }

    이렇게 원하는 뷰의 onTapGesture를 붙여 location을 판단할 수 있습니다.

    즉 해당 코드에서 원을 한번 탭하면 전체 디바이스의 좌표를 기준으로 현재 탭 된 위치가 location 값으로 저장되게 됩니다.

     

    아주 아주 쉽죠? iOS 16 이상 미니멈 타겟으로 사용하신다면 여기까지만 보셔도 됩니다.

     

    iOS 16 이하도 이런 기능을 지원해야한다면....
    이제 바로 보시죠!

     

    iOS 16 이하에서는 위와 같은 기본적으로 구현된 기능이 전혀 없습니다.

    그렇기에 SwiftUI 세상에서는 기존 구현된 제스쳐들의 조합을 통해 이런 기능들을 커스텀하게 만들어줘야 합니다.

    자 그럼 만들어보자구요!

     

    iOS 16 이전 SwiftUI 세상에서 Tap 위치 구하는 커스텀 제스쳐 만들기

    우선 두가지를 활용해야 합니다.

    TapGesture와 DragGesture

    TapGesture에서는 현재 탭 된 위치를 구할수가 전혀 없지만 DragGesture에는 기본적으로 드래깅을 위해 드래그가 시작된 현재 위치와 중간 그리고 끝나는 위치들을 모두 구할 수 있어요.

    그렇기에 우리는 이 두가지를 잘 섞어서 하나의 TapGesture를 만들 수 있습니다.

    (당연하지만 야매같은 느낌이죠!)

     

    그럼 코드로 먼저 보시죠🙋🏻

    아래 오늘 알아볼 코드들은 참고 자료에 있는 스택오버플로의 해결방안중 하나를 기반으로 합니다!

    import SwiftUI
    
    public struct CustomTapGesture: Gesture {
      public typealias Value = SimultaneousGesture<TapGesture, DragGesture>.Value
      
      let count: Int
      let coordinateSpace: CoordinateSpace
      
      init(
        count: Int = 1,
        coordinateSpace: CoordinateSpace = .global
      ) {
        self.count = count
        self.coordinateSpace = coordinateSpace
      }
      
      public var body: SimultaneousGesture<TapGesture, DragGesture> {
        SimultaneousGesture(
          TapGesture(count: count),
          DragGesture(minimumDistance: 0, coordinateSpace: coordinateSpace)
        )
      }
      
      public func onEnded(perform action: @escaping (CGPoint) -> Void) -> _EndedGesture<CustomTapGesture> {
        self.onEnded { (value: Value) -> Void in
          guard value.first != nil else { return }
          guard let location = value.second?.startLocation else { return }
          guard let endLocation = value.second?.location else { return }
          guard ((location.x-1)...(location.x+1))
            .contains(endLocation.x), ((location.y-1)...(location.y+1))
            .contains(endLocation.y) else {
            return
          }
          action(location)
        }
      }
    }

    보시면 SimultaneousGesture로 탭과 드래그 제스쳐를 받아요.

    즉 두 제스쳐를 동시에 합쳐주겠다는 소리입니다.

    탭이 되었을때 드래그를 판단해요.

    드래그의 최소 거리가 0 즉, 항상 판단하게 된다는 소리입니다.

    그리고 탭이 끝났을때 드래그도 끝나게 되니 location을 onEnded에서 구할 수 있어요.

    구하는것은 탭될때 드래그도 동시 진행되니 startLocation만 사실 알면되죠.

    그런데 탭이 끝날때 드래그도 끝나고 이게 드래그인지 탭인지 오차가 있을 수 있으니 +-1의 오차 범위를 구할 수 있도록 해줍니다.

    이 범위를 벗어나면 탭 제스쳐라고 판단할 수 없으니 return해주면 되죠.

     

    결국 만들어진 이 제스쳐를 아래와 같이 View에서 사용할 수 있도록 ViewModifier로 만들어주면 됩니다.

    import SwiftUI
    
    extension View {
      public func onCustomTapGesture(
        count: Int,
        coordinateSpace: CoordinateSpace = .global,
        perform action: @escaping (CGPoint) -> Void
      ) -> some View {
        simultaneousGesture(CustomTapGesture(count: count, coordinateSpace: coordinateSpace)
          .onEnded(perform: action)
        )
      }
      
      public func onCustomTapGesture(
        count: Int,
        perform action: @escaping (CGPoint) -> Void
      ) -> some View {
        onCustomTapGesture(count: count, coordinateSpace: .global, perform: action)
      }
      
      public func onCustomTapGesture(
        perform action: @escaping (CGPoint) -> Void
      ) -> some View {
        onCustomTapGesture(count: 1, coordinateSpace: .global, perform: action)
      }
    }

    View를 extension하여 커스텀 탭 제스쳐 View Modifier를 설정해줄 수 있어요.

    저의 경우는 탭은 한번만 그리고 위치 판단은 디바이스 전체 좌표를 기준으로 삼았습니다.

    이제 원하는 파라미터를 넣어 편하게 입맛대로 사용하면 됩니다.

    이후 사용부에서 action에 원하는 로직만 잘 넣어주면 됩니다!

     

    그럼 한번 사용해볼께요!
    import SwiftUI
    
    struct ContentView: View {
      @State private var location: CGPoint = .zero
      
      var body: some View {
        VStack(spacing: 20) {
          Image(systemName: "globe")
            .imageScale(.large)
            .foregroundColor(.accentColor)
            .onCustomTapGesture { location in
              self.location = location
            }
          
          Text("Get position of Tap Gesture")
            .onCustomTapGesture { location in
              self.location = location
            }
          
          Circle()
            .fill(self.location.y > 350 ? Color.blue : Color.red)
            .frame(width: 100, height: 100, alignment: .center)
            .onCustomTapGesture { location in
              self.location = location
            }
          
          Text("current position is \(location.x) \(location.y)")
        }
        .padding()
      }
    }

    요렇게 아주 쉬운 예제 코드를 짜봤어요.

    이러면 각 뷰가 탭되면 제일 하단 현재 위치를 알려주는 텍스트가 바뀔거에요.

    또한 Circle의 색상도 탭 된 포지션에 따라 바뀝니다.

    바로 보시죠!

    짜잔! 원하는 바를 이뤘습니다.

     

    그럼 이만..👋👋👋

     

    마무리

    SwiftUI 세상에 오신걸 환영합니다.

    이게 바로 SwiftUI의 현주소죠!

    위 예제 코드들은 제 깃헙에 올려놨으니 참고하셔도 좋습니다😀

    https://github.com/GREENOVER/playground/tree/main/CustomTapGesture

     

    [참고 자료]

    https://developer.apple.com/documentation/swiftui/view/ontapgesture(count:coordinatespace:perform:) 

     

    Apple Developer Documentation

     

    developer.apple.com

    https://stackoverflow.com/questions/56513942/how-to-detect-a-tap-gesture-location-in-swiftui

     

    How to detect a tap gesture location in SwiftUI?

    (For SwiftUI, not vanilla UIKit) Very simple example code to, say, display red boxes on a gray background: struct ContentView : View { @State var points:[CGPoint] = [CGPoint(x:0,y:0), CGPoint(...

    stackoverflow.com

Designed by Tistory.