๐—ถ๐—ข๐—ฆ/๐ŸŽ iOS ์‹ค์ „ํŽธ

[iOS / Uikit] ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋ž ๊ตฌํ˜„ํ•˜๊ธฐ :interactiveMovement

z_ero 2025. 12. 4. 14:24

์ตœ๊ทผ์— ๋ฉœ๋ก ๋ฎค์ง ํด๋ก ์ฝ”๋”ฉ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜์˜€๋Š”๋ฐ์š”!
ํ•ด๋‹น ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์ปฌ๋ ‰์…˜๋ทฐ์—์„œ ์…€ ์ „์ฒด๊ฐ€ ์•„๋‹ˆ๋ผ ์…€ ๋‚ด๋ถ€์˜ ํŠน์ • ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ๋งŒ ๋“œ๋ž˜๊ทธ์•ค๋“œ๋ž์ด ๋™์ž‘ํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

 

UIKit์—์„œ Drag & Drop์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ํฌ๊ฒŒ ๋‘ ๊ฐ€์ง€๊ฐ€ ์žˆ์–ด์š”.


1. UICollectionViewDragDelegate์™€ UICollectionViewDropDelegate๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹
2. LongPress ์ œ์Šค์ฒ˜๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ์‹

๋‘ ๊ฐ€์ง€ ๋ฐฉ์‹์ด ์–ด๋–ป๊ฒŒ ๋‹ค๋ฅธ์ง€ ๊ฐ„๋‹จํžˆ ์ •๋ฆฌํ•ด๋ณผ๊นŒ์š”?


1) UICollectionViewDragDelegate · UICollectionViewDropDelegate ์‚ฌ์šฉ

UIKit์—์„œ ๊ณต์‹์ ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” Drag & Drop ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

iOS 11 ์ดํ›„๋กœ ๋„์ž…๋œ ์‹œ์Šคํ…œ ์ธํ„ฐ๋ž™์…˜์ด๋ผ ๊ธฐ๋ณธ ์ œ์Šค์ฒ˜์™€ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋™์ž‘ํ•˜๊ณ ,

๋ฏธ๋ฆฌ ์ œ๊ณต๋˜๋Š” ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ๊ทธ๋Œ€๋กœ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์–ด์š”.


์žฅ์ 

  • ์‹œ์Šคํ…œ์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋ณธ ๋“œ๋ž˜๊ทธ ๋™์ž‘์ด ๋งค์šฐ ์ž์—ฐ์Šค๋Ÿฝ์Šต๋‹ˆ๋‹ค.
  • ๋‹ค๋ฅธ ์ œ์Šค์ฒ˜์™€ ์ถฉ๋Œ์ด ๊ฑฐ์˜ ์—†์Šต๋‹ˆ๋‹ค.
  • drag / drop ์ƒํƒœ์— ๋”ฐ๋ผ delegate๊ฐ€ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์–ด ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ํŽธํ•ฉ๋‹ˆ๋‹ค.
  • ์™ธ๋ถ€ ์•ฑ ๊ฐ„ drag & drop๋„ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

๋‹จ์ 

  • ์…€ ์ „์ฒด๋ฅผ ๊ธธ๊ฒŒ ๋ˆŒ๋ €์„ ๋•Œ๋งŒ ๋“œ๋ž˜๊ทธ๊ฐ€ ์‹œ์ž‘๋˜๊ธฐ ๋•Œ๋ฌธ์—, ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ ๋•Œ๋งŒ ๋“œ๋ž˜๊ทธ๋ฅผ ์‹œ์ž‘ํ•ด์•ผ ํ•˜๋Š” UI์™€๋Š” ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • ๋“œ๋ž˜๊ทธ ์ค‘ ์…€ ์™ธํ˜•(์ƒ‰์ƒ, ๊ทธ๋ฆผ์ž ๋“ฑ)์„ ์ง์ ‘ ์ œ์–ดํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.


2) LongPressGestureRecognizer๋ฅผ ์ด์šฉํ•œ interactiveMovement

๋‘ ๋ฒˆ์งธ ๋ฐฉ์‹์€ LongPress ์ œ์Šค์ฒ˜๋ฅผ ์ปฌ๋ ‰์…˜๋ทฐ(๋˜๋Š” ํŠน์ • ๋ฒ„ํŠผ) ์œ„์— ์ง์ ‘ ๋‹ฌ๊ณ , ์ œ์Šค์ฒ˜ ์ƒํƒœ์— ๋”ฐ๋ผ
beginInteractiveMovement, updateInteractiveMovement, endInteractiveMovement๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

 

