UIKit
UIKit์ด ์ด์ด์ฃผ๋ ์๋ก์ด ์ฐจ์์ ํ๋ถํ ๊ทธ๋ํฝ์ค์ ๋ฌผ๋ฆฌ ์์ง.
๊ฐ์
UIKit Dynamics๋ ์ผ๋ฐ UIView์ ์ค์๊ฐ ๋ฌผ๋ฆฌ ์๋ฎฌ๋ ์ด์ ์ ์ ์ฉํ๋ ํ๋ ์์ํฌ์ ๋๋ค. ๊ฒ์ ์์ง์ ๋์ ํ์ง ์๊ณ ๋ ์นด๋, ๋ฒํผ, ์ด๋ฏธ์ง ๊ฐ์ ํ๋ฒํ UI ์์์ ์ค๋ ฅยท์ถฉ๋ยทํ์ฑ์ ๋ถ์ฌํ ์ ์์ด, ์ธํฐ๋ํฐ๋ธ UI์ โ๋ฌผ๋ฆฌ์ ์ธ ์๋งโ์ ๋ํ ๋ ๊ฐ์ฅ ์ ํฉํฉ๋๋ค.
ํต์ฌ ๊ตฌ์กฐ:
UIDynamicAnimatorโ ๋ฌผ๋ฆฌ ์์ง ๋ณธ์ฒด.referenceView๋ผ๋ ์ขํ ๊ณต๊ฐ ์์์ ์๋ฎฌ๋ ์ด์ ์ ๋๋ฆฝ๋๋ค.UIDynamicBehaviorโ ๊ฐ์ง ๋ฌผ๋ฆฌ ๋ฒ์น. ์ค๋ ฅ/์ถฉ๋/์ค๋ /์ดํ์น๋จผํธ ๋ฑ์ ์กฐํฉํด ๋์์ ๊ตฌ์ฑํฉ๋๋ค.
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)
}
ํต์ฌ ํฌ์ธํธ:
gravityDirection์ ํ๋ฉด ์ขํ(top-left origin) ๊ธฐ์ค ๋ฒกํฐ. ๋๋ฐ์ด์ค ์ขํ๊ฐ ์๋๋ฏ๋ก ์ธํฐํ์ด์ค ํ์ ์ ๋ฐ๋ผ ๋งคํ์ด ํ์ํฉ๋๋ค.- ์นด๋๋ผ๋ฆฌ ์ถฉ๋ยทํ์ด ์ค๋ฅด๋๋ก
UICollisionBehavior+UIDynamicItemBehavior(elasticity:)๋ ํจ๊ป ์ ์ฉ. - ํ๋ฉด ํ์ ์ ๋ง์์ผ ์ค๋ ฅ์ด ์ผ๊ด๋๋ฏ๋ก ์ด ๋ฐ๋ชจ๋ Landscape Lock์ด ๊ฑธ๋ ค ์์ต๋๋ค.
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
- UIKit Dynamics๋ ๊ธฐ์กด UIView๋ฅผ ๊ทธ๋๋ก ๋ฌผ๋ฆฌ ๊ฐ์ฒด๋ก ์ฐ๋ฏ๋ก, SwiftUI ํ๋ฉด์ ๋ถ๋ถ์ ์ผ๋ก ๋ฌผ๋ฆฌ ์ธํฐ๋์ ์ ๋ฃ๊ณ ์ถ์ ๋ ๊ฐ์ฅ ๊ฐ๋ณ์ต๋๋ค.
UISnapBehavior.damping(0~1)์ผ๋ก ์ค๋ ์ โํ์ฑ ๋๋โ์ ์กฐ์ . 0.4~0.6์ด ๋ณดํธ์ ์ผ๋ก ์์ฐ์ค๋ฌ์.- ์๋ฎฌ๋ ์ด์
์ด ์์ ํ๋๋ฉด ์๋ ์ผ์์ ์ง๋์ง๋ง, ์ ์ฐ๋ Behavior๋ ๋ช
์์ ์ผ๋ก
removeBehavior.
์ฃผ์ ์ฌํญ
- SwiftUI์์๋ ๋ฐ๋์
UIViewRepresentable๋๋UIViewControllerRepresentable๋ํผ ํ์. - Auto Layout๊ณผ ์ถฉ๋ํ๋ฏ๋ก, ๋ฌผ๋ฆฌ ๋์ ๋ทฐ๋ frame ๊ธฐ๋ฐ์ผ๋ก ๋ฐฐ์นํ์ธ์.
UIDynamicAnimator์referenceView๊ฐ ํด์ ๋๋ฉด ์๋ฎฌ๋ ์ด์ ์ด ๋ฉ์ถฅ๋๋ค. ๊ฐํ ์ฐธ์กฐ๋ฅผ ์ ์ง.- ๊ฐ์ฒด 100๊ฐ ์ด์ + ๋ณต์กํ Behavior ์กฐํฉ์ CPU ๋ณ๋ชฉ. ๊ทธ ์์ ์ด๋ฉด SpriteKit์ผ๋ก ์ ํ์ ๊ณ ๋ ค.