๊ฐœ์š”

UIKit Dynamics๋Š” ์ผ๋ฐ˜ UIView์— ์‹ค์‹œ๊ฐ„ ๋ฌผ๋ฆฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ์ ์šฉํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. ๊ฒŒ์ž„ ์—”์ง„์„ ๋„์ž…ํ•˜์ง€ ์•Š๊ณ ๋„ ์นด๋“œ, ๋ฒ„ํŠผ, ์ด๋ฏธ์ง€ ๊ฐ™์€ ํ‰๋ฒ”ํ•œ UI ์š”์†Œ์— ์ค‘๋ ฅยท์ถฉ๋Œยทํƒ„์„ฑ์„ ๋ถ€์—ฌํ•  ์ˆ˜ ์žˆ์–ด, ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ UI์— โ€œ๋ฌผ๋ฆฌ์ ์ธ ์†๋ง›โ€์„ ๋”ํ•  ๋•Œ ๊ฐ€์žฅ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

ํ•ต์‹ฌ ๊ตฌ์กฐ:

animator = UIDynamicAnimator(referenceView: view)

let gravity   = UIGravityBehavior(items: [card])
let collision = UICollisionBehavior(items: [card])
collision.translatesReferenceBoundsIntoBoundary = true
let item      = UIDynamicItemBehavior(items: [card])
item.elasticity = 0.6

[gravity, collision, item].forEach { animator.addBehavior($0) }

์ด ๋ชจ๋“ˆ์˜ 5๊ฐœ ๋ฐ๋ชจ๋Š” ๊ฐ๊ฐ ๋‹ค๋ฅธ Behavior ์กฐํ•ฉ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.


1. Gravity Cards

๋ฌด์—‡์„ ๋ฐฐ์šฐ๋‚˜ โ€” UIGravityBehavior๋กœ ์ค‘๋ ฅ์„ ์ ์šฉํ•˜๊ณ  CoreMotion์œผ๋กœ ๋””๋ฐ”์ด์Šค ๊ธฐ์šธ๊ธฐ์— ๋”ฐ๋ผ ์ค‘๋ ฅ ๋ฐฉํ–ฅ์„ ํšŒ์ „์‹œํ‚ค๋Š” ํŒจํ„ด.

let gravity = UIGravityBehavior(items: cards)

motionManager.startDeviceMotionUpdates(to: .main) { motion, _ in
    guard let m = motion else { return }
    // ํ™”๋ฉด ์ขŒํ‘œ๊ณ„๋กœ ๋ณ€ํ™˜๋œ ์ค‘๋ ฅ ๋ฒกํ„ฐ
    let v = OrientationAwareGravity.uiKitVector(
        deviceX: m.gravity.x, deviceY: m.gravity.y
    )
    gravity.gravityDirection = CGVector(dx: v.dx * 2, dy: v.dy * 2)
}

ํ•ต์‹ฌ ํฌ์ธํŠธ:


2. Snap Grid

๋ฌด์—‡์„ ๋ฐฐ์šฐ๋‚˜ โ€” UISnapBehavior๋กœ ๋“œ๋ž˜๊ทธํ•œ ํ•ญ๋ชฉ์„ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๊ทธ๋ฆฌ๋“œ ํฌ์ธํŠธ์— ์ž์„์ฒ˜๋Ÿผ ๋Œ์–ด๋‹น๊ธฐ๋Š” ํŒจํ„ด.

@objc func handlePan(_ gesture: UIPanGestureRecognizer) {
    guard let item = gesture.view else { return }
    switch gesture.state {
    case .began:
        // ๋“œ๋ž˜๊ทธ ์ค‘์—๋Š” snap ์ œ๊ฑฐ
        if let snap = snapBehaviors[item] { animator?.removeBehavior(snap) }
    case .changed:
        item.center = gesture.location(in: containerView)
        animator?.updateItem(usingCurrentState: item)
    case .ended:
        // ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๊ทธ๋ฆฌ๋“œ ํฌ์ธํŠธ๋กœ snap
        let nearest = nearestGridPoint(to: item.center)
        let snap = UISnapBehavior(item: item, snapTo: nearest)
        snap.damping = currentDamping
        animator?.addBehavior(snap)
        snapBehaviors[item] = snap
    default: break
    }
}