์žฅ์ 

  • ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ์„œ๋งŒ ๋“œ๋ž˜๊ทธ๋ฅผ ์‹œ์ž‘ํ•˜๋Š” UI์ฒ˜๋Ÿผ ์ปค์Šคํ…€ ์š”๊ตฌ์‚ฌํ•ญ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์ข‹์Šต๋‹ˆ๋‹ค.
  • ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ ์‹œ ์…€ ํ™•๋Œ€, ๊ทธ๋ฆผ์ž ์ถ”๊ฐ€ ๋“ฑ ๋น„์ฃผ์–ผ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์ด ์ž์œ ๋กญ์Šต๋‹ˆ๋‹ค.
  • ์‹œ์Šคํ…œ DragDelegate๋ณด๋‹ค ๋™์ž‘ ์ œ์–ด ๋ฒ”์œ„๊ฐ€ ๋„“์Šต๋‹ˆ๋‹ค.

๋‹จ์ 

  • x์ถ• ํ”๋“ค๋ฆผ, indexPath nil ๋ฌธ์ œ ๋“ฑ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ์ง์ ‘ ํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ์‹œ์Šคํ…œ ๊ธฐ๋ณธ Drag & Drop๋ณด๋‹ค ์ œ์Šค์ฒ˜ ์ถฉ๋Œ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ €๋Š” ์…€ ์ „์ฒด๊ฐ€ ์•„๋‹ˆ๋ผ ์…€ ์•ˆ์˜ ํŠน์ • ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ๋งŒ ๋“œ๋ž˜๊ทธ์•ค๋“œ๋ž์ด ์‹œ์ž‘๋˜์–ด์•ผ ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์—, 
LongPressGestureRecognizer๋ฅผ ์‚ฌ์šฉํ•ด ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค!


1. ๋ฒ„ํŠผ์— LongPress ์ œ์Šค์ฒ˜ ์—ฐ๊ฒฐํ•˜๊ธฐ

๋จผ์ € ๋“œ๋ž˜๊ทธ๋ฅผ ์‹œ์ž‘ํ•  ๋ฒ„ํŠผ(menuIconView)์— LongPress ์ œ์Šค์ฒ˜๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

menuIconView.isUserInteractionEnabled = true
menuIconView.addGestureRecognizer(
    UILongPressGestureRecognizer(target: self, action: #selector(handleDrag(_:)))
)


UILongPressGestureRecognizer์˜ ๊ธฐ๋ณธ press duration์€ 0.5์ดˆ์ž…๋‹ˆ๋‹ค.

๋ฐ˜์‘ ์‹œ๊ฐ„์„ ๋” ๋น ๋ฅด๊ฒŒ ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์•„๋ž˜์ฒ˜๋Ÿผ ์กฐ์ ˆํ•  ์ˆ˜ ์žˆ์–ด์š”.

let longPress = UILongPressGestureRecognizer(target: self,
                                             action: #selector(handleDrag(_:)))
longPress.minimumPressDuration = 0.0   // ์›ํ•˜๋Š” ์‹œ๊ฐ„์œผ๋กœ ๋ณ€๊ฒฝ
menuIconView.addGestureRecognizer(longPress)


2. ๋“œ๋ž˜๊ทธ ๋™์ž‘์„ ์ฒ˜๋ฆฌํ•˜๋Š” handleDrag ๊ตฌํ˜„

@objc private func handleDrag(_ gesture: UILongPressGestureRecognizer) {
    guard let cv = superview as? UICollectionView else { return }
    guard let indexPath = cv.indexPath(for: self) else { return }

    switch gesture.state {
    case .began:
        cv.beginInteractiveMovementForItem(at: indexPath)

    case .changed:
        let location = gesture.location(in: cv)

        // ์ขŒ์šฐ๋กœ ํ”๋“ค๋ฆฌ์ง€ ์•Š๋„๋ก x ๊ฐ’์„ ๊ณ ์ •ํ–ˆ์–ด์š”!
        let fixedLocation = CGPoint(x: self.center.x, y: location.y)
        cv.updateInteractiveMovementTargetPosition(fixedLocation)

    case .ended:
        cv.endInteractiveMovement()

    default:
        cv.cancelInteractiveMovement()
    }
}


๋“œ๋ž˜๊ทธ ์ค‘์ธ ์…€์€ ์‚ฌ์šฉ์ž์˜ ์†๊ฐ€๋ฝ ์œ„์น˜๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋”ฐ๋ผ๊ฐ€์•ผ ํ•˜๊ฒ ์ฃ ?

๋”ฐ๋ผ์„œ gesture๊ฐ€ ์ „๋‹ฌํ•˜๋Š” (x, y) ์ขŒํ‘œ ๊ฐ’์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ด๋™ ์œ„์น˜๋ฅผ ๊ณ„์† ์—…๋ฐ์ดํŠธํ•ด์ค˜์•ผ ํ•ด์š”.


๊ทธ๋Ÿฐ๋ฐ ์ด๋•Œ.. ์•„๋ž˜์™€ ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 


interactiveMovement๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ x์ถ•·y์ถ• ์ „์ฒด๋ฅผ ๊ทธ๋Œ€๋กœ ๋”ฐ๋ผ๊ฐ€๊ธฐ ๋•Œ๋ฌธ์—,
๋ฆฌ์ŠคํŠธ๋ทฐ์—์„œ๋Š” ์…€์ด ์ขŒ์šฐ๋กœ ํ”๋“ค๋ฆฌ๋Š” ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ์–ด์š”.


Solution: x์ถ• ๊ณ ์ •์œผ๋กœ ํ”๋“ค๋ฆผ ํ•ด๊ฒฐํ•˜๊ธฐ

๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ ์•„๋ž˜ ์ฝ”๋“œ์ฒ˜๋Ÿผ x ๊ฐ’์„ self.center.x๋กœ ๊ณ ์ •ํ•ด ์ƒํ•˜ ์ด๋™๋งŒ ๊ฐ€๋Šฅํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค!

let fixedLocation = CGPoint(x: self.center.x, y: location.y)
cv.updateInteractiveMovementTargetPosition(fixedLocation)

 

 

๊ทธ๋Ÿผ ์š”๋ ‡๊ฒŒ ํ•ด๊ฒฐ๋ฉ๋‹ˆ๋‹ค~

3. CollectionView ์„ค์ •

๋ทฐ์ปจํŠธ๋กค๋Ÿฌ์—์„œ๋„ dragInteraction์„ ๋น„ํ™œ์„ฑํ™”ํ•˜๊ณ  ๊ธฐ๋ณธ ์„ค์ •์„ ๋„ฃ์–ด์คฌ์Šต๋‹ˆ๋‹ค.

private func setupCollectionView() {
    cv.dragInteractionEnabled = false
    cv.isUserInteractionEnabled = true
    cv.isScrollEnabled = true
}


๊ทธ๋ฆฌ๊ณ  ์ด๋™๋œ ์ˆœ์„œ๋ฅผ ์‹ค์ œ ๋ฐ์ดํ„ฐ์— ๋ฐ˜์˜ํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ๊ผญ ํ•„์š”ํ•ด์š”!

extension MixUpViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView,
                        moveItemAt sourceIndexPath: IndexPath,
                        to destinationIndexPath: IndexPath) {

        let moved = music.remove(at: sourceIndexPath.item)
        music.insert(moved, at: destinationIndexPath.item)
    }
}

 

