관리 메뉴

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

[iOS - swift] 2. long press gesture와 애니메이션 - 드래그할때 다른 뷰 줄어들고, 해당 뷰 크게하기 (UIView.animate, CGAffineTransform, concatenating) 본문

UI 컴포넌트 (swift)

[iOS - swift] 2. long press gesture와 애니메이션 - 드래그할때 다른 뷰 줄어들고, 해당 뷰 크게하기 (UIView.animate, CGAffineTransform, concatenating)

jake-kim 2023. 7. 19. 00:12

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 구현)

4. long press gesture와 애니메이션 - gesture 도중 화면 끝으로 가면 자동으로 스크롤되는 기능 구현 (#Horizontal Scroll, #수평 스크롤)

 

drag 시작과 종료 시 뷰 사이즈 줄어들고 크게 효과 적용

뷰 준비

  • 3개의 뷰
    • 하나의 뷰를 drag할 때, 해당 뷰를 크게하고 나머지 두 개의 뷰를 줄어들게하여 drag & drop 애니메이션

  • 코드
    • long gresture 등록 후 began, changed, default에서 각 처리
class ViewController: UIViewController {
    private let someView = UIView()
    private let otherView = UIView()
    private let anotherView = UIView()
    
    override func viewDidLoad() {
    
    	...
        
        let longPressGesture = UILongPressGestureRecognizer()
        longPressGesture.minimumPressDuration = 0.3
        longPressGesture.isEnabled = true
        longPressGesture.delegate = self
        longPressGesture.addTarget(self, action: #selector(handleLongPress))
        someView.addGestureRecognizer(longPressGesture)
    }
    
    @objc private func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
        switch gesture.state {
        case .began:
            handleBegan(gesture)
        case .changed:
            handleChanged(gesture)
        default:
            // ended, canceled, failed
            handleEnded(gesture)
        }
    }
  • hnaldeBegan, handleChanged, handleEnded 코드
    • 뷰를 이동시키는 코드 개념은 이전 포스팅 글 참고
private func handleBegan(_ gesture: UILongPressGestureRecognizer) {
    originalPosition = gesture.location(in: view)
    snapshotedView = someView.snapshotView(afterScreenUpdates: true)
    snapshotedView?.frame = someView.frame
    view.addSubview(snapshotedView!)
    someView.alpha = 0
}

private func handleChanged(_ gesture: UILongPressGestureRecognizer) {
    let newLocation = gesture.location(in: view)
    let xOffset = newLocation.x - originalPosition.x
    let yOffset = newLocation.y - originalPosition.y
    let translation = CGAffineTransform(translationX: xOffset, y: yOffset)
    snapshotedView?.transform = translation
}

private func handleEnded(_ gesture: UILongPressGestureRecognizer) {
    someView.frame.origin = snapshotedView?.frame.origin ?? .zero
    snapshotedView?.alpha = 0
    snapshotedView?.removeFromSuperview()
    someView.alpha = 1
}

애니메이션 적용 아이디어

  • .began에서 이동시킬 뷰의 크기를 키우기 (UIView.animate, transform)
  • .changed에서 이동시킬 뷰의 크기와 위치 동시에 바꾸어주기 (concatenating, transform)

구현

  • UIView.animate를 사용하여 affine 변환이 자연스럽게 동작하도록 적절한 durration과 spring damping을 추가
  • CGAffineTransform의 메소드인 concatenating()을 사용하면 동시에 여러가지 affine 변환이 가능
  • 드래그 시작 시 드래그 시킬 뷰에 효과주기 (concatenating으로 합치기)
    • 이동시킬 뷰의 스캐일을 키우기 (CGAffineTransform(scalex:y:))
    • 이동시킬 뷰의 y 위치를 조금 내리기 (CGAffineTransform(translationX:y:))
  • 나머지 뷰들의 크기를 줄이기 (CGAffineTransform(scaleX:y:))
private func handleBegan(_ gesture: UILongPressGestureRecognizer) {
    originalPosition = gesture.location(in: view)
    snapshotedView = someView.snapshotView(afterScreenUpdates: true)
    snapshotedView?.frame = someView.frame
    view.addSubview(snapshotedView!)
    someView.alpha = 0
    
    // Animation - 다른 뷰는 줄어들게하고, 이동시킬 뷰는 크게하기
    UIView.animate(
        withDuration: 0.4,
        delay: 0,
        usingSpringWithDamping: 0.8,
        initialSpringVelocity: 0,
        options: [.allowUserInteraction, .beginFromCurrentState],
        animations: { self.prepareDragAnimation() },
        completion: nil
    )
}

private func prepareDragAnimation() {
    let upScale = 1.2
    let scaleTranasform = CGAffineTransform(scaleX: upScale, y: upScale)
    
    let downYPosition = 10.0
    let translationTransform = CGAffineTransform(translationX: 0, y: downYPosition)
    snapshotedView?.transform = scaleTranasform.concatenating(translationTransform)
    
    snapshotedView?.alpha = 0.9

    let downScale = 0.8
    [otherView, anotherView]
        .forEach { subview in
            subview.transform = CGAffineTransform(scaleX: downScale, y: downScale)
        }
}
  • .changed 될때 크기를 키운 snapshotedView가 다시 줄어들게 되므로 스캐일 업 하는 코드 추가
    • 스캐일을 업하는 코드와 동시에 이동되어야 하므로 둘 다 affine 변환이기 때문에 concatenating 사용하여 동시에 적용되도록 구현
private func handleChanged(_ gesture: UILongPressGestureRecognizer) {
    let newLocation = gesture.location(in: view)
    let xOffset = newLocation.x - originalPosition.x
    let yOffset = newLocation.y - originalPosition.y
    let translationTransform = CGAffineTransform(translationX: xOffset, y: yOffset)
    
    // Animation - 업스케일 된 이동 시킬 뷰를 계속 업스케일된 상태로 유지하기
    let upScale = 1.2
    let scaleTranasform = CGAffineTransform(scaleX: upScale, y: upScale)
    snapshotedView?.transform = translationTransform.concatenating(scaleTranasform)
}
  • .ended에서 원래의 뷰 보여지게하고, 나머지 뷰들은 다시 scale 1로 되돌리기
    private func handleEnded(_ gesture: UILongPressGestureRecognizer) {
        someView.frame.origin = snapshotedView?.frame.origin ?? .zero
        snapshotedView?.alpha = 0
        snapshotedView?.removeFromSuperview()
        someView.alpha = 1
        
        // Animation - drag 시작할때 적용한 애니메이션 되돌리기
        UIView.animate(
            withDuration: 0.4,
            delay: 0,
            usingSpringWithDamping: 0.8,
            initialSpringVelocity: 0,
            options: [.allowUserInteraction, .beginFromCurrentState],
            animations: { self.animateEndDrop() },
            completion: { _ in
                // Hide the temporaryView, show the actualView
                self.snapshotedView?.removeFromSuperview()
                self.someView.alpha = 1
            }
        )
    }

    func animateEndDrop() {
        snapshotedView?.transform = .identity
        snapshotedView?.alpha = 1.0
        
        [someView, otherView, anotherView]
            .forEach { subview in
                UIView.animate(
                    withDuration: 0.3,
                    animations: {
                        subview.transform = .identity
                    }
                )
            }
    }

(완료)

* 보완하면 좋은 부분: drag 시작, drop 종료 할 때 뷰에 shadow 효과를 주고 radius도 변경해주면 더욱 자연스러운 애니메이션 효과가 가능 (다음 포스팅 글에서 계속)

 

* 전체 코드

https://github.com/JK0369/ExLongPresssAnimation2

Comments