damping ์Šฌ๋ผ์ด๋”(0~1)๋กœ ์–ผ๋งˆ๋‚˜ ํŠ•๊ธฐ๋Š” ์Šค๋ƒ…์ธ์ง€ ์กฐ์ ˆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 0์— ๊ฐ€๊นŒ์šธ์ˆ˜๋ก ์ง„๋™, 1์ด๋ฉด ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์ •์ฐฉ.


3. Collision Bubbles

๋ฌด์—‡์„ ๋ฐฐ์šฐ๋‚˜ โ€” UICollisionBehavior๋กœ ์—ฌ๋Ÿฌ ๊ฐ์ฒด๊ฐ€ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ž๋ฆฌ ์žก๋Š” ํƒœ๊ทธ ํด๋ผ์šฐ๋“œ๋ฅผ ๋งŒ๋“œ๋Š” ํŒจํ„ด.

let collision = UICollisionBehavior(items: bubbles)
collision.translatesReferenceBoundsIntoBoundary = true

let item = UIDynamicItemBehavior(items: bubbles)
item.elasticity = 0.6
item.allowsRotation = false   // ๊ธ€์ž ๋˜‘๋ฐ”๋กœ ์œ ์ง€

// ํƒญ ์‹œ ์ฃผ๋ณ€์œผ๋กœ ๋ฐ€์–ด๋‚ด๊ธฐ
let push = UIPushBehavior(items: [tappedBubble], mode: .instantaneous)
push.angle = .random(in: 0..<2 * .pi)
push.magnitude = 1.0
animator?.addBehavior(push)

UIPushBehavior์˜ .instantaneous ๋ชจ๋“œ๋Š” ํ•œ ๋ฒˆ์˜ ์ถฉ๊ฒฉ(์ฃผ๋จน์งˆ ํ•œ ๋ฒˆ)์„, .continuous ๋ชจ๋“œ๋Š” ์ง€์†์ ์ธ ํž˜(๋ฐ”๋žŒ ๊ฐ™์€)์„ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค.


4. Pendulum (Newtonโ€™s Cradle)

๋ฌด์—‡์„ ๋ฐฐ์šฐ๋‚˜ โ€” UIAttachmentBehavior๋กœ ์ค„/์Šคํ”„๋ง ์—ฐ๊ฒฐ์„ ๋งŒ๋“ค๊ณ , ์ค‘๋ ฅยท์ถฉ๋Œ๊ณผ ๊ฒฐํ•ฉํ•ด ์šด๋™๋Ÿ‰ ์ „๋‹ฌ์„ ๋ณด์—ฌ์ฃผ๋Š” ํŒจํ„ด.

for ball in balls {
    let attachment = UIAttachmentBehavior(
        item: ball,
        attachedToAnchor: anchorAbove(ball)
    )
    attachment.length = stringLength
    attachment.damping = 0      // ์ง„์ž ์šด๋™ ๋ฌด์†์‹ค
    animator?.addBehavior(attachment)
}

// ๋ชจ๋“  ๊ณต์ด ์„œ๋กœ ์ถฉ๋Œ
let collision = UICollisionBehavior(items: balls)
let item = UIDynamicItemBehavior(items: balls)
item.elasticity = 1.0           // ์™„์ „ ํƒ„์„ฑ ์ถฉ๋Œ
animator?.addBehavior(collision)
animator?.addBehavior(item)

๐Ÿ’ก ๋“œ๋ž˜๊ทธ ์ธํ„ฐ๋ž™์…˜์€ UISnapBehavior๋กœ: ์†์œผ๋กœ ๊ณต์„ ์žก์„ ๋•Œ๋Š” UISnapBehavior(item:snapTo:)๋กœ ์†๊ฐ€๋ฝ ์œ„์น˜์— ๋ถ€์ฐฉํ•˜๊ณ , ์†์„ ๋–ผ๋ฉด snap์„ ์ œ๊ฑฐํ•ด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋†“์•„์ค๋‹ˆ๋‹ค. (rafcio2k์˜ NewtonsCradlePlayground ํŒจํ„ด)


