관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 5. long press gesture와 애니메이션 - gesture 도중 화면 끝으로 가면 자동으로 스크롤되는 기능 본문

UI 컴포넌트 (swift)

[iOS - swift] 5. long press gesture와 애니메이션 - gesture 도중 화면 끝으로 가면 자동으로 스크롤되는 기능

jake-kim 2023. 8. 4. 23:50

1. long press gesture와 애니메이션 - 드래그 구현 방법 (snapshotView, CGAffineTransform)
2. long press gesture와 애니메이션 - 드래그할때 다른  줄어들고, 해당  크게하기 (UIView.animate, CGAffineTransform, concatenating)
3. long press gesture와 애니메이션 - 드래그와 cornerRadius, shadow 효과 (CABasicAnimation)
4. long press gesture와 애니메이션 - UIStackView에 DragDrop 적용 (DragDropStackView 구현)
5. long press gesture와 애니메이션 - gesture 도중 화면 끝으로 가면 자동으로 스크롤되는 기능 구현 (#Horizontal Scroll, #수평 스크롤)

long press 도중 자동스크롤 구현

  • 이전 포스팅 글에서는 UIScrollView + UIStackView로 long press하여 아이템을 이동하는 기능을 구현
  • 이번 포스팅 글에서는, long press하여 아이템을 이동할 때 화면 끝으로 갔을 때 자동으로 스크롤 되도록 하는 기능 구현이 목표]
long press 상태에서 화면 끝으로가면 자동으로 scroll되는 기능

구현 아이디어

  • long press의 changed 이벤트에서 UILongPressGestureRecognizer의 location(in:)을 사용하면 이동된 좌표를 구할 수 있는데, 이 좌표사용하여 화면 왼쪽, 상단에 닿았는지 확인이 가능 
  • UIScrollView를 가지고 있는 ViewController에서 화면 양쪽 끝에 가까워졌다는 이벤트를 받으면 그만큼 scrollView를 setContentOffset()으로 스크롤 이동하면 완료

구현

  • drag & drop하면서 ViewController쪽에 이벤트를 던져줄 Delegate에 드래그 관련된 필드 추가
    • containerView: long press gesture의 .changed이벤트에서 location(in:)을 사용하여 화면 기준 x좌표를 구해야하는데, 이때 들어갈 뷰
    • didDragOverMin: 왼쪽에 가까워진 경우 호출될 함수
    • didDragOverMax: 오른쪽에 가까워진 경우 호출될 함수
protocol DragDropStackViewDelegate {
    var containerView: UIView { get } // <-
    
    func didBeginDrag()
    func didDragOverMin(diff: Double) // <-
    func didMove(fromIndex: Int, toIndex: Int)
    func didDragOverMax(diff: Double) // <-
    func didEndDrop()
}
  • drag & drop의 config에 스크롤 해야하는 시점에 관한 값 추가
    • thresholdRatioForDeviceWidth 필드: UIScreen.main.bounds.width에 곱하여 스크롤 되어야하는 기준값을 계산할때 사용
struct HorizontalDragDropConfig {
    ...
    let thresholdRatioForDeviceWidth: Double
    
    init(
		...
        thresholdRatioForDeviceWidth: Double = 0.9
    ) {
		...
        self.thresholdRatioForDeviceWidth = thresholdRatioForDeviceWidth
    }
}
  • long press의 .changed 이벤트에서 오른쪽으로 스크롤 하는 것인지, 왼쪽으로 스크롤하는 것인지 판단 한 후, 각 스크롤 처리
    • 여기서 absolutedX를 구하여 right, left 스크롤 처리하는 함수에 각각 전달
func handleChanged(gesture: UILongPressGestureRecognizer) {
    let newLocation = gesture.location(in: self)
    let xOffset = newLocation.x - originalPosition.x
    let translation = CGAffineTransform(translationX: xOffset, y: 0) // 0을 대입해야 영역 안에서만 드래깅 가능
    
    guard let snapshotView else { return }
    let scale = CGAffineTransform(scaleX: config.dargViewScale, y: config.dargViewScale)
    snapshotView.transform = scale.concatenating(translation)
    
    let maxX = snapshotView.frame.maxX
    let midX = snapshotView.frame.midX
    let minX = snapshotView.frame.minX
    let absolutedX = gesture.location(in: delegate?.containerView ?? UIView()).x // <-
    let index = arrangedSubviews
        .firstIndex(where: { $0 == actualView }) ?? 0
    
    if midX > pointForDragDrop.x {
        handleChangedWhenDraggingRight(index: index, maxX: maxX, midX: midX, minX: minX, absolutedX: absolutedX)
    } else {
        handleChangedWhenDraggingLeft(index: index, maxX: maxX, midX: midX, minX: minX, absolutedX: absolutedX)
    }
}
  • left, right 스크롤 처리 함수에서 각각 threshold 값을 확인한 후 델리게이트로 이벤트 전달
func handleChangedWhenDraggingRight(index: Int, maxX: Double, midX: Double, minX: Double, absolutedX: Double) {
    let thresholdX = UIScreen.main.bounds.width * config.thresholdRatioForDeviceWidth
    if thresholdX < absolutedX {
        delegate?.didDragOverMax(diff: absolutedX - thresholdX)
    }
    ...
}

func handleChangedWhenDraggingLeft(index: Int, maxX: Double, midX: Double, minX: Double, absolutedX: Double) {
    let thresholdX = UIScreen.main.bounds.width * abs(1 - config.thresholdRatioForDeviceWidth)
    if absolutedX < thresholdX {
        delegate?.didDragOverMin(diff: thresholdX - absolutedX)
    }
    ...
}
  • UIScrollView를 가지고 있는 ViewController에서 이 이벤트를 받아서, scrollView의 offset을 갱신하면 완료
    • diff * 1.5를 해주는 이유는 스크롤이 자연스럽게 보이도록 휴리스틱하게 값을 설정한 것
func didDragOverMin(diff: Double) {
    let lastOffsetX = scrollView.contentOffset.x
    let scrollLeftOffsetX = lastOffsetX - diff * 1.5
    guard 0 < scrollLeftOffsetX else { return }
    scrollView.setContentOffset(.init(x: scrollLeftOffsetX, y: 0), animated: true)
}

func didDragOverMax(diff: Double) {
    let lastOffsetX = scrollView.contentOffset.x
    let scrollLeftOffsetX = lastOffsetX + diff * 1.5
    guard scrollLeftOffsetX < scrollView.contentSize.width else { return }
    scrollView.setContentOffset(.init(x: scrollLeftOffsetX, y: 0), animated: true)
}

(완성)

long press 상태에서 화면 끝으로가면 자동으로 scroll되는 기능

* 전체 코드: https://github.com/JK0369/ExHorizontalDragDropStackView

Comments