SwiftUI

[SwiftUI] 연속한 뷰에 대해 드래그 제스처로 on/off할 수 있는 기능 구현 | 드래그 제스처 .gesture DragGesture

하이D:) 2023. 7. 13. 16:20

forEach로나열해 준연속한 뷰에 대해 드래그 제스처로 연속해서 각 셀을 on/off할 있는 기능을 구현하고 싶어 SwiftUI로 드래그 기능을 구현한 여러 가지 샘플코드들을 테스트해 보았고 가장 나의 케이스에 맞는 코드를 참고하여 이 기능을 구현하고 dragGesture에 대해 잘 익힐 수 있는 기회가 되었다.

우선, 기능 구현을 위해 필요한 요소들을 나열하면서 각각 필요한 이유에 대해 설명하고자 한다.

 

#🥨 특정 뷰에서드레그 하는 제스처를감지하고업데이트시켜주기 위해서는. gesture수정자와 DragGesture 구조체 사용

특정 뷰에서드레그 하는 제스처를감지하고업데이트시켜주기 위해서는. gesture수정자 DragGesture 구조체를

  • gesture 수정자 : 사용자가 앱을 사용할 때 하는 제스처를 트레킹 해주기 위한 수정자. 원하는 뷰에다가 .gesture 수정자를 붙이면 그 뷰에서 일어나는 제스처에 대한 설정이 가능하다.
  • DragGesture 구조체  
    • https://developer.apple.com/documentation/swiftui/draggesture
    • 드래그 이벤트 시퀀스가 변경될 때 작업을 호출하는 드래그 동작이다. (A dragging motion that invokes an action as the drag-event sequence changes.)
    • drag 관련 액션 이벤트를 생성할 때 사용 : 제스쳐를 인식하기 위해 제스쳐를 생성하고 구성하는 구조체이며, gesture 수정자 안에 사용할 수 있다 (To recognize a drag gesture on a view, create and configure the gesture, and then add it to the view using the gesture(_:including:) modifier.)
    • DragGesture의 initializer : init(minimumDistance: CGFloat, coordinateSpace: some CoordinateSpaceProtocol)
      • minimumDistance : 제스처가 성공하기 위한 최소 드래그 거리
      • coordinateSpace : 드래그 위치값을 받을 좌표 공간
    • 제스쳐를 수행할 때 실행되는 함수 아래 가지이며, 가지 함수를 활용하여 드래그 기능을구현해 줄수 있다.
      • updating(_:body:) : 제스처 값이 변경되면 제공된 제스처 상태 속성(@GestureState프로퍼티래퍼 정의한 프로퍼티)을 업데이트한다.
         
      •  
        onChanged : 제스처 값이 변경될  수행할 작업을 추가
      • onEnded : 제스처가 종료될  수행할 작업을 추가
 

DragGesture | Apple Developer Documentation

A dragging motion that invokes an action as the drag-event sequence changes.

developer.apple.com

 

#🥨 @GestureState  프로퍼티 래퍼 사용해서 현재 드래그하고 있는 위치를 감지

@GestureState  프로퍼티래퍼로 정의해 준 프로퍼티를 사용해서 현재 드래그하고 있는 위치를 감지하고 DragGesture 구조체의 .updating 함수로 현재 드래그 하고 있는 위치에 대해 업데이트해준다

 

struct DragGesturePracticeView: View {

    @GestureState private var location: CGPoint = .zero
    
    ...
}
.gesture(
	DragGesture(minimumDistance: 0, coordinateSpace: .global)
		.updating($location) { (value, state, transaction) in
				state = value.location
		}

)

 

#🥨 범위 안에 들어왔을  on/off 를 토글 시켜주어야 하기때문에 어떤 셀인지, 몇 번째 셀인지 일시적으로저장시켜 주기 위한상태값 하나 생성

forEach로나열해 준뷰에 대해 하나하나 드래그가 되었을 때 on/off 를 토글 시켜주어야 하기때문에 @State private var highlighted 로 정의해준 프로퍼티가 필요하다. 지금 드레그 하고 있는 위치가 해당 뷰의 백그라운드 범위에 들어왔을 뷰에 해당하는 인덱스로 업데이트 시켜준다.

 

struct DragGesturePracticeView: View {

    @State private var highlighted: Int? = nil
    
    ...
}

 

#🥨 .background 수정자를 사용해서 지금 드래그 하고 있는 위치가 해당뷰() 포함되는지를 파악