5. Elastic Menu

๋ฌด์—‡์„ ๋ฐฐ์šฐ๋‚˜ โ€” UIAttachmentBehavior์˜ ์Šคํ”„๋ง ๋ชจ๋“œ๋กœ ํ•ญ๋ชฉ๋“ค์„ ์ด์ƒ์  ์œ„์น˜์— ๋งค๋‹ฌ๊ณ , ํ•ญ๋ชฉ ์‚ฌ์ด๋ฅผ ์•ฝํ•œ ์Šคํ”„๋ง์œผ๋กœ ์—ฐ๊ฒฐํ•ด ์ฒด์ธ์ฒ˜๋Ÿผ ๋”ฐ๋ผ์˜ค๋Š” ๋ฉ”๋‰ด๋ฅผ ๋งŒ๋“œ๋Š” ํŒจํ„ด.

// ๊ฐ ๋ฉ”๋‰ด ์•„์ดํ…œ์„ ์ž๊ธฐ์˜ "๊ณ ์ • ์œ„์น˜"์— ์Šคํ”„๋ง์œผ๋กœ ๋งค๋‹ฎ
for (i, item) in items.enumerated() {
    let anchor = idealPosition(for: i)
    let spring = UIAttachmentBehavior(item: item, attachedToAnchor: anchor)
    spring.length = 0
    spring.damping = 0.5
    spring.frequency = 3.0
    animator?.addBehavior(spring)
}

// ์ธ์ ‘ ์•„์ดํ…œ๋ผ๋ฆฌ๋„ ์•ฝํ•œ ์Šคํ”„๋ง์œผ๋กœ ์—ฐ๊ฒฐ
for i in 0..<(items.count - 1) {
    let chain = UIAttachmentBehavior(item: items[i], attachedTo: items[i+1])
    chain.damping = 0.5
    chain.frequency = 2.0
    animator?.addBehavior(chain)
}

ํ•œ ํ•ญ๋ชฉ์„ ๋‹น๊ธฐ๋ฉด ์ž๊ธฐ anchor๊ฐ€ ๋Œ์–ด๋‹น๊ธฐ๋Š” ํž˜ + ์˜† ํ•ญ๋ชฉ๊ณผ์˜ ์ฒด์ธ ํž˜์ด ๋™์‹œ์— ์ž‘์šฉํ•ด, ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์ถœ๋ ์ด๋Š” ๋ฉ”๋‰ด๊ฐ€ ๋งŒ๋“ค์–ด์ง‘๋‹ˆ๋‹ค.


Behavior ๋น ๋ฅธ ์ฐธ์กฐ

Behavior ์šฉ๋„
UIGravityBehavior ์ผ์ •ํ•œ ๊ฐ€์†๋„(์ค‘๋ ฅ)
UICollisionBehavior ๊ฐ์ฒด ๊ฐ„ ๋˜๋Š” ๊ฒฝ๊ณ„์™€์˜ ์ถฉ๋Œ
UIAttachmentBehavior ๋‘ ์ /๊ฐ์ฒด๋ฅผ ๊ณ ์ •ยท์Šคํ”„๋ง ์—ฐ๊ฒฐ (์ค„, ์Šคํ”„๋ง)
UISnapBehavior ํ•œ ์ ์œผ๋กœ ์ž์„์ฒ˜๋Ÿผ ํก์ธ (๊ฐ์‡  ์ง„๋™)
UIPushBehavior ์ˆœ๊ฐ„ ํž˜(.instantaneous) ๋˜๋Š” ์ง€์† ํž˜(.continuous)
UIDynamicItemBehavior ๋งˆ์ฐฐยทํƒ„์„ฑยท๋ฐ€๋„ยทํšŒ์ „ ํ—ˆ์šฉ ๋“ฑ ๋ฌผ๋ฆฌ ์†์„ฑ

์‹ค์ „ ํŒ

Best Practices

์ฃผ์˜ ์‚ฌํ•ญ