๋ฐ์ดํ„ฐ ๋ฐฐ์—ด์„ ์—…๋ฐ์ดํŠธํ•˜์ง€ ์•Š์œผ๋ฉด UI๋Š” ์›€์ง์ด์ง€๋งŒ ์‹ค์ œ ๋ชจ๋ธ์˜ ์ˆœ์„œ๋Š” ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.


 

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ธฐ๋ณธ์ ์ธ ๋“œ๋ž˜๊ทธ์•ค๋“œ๋ž ๊ธฐ๋Šฅ์€ ์™„์„ฑ๋ฉ๋‹ˆ๋‹ค.

 

 

ํ•˜์ง€๋งŒ ์—ฌ๊ธฐ๊นŒ์ง€ ๊ตฌํ˜„ํ•˜๋ฉด ์กฐ๊ธˆ ๋ฐ‹๋ฐ‹ํ•˜๊ฒŒ ๋А๊ปด์งˆ ์ˆ˜ ์žˆ์–ด์š”.

์ด์ œ ๋“œ๋ž˜๊ทธ์•ค๋“œ๋ž์— ์‹œ๊ฐ์  ํšจ๊ณผ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณผ๊นŒ์š”?

 

+ ๋“œ๋ž˜๊ทธ ์‹œ ์…€ ํ™•๋Œ€ ํšจ๊ณผ ๋„ฃ๊ธฐ

case .began:
    cv.beginInteractiveMovementForItem(at: indexPath)

    UIView.animate(withDuration: 0.15) {
        self.transform = CGAffineTransform(scaleX: 1.03, y: 1.03)
    }

case .ended:
    cv.endInteractiveMovement()

    UIView.animate(withDuration: 0.15) {
        self.transform = .identity
    }

default:
    cv.cancelInteractiveMovement()
    UIView.animate(withDuration: 0.15) {
        self.transform = .identity
    }

 

+ ๋“œ๋ž˜๊ทธ ์ค‘ ๊ทธ๋ฆผ์ž ํšจ๊ณผ ์ถ”๊ฐ€ํ•˜๊ธฐ

case .began:
    UIView.animate(withDuration: 0.15) {
        self.layer.shadowColor = UIColor.black.cgColor
        self.layer.shadowOpacity = 0.25
        self.layer.shadowRadius = 8
        self.layer.shadowOffset = CGSize(width: 0, height: 3)
    }

case .ended:
    UIView.animate(withDuration: 0.15) {
        self.layer.shadowOpacity = 0
        self.transform = .identity
    }

 

๋ชจ๋“  ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด, ๊ฒฐ๊ณผ๋Š” ์š”๋ ‡๊ฒŒ ๋‚˜์˜ต๋‹ˆ๋‹ค~~! 

๋ฐ˜์‘ํ˜•