LazyHGrid 사용하여 연속되는 뷰를 넘나드는 드래깅에 따라 하나하나에 변화를 주기 위해서는 .background 수정자를 사용해서 지금 드래그 하고 있는 위치가 해당뷰(셀)에 포함되는지를 파악해주는 것이 중요하다. 때문에, 아래와 같은 함수를 정의하여 해당 뷰의 배경에서 드레그를하고 있다면 highlighted 프로퍼티 값을 변경해주는 등의 설정이 필요하다.

struct DragGesturePracticeView: View {
	...

    @State private var highlighted: Int? = nil
	
    ...
    
    func rectReader(index: Int) -> some View {
        return GeometryReader { (geometry) -> AnyView in
            if geometry.frame(in: .global).contains(self.location) {
 //✅ 지금 내가 드레그 하고 있는위치(self.location)가 index번째의 셀영역에(셀의 배경 영역) 포함되어 있으면 셀의 on/off 를 토글시켜준다
 //✅ 맨 처음에 드래그가 배경 영역(rectReader)에 포함될 때는 self.highlighted != index 의 경우이므로 토글 시켜주고 그 이후에 드레그가 같은 셀의 배경 영역 안에서 움직일 때는 토글 시켜주지 않도록
                DispatchQueue.main.async {
                    if(self.highlighted != index) {
                        self.data[index].isOn.toggle()
                    }
                    self.highlighted = index
                }
            }
            return AnyView(Rectangle().fill(Color.clear))
        }
    }
    
    var body: some View {
		...
    }
}

 

#🥨  최종 코드

struct Player {
    var name : Int
    var isOn : Bool
}

struct PlayerView: View {
    var scaled: Bool = false
    var player: Player
    
    var body: some View {
        ZStack(alignment: .topLeading) {
            
            Rectangle()
                .frame(width: 30, height: 90)
                .foregroundColor(player.isOn ? .green : .gray)
                .cornerRadius(5.0)
                .scaleEffect(scaled ? 1.5 : 1)
            
            VStack {
                Text(String(player.name))
                    .foregroundColor(.white)
            }
            .padding([.top, .leading], 10)
        }
        .zIndex(scaled ? 2 : 1)
    }
}


struct DragGesturePracticeView: View {
    @State private var data: [Player] =  [
        Player(name: 0, isOn: false),
        Player(name: 1, isOn: true),
        Player(name: 2, isOn: false),
        Player(name: 3, isOn: false),
        Player(name: 4, isOn: false),
        Player(name: 5, isOn: false),
        Player(name: 6, isOn: false),
        Player(name: 7, isOn: false),
        Player(name: 8, isOn: false),
        Player(name: 9,  isOn: false),
        Player(name: 10,  isOn: false),
        Player(name: 11,  isOn: false),
    ]

    @GestureState private var location: CGPoint = .zero
    @State private var highlighted: Int? = nil
    
    let rows = [
        GridItem(.fixed(100))
    ]
    
    func rectReader(index: Int) -> some View {
        return GeometryReader { (geometry) -> AnyView in
            if geometry.frame(in: .global).contains(self.location) {
 //✅ 지금 내가 드레그 하고 있는위치(self.location)가 index번째의 셀영역에 (셀의 배경 영역) 포함되어 있으면 셀의 on/off 를 토글시켜준다
 //✅ 맨 처음에 드래그가 배경 영역(rectReader)에 포함될 때는 (self.highlighted != index) 의 경우이므로 토글 시켜주고 그 이후에 드레그가 배경 영역(rectReader) 안에서 움직일 때는 토글 시켜주지 않도록
                DispatchQueue.main.async {
                    if(self.highlighted != index) {
                        self.data[index].isOn.toggle()
                    }
                    self.highlighted = index
                }
            }
            return AnyView(Rectangle().fill(Color.clear))
        }
    }
    
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false){
            LazyHGrid(rows: rows, alignment: .center, spacing: 2) {
                ForEach(0..<data.count, id : \.self) { i in
                        PlayerView(scaled: self.highlighted == i, player: self.data[i])
                            .background(self.rectReader(index: i))
                            .gesture(
                                DragGesture(minimumDistance: 0, coordinateSpace: .global)
                                    .updating($location) { (value, state, transaction) in
                                        state = value.location
                                    }
                                    .onEnded {_ in
                                        self.highlighted = nil
                                    }
                            )
                    
                }
            }
            .padding(.vertical,30)
        }
    }
}

 

 

 

참고

https://stackoverflow.com/questions/59797968/swiftui-drag-gesture-across-multiple-subviews