์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŽจ Core Animation๊ณผ UIView ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ๋งŒ๋“œ๋Š” ์‚ด์•„์žˆ๋Š” ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ UI ์„ธ๊ณ„

๐ŸŽจ Core Animation๊ณผ UIView ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ๋งŒ๋“œ๋Š” ์‚ด์•„์žˆ๋Š” ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ UI ์„ธ๊ณ„

iOS ์•ฑ์— ์ƒ๋ช…์„ ๋ถˆ์–ด๋„ฃ๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋งˆ์Šคํ„ฐ ๊ฐ€์ด๋“œ โœจ
์•ˆ๋…•! ์˜ค๋Š˜์€ iOS ๊ฐœ๋ฐœ์—์„œ ์ •๋ง ์ค‘์š”ํ•˜๋ฉด์„œ๋„ ์žฌ๋ฏธ์žˆ๋Š” ์ฃผ์ œ๋ฅผ ๋‹ค๋ค„๋ณผ ๊ฑฐ์•ผ. ๋ฐ”๋กœ Core Animation๊ณผ UIView ์• ๋‹ˆ๋ฉ”์ด์…˜์ด์•ผ! ๐Ÿš€

ํ˜น์‹œ ์•ฑ์„ ์“ฐ๋‹ค๊ฐ€ "์™€, ์ด ์›€์ง์ž„ ์ง„์งœ ๋ถ€๋“œ๋Ÿฝ๋‹ค" ๋˜๋Š” "์ด ํ™”๋ฉด ์ „ํ™˜ ๋„ˆ๋ฌด ์ž์—ฐ์Šค๋Ÿฌ์šด๋ฐ?"๋ผ๊ณ  ๋А๋‚€ ์  ์žˆ์ง€? ๊ทธ๊ฒŒ ๋ฐ”๋กœ ์˜ค๋Š˜ ์šฐ๋ฆฌ๊ฐ€ ๋ฐฐ์šธ ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ํž˜์ด์•ผ. ๋‹จ์ˆœํžˆ ํ™”๋ฉด์— ๋ญ”๊ฐ€๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ๊ณผ ์ƒ๋™๊ฐ ์žˆ๊ฒŒ ์›€์ง์ด๋ฉฐ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ์€ ์™„์ „ํžˆ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๋งŒ๋“ค์–ด๋‚ด๊ฑฐ๋“ .

iOS ๊ฐœ๋ฐœ์„ ๋ฐฐ์šฐ๊ณ  ์žˆ๊ฑฐ๋‚˜, ๋” ๋‚˜์€ UI/UX๋ฅผ ๋งŒ๋“ค๊ณ  ์‹ถ์€ ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ์ด ๊ธ€์ด ์—„์ฒญ ๋„์›€์ด ๋  ๊ฑฐ์•ผ. ์žฌ๋Šฅ๋„ท์—์„œ๋„ iOS ๊ฐœ๋ฐœ ๊ด€๋ จ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•˜๋Š” ๋ถ„๋“ค์ด ๋งŽ์€๋ฐ, ์• ๋‹ˆ๋ฉ”์ด์…˜์€ ์ •๋ง ํ•„์ˆ˜ ์Šคํ‚ฌ์ด๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ˜Š

๐ŸŽฌ ์• ๋‹ˆ๋ฉ”์ด์…˜, ์™œ ์ค‘์š”ํ• ๊นŒ?

๋จผ์ € ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์™œ ์ค‘์š”ํ•œ์ง€๋ถ€ํ„ฐ ์ด์•ผ๊ธฐํ•ด๋ณด์ž. ๋‹จ์ˆœํžˆ "์˜ˆ์˜๊ฒŒ ๋ณด์ด๋ ค๊ณ "๋งŒ์€ ์•„๋‹ˆ์•ผ!

1. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ํ–ฅ์ƒ ๐ŸŒŸ
์• ๋‹ˆ๋ฉ”์ด์…˜์€ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•ด. ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ ์‚ด์ง ๋ˆŒ๋ฆฌ๋Š” ํšจ๊ณผ, ํ™”๋ฉด์ด ์ „ํ™˜๋  ๋•Œ ์Šฌ๋ผ์ด๋“œ ํšจ๊ณผ ๋“ฑ์€ ์‚ฌ์šฉ์ž์—๊ฒŒ "๋„ค, ๋‹น์‹ ์˜ ํ–‰๋™์ด ์ œ๋Œ€๋กœ ์ธ์‹๋์–ด์š”"๋ผ๊ณ  ๋งํ•ด์ฃผ๋Š” ๊ฑฐ์ง€.

2. ๋งฅ๋ฝ ์ œ๊ณต ๐Ÿ“
ํ™”๋ฉด์ด ๊ฐ‘์ž๊ธฐ ๋ฐ”๋€Œ๋ฉด ์‚ฌ์šฉ์ž๋Š” ํ˜ผ๋ž€์Šค๋Ÿฌ์›Œํ•ด. ํ•˜์ง€๋งŒ ๋ถ€๋“œ๋Ÿฌ์šด ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์žˆ์œผ๋ฉด "์•„, ์ด ํ™”๋ฉด์—์„œ ์ € ํ™”๋ฉด์œผ๋กœ ๋„˜์–ด๊ฐ€๋Š”๊ตฌ๋‚˜"๋ผ๊ณ  ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์–ด.

3. ์ฃผ์˜ ์ง‘์ค‘ ๐Ÿ‘€
์ค‘์š”ํ•œ ์ •๋ณด๋‚˜ ์ƒˆ๋กœ์šด ์š”์†Œ๊ฐ€ ๋‚˜ํƒ€๋‚  ๋•Œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด ์‚ฌ์šฉ์ž์˜ ์‹œ์„ ์„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์œ ๋„ํ•  ์ˆ˜ ์žˆ์–ด. ๊ฐ‘์ž๊ธฐ ๋‚˜ํƒ€๋‚˜๋Š” ๊ฒƒ๋ณด๋‹ค ํŽ˜์ด๋“œ ์ธ์ด๋‚˜ ์Šฌ๋ผ์ด๋“œ ์ธ ํšจ๊ณผ๊ฐ€ ํ›จ์”ฌ ํšจ๊ณผ์ ์ด์ง€!

4. ๋ธŒ๋žœ๋“œ ์ •์ฒด์„ฑ ๐ŸŽจ
๋…ํŠนํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์Šคํƒ€์ผ์€ ์•ฑ์˜ ๊ฐœ์„ฑ์„ ๋งŒ๋“ค์–ด์ค˜. ์• ํ”Œ์˜ ๋ถ€๋“œ๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜, ๊ตฌ๊ธ€์˜ ๋จธํ‹ฐ๋ฆฌ์–ผ ๋””์ž์ธ ์• ๋‹ˆ๋ฉ”์ด์…˜์ฒ˜๋Ÿผ ๋ง์ด์•ผ.
๐Ÿ’ก ์žฌ๋ฏธ์žˆ๋Š” ์‚ฌ์‹ค!
์ธ๊ฐ„์˜ ๋ˆˆ์€ ์ดˆ๋‹น ์•ฝ 10-12๊ฐœ์˜ ์ด๋ฏธ์ง€๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋Š”๋ฐ, 24fps(์ดˆ๋‹น ํ”„๋ ˆ์ž„) ์ด์ƒ์ด ๋˜๋ฉด ์šฐ๋ฆฌ๋Š” ๊ทธ๊ฒƒ์„ ์—ฐ์†์ ์ธ ์›€์ง์ž„์œผ๋กœ ์ธ์‹ํ•ด. iOS๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ 60fps๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋ Œ๋”๋งํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์—„์ฒญ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋А๊ปด์ง€๋Š” ๊ฑฐ์•ผ!
์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ์—ญํ•  ๐Ÿ˜Š ๋ฒ„ํŠผ ํด๋ฆญ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ํ–ฅ์ƒ ๋งฅ๋ฝ ์ œ๊ณต ์ฃผ์˜ ์ง‘์ค‘ ๋ธŒ๋žœ๋“œ ์ •์ฒด์„ฑ

๐Ÿ“ฑ UIView ์• ๋‹ˆ๋ฉ”์ด์…˜ - ์‹œ์ž‘์€ ๊ฐ„๋‹จํ•˜๊ฒŒ!

iOS์—์„œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ํฌ๊ฒŒ ๋‘ ๊ฐ€์ง€์•ผ. ๋จผ์ € ๋” ๊ฐ„๋‹จํ•˜๊ณ  ์ง๊ด€์ ์ธ UIView ์• ๋‹ˆ๋ฉ”์ด์…˜๋ถ€ํ„ฐ ์•Œ์•„๋ณด์ž! ๐ŸŽฏ

UIView ์• ๋‹ˆ๋ฉ”์ด์…˜์€ UIKit์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ณ ์ˆ˜์ค€(high-level) API์•ผ. "๊ณ ์ˆ˜์ค€"์ด๋ผ๋Š” ๋ง์€ ๋ณต์žกํ•œ ๋‚ด๋ถ€ ๋™์ž‘์„ ์‹ ๊ฒฝ ์“ฐ์ง€ ์•Š๊ณ ๋„ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๋œป์ด์ง€. ๋งˆ์น˜ ์ž๋™์ฐจ๋ฅผ ์šด์ „ํ•  ๋•Œ ์—”์ง„์˜ ์„ธ๋ถ€ ์ž‘๋™ ์›๋ฆฌ๋ฅผ ๋ชฐ๋ผ๋„ ์šด์ „ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ!

๐ŸŽจ ๊ธฐ๋ณธ UIView ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ฉ”์„œ๋“œ

UIView ํด๋ž˜์Šค๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์œ„ํ•œ ์—ฌ๋Ÿฌ ํด๋ž˜์Šค ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•ด. ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ๊ฒƒ๋ถ€ํ„ฐ ์‚ดํŽด๋ณด์ž!

1. animate(withDuration:animations:) ๐Ÿ“
๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ฉ”์„œ๋“œ์•ผ. ์ง€์ •ํ•œ ์‹œ๊ฐ„ ๋™์•ˆ ๋ทฐ์˜ ์†์„ฑ์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์–ด.
UIView.animate(withDuration: 0.3) {
    // ์• ๋‹ˆ๋ฉ”์ด์…˜ํ•  ์†์„ฑ ๋ณ€๊ฒฝ
    myView.alpha = 0.5
    myView.frame.origin.y += 100
    myView.backgroundColor = .blue
}
์œ„ ์ฝ”๋“œ๋Š” 0.3์ดˆ ๋™์•ˆ ๋ทฐ์˜ ํˆฌ๋ช…๋„๋ฅผ 50%๋กœ ๋งŒ๋“ค๊ณ , Y ์œ„์น˜๋ฅผ 100ํฌ์ธํŠธ ์•„๋ž˜๋กœ ์ด๋™์‹œํ‚ค๋ฉฐ, ๋ฐฐ๊ฒฝ์ƒ‰์„ ํŒŒ๋ž€์ƒ‰์œผ๋กœ ๋ฐ”๊ฟ”. ์—„์ฒญ ๊ฐ„๋‹จํ•˜์ง€? ๐Ÿ˜Š

2. animate(withDuration:animations:completion:) โœ…
์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋๋‚œ ํ›„ ์ถ”๊ฐ€ ์ž‘์—…์„ ํ•˜๊ณ  ์‹ถ์„ ๋•Œ ์‚ฌ์šฉํ•ด.
UIView.animate(withDuration: 0.5, animations: {
    myView.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
}) { finished in
    if finished {
        print("์• ๋‹ˆ๋ฉ”์ด์…˜ ์™„๋ฃŒ!")
        // ์ถ”๊ฐ€ ์ž‘์—… ์ˆ˜ํ–‰
    }
}
3. animate(withDuration:delay:options:animations:completion:) ๐ŸŽ›๏ธ
๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ๋ฉ”์„œ๋“œ์•ผ! ๋”œ๋ ˆ์ด์™€ ๋‹ค์–‘ํ•œ ์˜ต์…˜์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด.
UIView.animate(
    withDuration: 0.8,
    delay: 0.2,
    options: [.curveEaseInOut, .allowUserInteraction],
    animations: {
        myView.center = CGPoint(x: 200, y: 300)
        myView.alpha = 1.0
    },
    completion: { _ in
        print("์ด๋™ ์™„๋ฃŒ!")
    }
)
๐Ÿ”ง ์ฃผ์š” ์• ๋‹ˆ๋ฉ”์ด์…˜ ์˜ต์…˜๋“ค

.curveEaseInOut - ์‹œ์ž‘๊ณผ ๋์ด ๋ถ€๋“œ๋Ÿฌ์šด ๊ณก์„  (๊ฐ€์žฅ ์ž์—ฐ์Šค๋Ÿฌ์›€)
.curveEaseIn - ์ฒœ์ฒœํžˆ ์‹œ์ž‘ํ•ด์„œ ๋น ๋ฅด๊ฒŒ ๋๋‚จ
.curveEaseOut - ๋น ๋ฅด๊ฒŒ ์‹œ์ž‘ํ•ด์„œ ์ฒœ์ฒœํžˆ ๋๋‚จ
.curveLinear - ์ผ์ •ํ•œ ์†๋„๋กœ ์ง„ํ–‰
.allowUserInteraction - ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ค‘์—๋„ ์‚ฌ์šฉ์ž ํ„ฐ์น˜ ๊ฐ€๋Šฅ
.repeat - ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ฌดํ•œ ๋ฐ˜๋ณต
.autoreverse - ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์—ญ๋ฐฉํ–ฅ์œผ๋กœ๋„ ์‹คํ–‰

๐ŸŒŠ ์Šคํ”„๋ง ์• ๋‹ˆ๋ฉ”์ด์…˜ - ์ž์—ฐ์Šค๋Ÿฌ์šด ์›€์ง์ž„์˜ ๋น„๋ฐ€

์Šคํ”„๋ง ์• ๋‹ˆ๋ฉ”์ด์…˜์€ ๋ฌผ๋ฆฌ์ ์ธ ์Šคํ”„๋ง์ฒ˜๋Ÿผ ํŠ•๊ธฐ๋Š” ํšจ๊ณผ๋ฅผ ๋งŒ๋“ค์–ด์ค˜. ์ด๊ฒŒ ์ง„์งœ ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ์ƒ๋™๊ฐ ์žˆ๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ํ•ต์‹ฌ์ด์•ผ! ๐ŸŽช
UIView.animate(
    withDuration: 0.6,
    delay: 0,
    usingSpringWithDamping: 0.7,  // 0~1, ๋‚ฎ์„์ˆ˜๋ก ๋” ํŠ•๊น€
    initialSpringVelocity: 0.5,    // ์ดˆ๊ธฐ ์†๋„
    options: .curveEaseInOut,
    animations: {
        myButton.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
    },
    completion: nil
)
damping ๊ฐ’์˜ ์˜๋ฏธ:
โ€ข 1.0 - ํŠ•๊น€ ์—†์ด ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์ •์ง€
โ€ข 0.7 - ์•ฝ๊ฐ„์˜ ํŠ•๊น€ (์ถ”์ฒœ!)
โ€ข 0.3 - ๋งŽ์ด ํŠ•๊น€
โ€ข 0.1 - ์—„์ฒญ ํŠ•๊น€ (๊ณผํ•˜๋ฉด ์–ด์ง€๋Ÿฌ์šธ ์ˆ˜ ์žˆ์Œ)

์‹ค์ œ๋กœ ์•ฑ์„ ๋งŒ๋“ค ๋•Œ๋Š” 0.6~0.8 ์‚ฌ์ด์˜ damping ๊ฐ’์„ ๋งŽ์ด ์‚ฌ์šฉํ•ด. ์ž์—ฐ์Šค๋Ÿฌ์šฐ๋ฉด์„œ๋„ ์ƒ๋™๊ฐ์ด ์žˆ๊ฑฐ๋“ !

๐ŸŽญ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ฐ€๋Šฅํ•œ ์†์„ฑ๋“ค

UIView ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋Š” ์†์„ฑ๋“ค์„ ์ •๋ฆฌํ•ด๋ณผ๊ฒŒ. ์ด๊ฒƒ๋“ค๋งŒ ์•Œ์•„๋„ ์›ฌ๋งŒํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์€ ๋‹ค ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ๐Ÿ’ช
์†์„ฑ ์„ค๋ช… ์˜ˆ์‹œ
frame ๋ทฐ์˜ ์œ„์น˜์™€ ํฌ๊ธฐ view.frame = CGRect(x: 100, y: 200, width: 150, height: 150)
bounds ๋ทฐ์˜ ๋‚ด๋ถ€ ์ขŒํ‘œ๊ณ„ view.bounds.size = CGSize(width: 200, height: 200)
center ๋ทฐ์˜ ์ค‘์‹ฌ์  view.center = CGPoint(x: 200, y: 300)
alpha ํˆฌ๋ช…๋„ (0~1) view.alpha = 0.5
backgroundColor ๋ฐฐ๊ฒฝ์ƒ‰ view.backgroundColor = .systemBlue
transform ํšŒ์ „, ํฌ๊ธฐ, ์ด๋™ ๋ณ€ํ™˜ view.transform = CGAffineTransform(rotationAngle: .pi)
โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ!
๋ชจ๋“  ์†์„ฑ์ด ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ฐ€๋Šฅํ•œ ๊ฑด ์•„๋‹ˆ์•ผ. ์˜ˆ๋ฅผ ๋“ค์–ด isHidden ์†์„ฑ์€ ์• ๋‹ˆ๋ฉ”์ด์…˜๋˜์ง€ ์•Š์•„. ๋Œ€์‹  alpha๋ฅผ 0์œผ๋กœ ๋งŒ๋“ค์–ด์„œ ๋น„์Šทํ•œ ํšจ๊ณผ๋ฅผ ๋‚ผ ์ˆ˜ ์žˆ์ง€!

๐ŸŽฏ ์‹ค์ „ ์˜ˆ์ œ - ๋ฒ„ํŠผ ํƒญ ์• ๋‹ˆ๋ฉ”์ด์…˜

์‹ค์ œ๋กœ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ๋ฒ„ํŠผ ํƒญ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋งŒ๋“ค์–ด๋ณด์ž! ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์‚ด์ง ์ž‘์•„์กŒ๋‹ค๊ฐ€ ๋‹ค์‹œ ์ปค์ง€๋Š” ํšจ๊ณผ์•ผ. ๐Ÿ“ฑ
@IBAction func buttonTapped(_ sender: UIButton) {
    // 1๋‹จ๊ณ„: ๋ฒ„ํŠผ์„ ์ž‘๊ฒŒ ๋งŒ๋“ค๊ธฐ
    UIView.animate(
        withDuration: 0.1,
        animations: {
            sender.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
        },
        completion: { _ in
            // 2๋‹จ๊ณ„: ์›๋ž˜ ํฌ๊ธฐ๋กœ ๋Œ์•„์˜ค๊ธฐ (์Šคํ”„๋ง ํšจ๊ณผ)
            UIView.animate(
                withDuration: 0.3,
                delay: 0,
                usingSpringWithDamping: 0.5,
                initialSpringVelocity: 0.5,
                options: [],
                animations: {
                    sender.transform = .identity
                },
                completion: nil
            )
        }
    )
    
    // ์‹ค์ œ ๋ฒ„ํŠผ ๊ธฐ๋Šฅ ์ˆ˜ํ–‰
    performButtonAction()
}
์ด ์ฝ”๋“œ๋Š” ๋ฒ„ํŠผ์„ ํƒญํ•˜๋ฉด ๋จผ์ € 0.1์ดˆ ๋™์•ˆ 95% ํฌ๊ธฐ๋กœ ์ค„์–ด๋“ค๊ณ , ๊ทธ ๋‹ค์Œ ์Šคํ”„๋ง ํšจ๊ณผ์™€ ํ•จ๊ป˜ ์›๋ž˜ ํฌ๊ธฐ๋กœ ๋Œ์•„์™€. .identity๋Š” ๋ณ€ํ™˜์„ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ํŠน๋ณ„ํ•œ ๊ฐ’์ด์•ผ. ์ •๋ง ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ๋ฐ˜์‘์„ฑ ์ข‹์€ ๋А๋‚Œ์„ ์ค„ ์ˆ˜ ์žˆ์–ด! ๐Ÿ˜Š

โšก Core Animation - ๋” ๊ฐ•๋ ฅํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ์„ธ๊ณ„

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ Core Animation์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์ž! UIView ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ž๋™์ฐจ์˜ ์ž๋™ ๋ณ€์†๊ธฐ๋ผ๋ฉด, Core Animation์€ ์ˆ˜๋™ ๋ณ€์†๊ธฐ ๊ฐ™์€ ๊ฑฐ์•ผ. ๋” ๋ณต์žกํ•˜์ง€๋งŒ ํ›จ์”ฌ ๋” ์„ธ๋ฐ€ํ•œ ์ œ์–ด๊ฐ€ ๊ฐ€๋Šฅํ•˜์ง€! ๐ŸŽฎ

Core Animation์€ iOS์˜ ๋ชจ๋“  ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ๊ธฐ๋ฐ˜์ด ๋˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. ์‚ฌ์‹ค UIView ์• ๋‹ˆ๋ฉ”์ด์…˜๋„ ๋‚ด๋ถ€์ ์œผ๋กœ๋Š” Core Animation์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด. ํ•˜์ง€๋งŒ Core Animation์„ ์ง์ ‘ ์‚ฌ์šฉํ•˜๋ฉด UIView ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ๋Š” ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ฒƒ๋“ค์„ ํ•  ์ˆ˜ ์žˆ์–ด!

๐ŸŽจ CALayer - ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ์‹ค์ œ ์ฃผ์ธ๊ณต

Core Animation์˜ ํ•ต์‹ฌ์€ CALayer์•ผ. ๋ชจ๋“  UIView๋Š” ๋‚ด๋ถ€์— CALayer๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ , ์‹ค์ œ๋กœ ํ™”๋ฉด์— ๊ทธ๋ ค์ง€๋Š” ๊ฑด ์ด ๋ ˆ์ด์–ด์•ผ. ๐Ÿ–ผ๏ธ

UIView๋Š” ํ„ฐ์น˜ ์ด๋ฒคํŠธ๋‚˜ ๋ ˆ์ด์•„์›ƒ ๊ฐ™์€ ๊ณ ์ˆ˜์ค€ ๊ธฐ๋Šฅ์„ ๋‹ด๋‹นํ•˜๊ณ , CALayer๋Š” ์‹ค์ œ ๊ทธ๋ฆฌ๊ธฐ์™€ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋‹ด๋‹นํ•ด. ์—ญํ•  ๋ถ„๋‹ด์ด ๋ช…ํ™•ํ•˜์ง€?
// UIView์˜ ๋ ˆ์ด์–ด์— ์ ‘๊ทผํ•˜๊ธฐ
let myView = UIView()
let layer = myView.layer

// ๋ ˆ์ด์–ด ์†์„ฑ ๋ณ€๊ฒฝ
layer.cornerRadius = 10
layer.borderWidth = 2
layer.borderColor = UIColor.blue.cgColor
layer.shadowOpacity = 0.5
layer.shadowRadius = 5
๐ŸŽฏ CALayer๋กœ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ๋“ค

โ€ข ๋ชจ์„œ๋ฆฌ ๋‘ฅ๊ธ€๊ฒŒ ๋งŒ๋“ค๊ธฐ (cornerRadius)
โ€ข ํ…Œ๋‘๋ฆฌ ์ถ”๊ฐ€ (borderWidth, borderColor)
โ€ข ๊ทธ๋ฆผ์ž ํšจ๊ณผ (shadow ๊ด€๋ จ ์†์„ฑ๋“ค)
โ€ข 3D ๋ณ€ํ™˜ (transform3D)
โ€ข ๋งˆ์Šคํ‚น๊ณผ ํด๋ฆฌํ•‘
โ€ข ๋ณต์žกํ•œ ๊ฒฝ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜
โ€ข ๊ทธ๋ผ๋””์–ธํŠธ ํšจ๊ณผ

๐ŸŽฌ CABasicAnimation - ๊ธฐ๋ณธ ์• ๋‹ˆ๋ฉ”์ด์…˜

Core Animation์˜ ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํด๋ž˜์Šค์•ผ. ์†์„ฑ์˜ ์‹œ์ž‘๊ฐ’๊ณผ ๋๊ฐ’์„ ์ง€์ •ํ•˜๋ฉด ์ž๋™์œผ๋กœ ์ค‘๊ฐ„ ๊ณผ์ •์„ ๊ณ„์‚ฐํ•ด์ค˜! ๐ŸŽฏ
// ์œ„์น˜ ์ด๋™ ์• ๋‹ˆ๋ฉ”์ด์…˜
let animation = CABasicAnimation(keyPath: "position.x")
animation.fromValue = 50
animation.toValue = 300
animation.duration = 1.0
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)

myView.layer.add(animation, forKey: "moveX")

// ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ›„ ์‹ค์ œ ์œ„์น˜๋„ ๋ณ€๊ฒฝํ•ด์•ผ ํ•จ!
myView.layer.position.x = 300
๐Ÿ’ก ์ค‘์š”ํ•œ ํฌ์ธํŠธ!
Core Animation์€ ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ๋ ˆ์ด์–ด๋งŒ ์• ๋‹ˆ๋ฉ”์ด์…˜ํ•ด. ์ฆ‰, ๋ณด์ด๋Š” ๊ฒƒ๋งŒ ์›€์ง์ด๊ณ  ์‹ค์ œ ๊ฐ’์€ ๋ณ€ํ•˜์ง€ ์•Š์•„. ๊ทธ๋ž˜์„œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ›„์— ์‹ค์ œ ์†์„ฑ๊ฐ’๋„ ๋ณ€๊ฒฝํ•ด์ค˜์•ผ ํ•ด! ์ด๊ฒŒ UIView ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ์˜ ํฐ ์ฐจ์ด์ ์ด์•ผ.

๐ŸŒˆ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ฐ€๋Šฅํ•œ CALayer ์†์„ฑ๋“ค

CALayer์—์„œ ์• ๋‹ˆ๋ฉ”์ด์…˜ํ•  ์ˆ˜ ์žˆ๋Š” ์ฃผ์š” ์†์„ฑ๋“ค์„ ์•Œ์•„๋ณด์ž. ์ด๊ฒƒ๋“ค์€ keyPath๋กœ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์–ด!
keyPath ์„ค๋ช… ํƒ€์ž…
position ๋ ˆ์ด์–ด์˜ ์œ„์น˜ CGPoint
opacity ํˆฌ๋ช…๋„ Float (0~1)
backgroundColor ๋ฐฐ๊ฒฝ์ƒ‰ CGColor
cornerRadius ๋ชจ์„œ๋ฆฌ ๋‘ฅ๊ธ€๊ธฐ CGFloat
borderWidth ํ…Œ๋‘๋ฆฌ ๋‘๊ป˜ CGFloat
transform.scale ํฌ๊ธฐ ๋ณ€ํ™˜ CGFloat
transform.rotation ํšŒ์ „ CGFloat (๋ผ๋””์•ˆ)
shadowOpacity ๊ทธ๋ฆผ์ž ํˆฌ๋ช…๋„ Float

๐ŸŽช CAKeyframeAnimation - ๋ณต์žกํ•œ ๊ฒฝ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜

CABasicAnimation์ด A์—์„œ B๋กœ ๊ฐ€๋Š” ์ง์„  ์• ๋‹ˆ๋ฉ”์ด์…˜์ด๋ผ๋ฉด, CAKeyframeAnimation์€ ์—ฌ๋Ÿฌ ์ง€์ ์„ ๊ฑฐ์ณ๊ฐ€๋Š” ๋ณต์žกํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด์•ผ! ๐ŸŽข
// ํ”๋“ค๋ฆผ ํšจ๊ณผ ๋งŒ๋“ค๊ธฐ
let shake = CAKeyframeAnimation(keyPath: "transform.rotation")
shake.values = [0, -0.1, 0.1, -0.1, 0.1, 0]  // ๋ผ๋””์•ˆ ๊ฐ’
shake.keyTimes = [0, 0.2, 0.4, 0.6, 0.8, 1.0]
shake.duration = 0.5

myView.layer.add(shake, forKey: "shake")

// ๊ฒฝ๋กœ๋ฅผ ๋”ฐ๋ผ ์ด๋™ํ•˜๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜
let path = UIBezierPath()
path.move(to: CGPoint(x: 50, y: 50))
path.addCurve(
    to: CGPoint(x: 300, y: 300),
    controlPoint1: CGPoint(x: 100, y: 200),
    controlPoint2: CGPoint(x: 250, y: 100)
)

let pathAnimation = CAKeyframeAnimation(keyPath: "position")
pathAnimation.path = path.cgPath
pathAnimation.duration = 2.0
pathAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)

myView.layer.add(pathAnimation, forKey: "movePath")
์ฒซ ๋ฒˆ์งธ ์˜ˆ์ œ๋Š” ๋ทฐ๋ฅผ ์ขŒ์šฐ๋กœ ํ”๋“œ๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์ด์•ผ. ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ์‹คํŒจํ•  ๋•Œ ๋งŽ์ด ๋ณด์ด๋Š” ๊ทธ ํšจ๊ณผ! ๐Ÿ˜…

๋‘ ๋ฒˆ์งธ ์˜ˆ์ œ๋Š” ๋ฒ ์ง€์–ด ๊ณก์„ ์„ ๋”ฐ๋ผ ์›€์ง์ด๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์ด์•ผ. ์ง์„ ์ด ์•„๋‹Œ ๊ณก์„  ๊ฒฝ๋กœ๋กœ ์ด๋™ํ•  ์ˆ˜ ์žˆ์–ด์„œ ํ›จ์”ฌ ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ์—ญ๋™์ ์ธ ๋А๋‚Œ์„ ์ค„ ์ˆ˜ ์žˆ์ง€!

๐ŸŽญ CAAnimationGroup - ์—ฌ๋Ÿฌ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋™์‹œ์—!

์—ฌ๋Ÿฌ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋™์‹œ์— ์‹คํ–‰ํ•˜๊ณ  ์‹ถ์„ ๋•Œ ์‚ฌ์šฉํ•ด. ์˜ˆ๋ฅผ ๋“ค์–ด ์ด๋™ํ•˜๋ฉด์„œ ๋™์‹œ์— ํšŒ์ „ํ•˜๊ณ  ํฌ๊ธฐ๋„ ๋ณ€ํ•˜๋Š” ๋ณตํ•ฉ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ๐ŸŽจ
// ์ด๋™ ์• ๋‹ˆ๋ฉ”์ด์…˜
let move = CABasicAnimation(keyPath: "position")
move.toValue = CGPoint(x: 200, y: 200)

// ํšŒ์ „ ์• ๋‹ˆ๋ฉ”์ด์…˜
let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.toValue = CGFloat.pi * 2

// ํฌ๊ธฐ ์• ๋‹ˆ๋ฉ”์ด์…˜
let scale = CABasicAnimation(keyPath: "transform.scale")
scale.toValue = 1.5

// ํˆฌ๋ช…๋„ ์• ๋‹ˆ๋ฉ”์ด์…˜
let fade = CABasicAnimation(keyPath: "opacity")
fade.toValue = 0.3

// ๊ทธ๋ฃน์œผ๋กœ ๋ฌถ๊ธฐ
let group = CAAnimationGroup()
group.animations = [move, rotate, scale, fade]
group.duration = 2.0
group.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)

myView.layer.add(group, forKey: "complexAnimation")

// ์‹ค์ œ ๊ฐ’๋„ ๋ณ€๊ฒฝ
myView.layer.position = CGPoint(x: 200, y: 200)
myView.layer.transform = CATransform3DMakeScale(1.5, 1.5, 1.0)
myView.layer.opacity = 0.3
์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋„ค ๊ฐ€์ง€ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋™์‹œ์— ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์‹คํ–‰๋ผ. ์ •๋ง ๋ฉ‹์ง„ ํšจ๊ณผ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! โœจ
Core Animation ๊ตฌ์กฐ UIView ํ„ฐ์น˜ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋ ˆ์ด์•„์›ƒ ๊ด€๋ฆฌ ๊ณ ์ˆ˜์ค€ ์ธํ„ฐํŽ˜์ด์Šค ๐ŸŽฏ CALayer ์‹ค์ œ ๋ Œ๋”๋ง ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฒ˜๋ฆฌ ์ €์ˆ˜์ค€ ๊ทธ๋ž˜ํ”ฝ ๐ŸŽจ UIView Animation CABasic Animation CAKeyframe Animation CAAnimation Group ๊ฐ„๋‹จํ•จ โ†โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ†’ ๊ฐ•๋ ฅํ•จ ๊ณ ์ˆ˜์ค€ โ†โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ†’ ์ €์ˆ˜์ค€

๐ŸŽฏ ์‹ค์ „ ํ…Œํฌ๋‹‰ - ํ”„๋กœ์ฒ˜๋Ÿผ ์• ๋‹ˆ๋ฉ”์ด์…˜ํ•˜๊ธฐ

์ด์ œ ๊ธฐ๋ณธ์„ ์•Œ์•˜์œผ๋‹ˆ, ์‹ค์ „์—์„œ ์ •๋ง ์œ ์šฉํ•œ ํ…Œํฌ๋‹‰๋“ค์„ ์•Œ์•„๋ณด์ž! ์ด๊ฒƒ๋“ค๋งŒ ์•Œ์•„๋„ ๋‹น์‹ ์˜ ์•ฑ์ด ํ›จ์”ฌ ํ”„๋กœํŽ˜์…”๋„ํ•ด ๋ณด์ผ ๊ฑฐ์•ผ. ๐Ÿ’Ž

โฑ๏ธ ํƒ€์ด๋ฐ ํ•จ์ˆ˜์˜ ๋งˆ๋ฒ•

์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ๋А๋‚Œ์€ ํƒ€์ด๋ฐ ํ•จ์ˆ˜์— ๋”ฐ๋ผ ์™„์ „ํžˆ ๋‹ฌ๋ผ์ ธ. ๊ฐ™์€ ์ด๋™ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด๋ผ๋„ ํƒ€์ด๋ฐ ํ•จ์ˆ˜์— ๋”ฐ๋ผ ๊ธ‰ํ•˜๊ฒŒ ๋ณด์ด๊ฑฐ๋‚˜ ์—ฌ์œ ๋กญ๊ฒŒ ๋ณด์ผ ์ˆ˜ ์žˆ์–ด! โฐ
// UIView ์• ๋‹ˆ๋ฉ”์ด์…˜์—์„œ ํƒ€์ด๋ฐ ํ•จ์ˆ˜ ์‚ฌ์šฉ
UIView.animate(
    withDuration: 0.5,
    delay: 0,
    options: .curveEaseInOut,  // ํƒ€์ด๋ฐ ํ•จ์ˆ˜ ์˜ต์…˜
    animations: {
        myView.center.y += 100
    }
)

// Core Animation์—์„œ ์ปค์Šคํ…€ ํƒ€์ด๋ฐ ํ•จ์ˆ˜
let animation = CABasicAnimation(keyPath: "position.y")
animation.toValue = 300
animation.duration = 0.5

// ๋ฒ ์ง€์–ด ๊ณก์„ ์œผ๋กœ ์ปค์Šคํ…€ ํƒ€์ด๋ฐ ๋งŒ๋“ค๊ธฐ
animation.timingFunction = CAMediaTimingFunction(
    controlPoints: 0.25, 0.1, 0.25, 1.0
)

myView.layer.add(animation, forKey: "customTiming")
๐ŸŽจ ์ฃผ์š” ํƒ€์ด๋ฐ ํ•จ์ˆ˜๋“ค

linear - ์ผ์ •ํ•œ ์†๋„ (๊ธฐ๊ณ„์ ์ธ ๋А๋‚Œ)
easeIn - ์ฒœ์ฒœํžˆ ์‹œ์ž‘ (๋ฌด๊ฑฐ์šด ๋ฌผ์ฒด๊ฐ€ ์›€์ง์ด๊ธฐ ์‹œ์ž‘ํ•˜๋Š” ๋А๋‚Œ)
easeOut - ์ฒœ์ฒœํžˆ ๋๋‚จ (๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์ •์ง€ํ•˜๋Š” ๋А๋‚Œ)
easeInEaseOut - ์‹œ์ž‘๊ณผ ๋์ด ๋ถ€๋“œ๋Ÿฌ์›€ (๊ฐ€์žฅ ์ž์—ฐ์Šค๋Ÿฌ์›€) โญ
default - iOS ๊ธฐ๋ณธ๊ฐ’ (easeInEaseOut๊ณผ ๋น„์Šท)

๐ŸŽช ์ฒด์ด๋‹ ์• ๋‹ˆ๋ฉ”์ด์…˜ - ์ˆœ์ฐจ์  ์‹คํ–‰

์—ฌ๋Ÿฌ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ˆœ์„œ๋Œ€๋กœ ์‹คํ–‰ํ•˜๊ณ  ์‹ถ์„ ๋•Œ๊ฐ€ ์žˆ์ง€? ์ด๋Ÿด ๋•Œ๋Š” completion ํ•ธ๋“ค๋Ÿฌ๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๋ผ! ๐Ÿ”—
// 1๋‹จ๊ณ„: ์œ„๋กœ ์ด๋™
UIView.animate(withDuration: 0.3, animations: {
    self.myView.center.y -= 50
}) { _ in
    // 2๋‹จ๊ณ„: ํšŒ์ „
    UIView.animate(withDuration: 0.5, animations: {
        self.myView.transform = CGAffineTransform(rotationAngle: .pi)
    }) { _ in
        // 3๋‹จ๊ณ„: ์›๋ž˜ ์œ„์น˜๋กœ
        UIView.animate(withDuration: 0.3, animations: {
            self.myView.center.y += 50
            self.myView.transform = .identity
        })
    }
}
ํ•˜์ง€๋งŒ ์ด๋ ‡๊ฒŒ ์ค‘์ฒฉ๋˜๋ฉด ์ฝ”๋“œ๊ฐ€ ๋ณต์žกํ•ด์ง€์ง€? ์ด๋Ÿด ๋•Œ๋Š” UIViewPropertyAnimator๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋” ๊น”๋”ํ•ด! (iOS 10 ์ด์ƒ)
// UIViewPropertyAnimator๋กœ ๋” ๊น”๋”ํ•˜๊ฒŒ
let animator1 = UIViewPropertyAnimator(duration: 0.3, curve: .easeOut) {
    self.myView.center.y -= 50
}

animator1.addCompletion { _ in
    let animator2 = UIViewPropertyAnimator(duration: 0.5, curve: .easeInOut) {
        self.myView.transform = CGAffineTransform(rotationAngle: .pi)
    }
    
    animator2.addCompletion { _ in
        let animator3 = UIViewPropertyAnimator(duration: 0.3, curve: .easeIn) {
            self.myView.center.y += 50
            self.myView.transform = .identity
        }
        animator3.startAnimation()
    }
    
    animator2.startAnimation()
}

animator1.startAnimation()

๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์• ๋‹ˆ๋ฉ”์ด์…˜ - ์‚ฌ์šฉ์ž๊ฐ€ ์ œ์–ดํ•˜๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜

UIViewPropertyAnimator์˜ ์ง„์งœ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์€ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ค‘๊ฐ„์— ๋ฉˆ์ถ”๊ฑฐ๋‚˜, ๋˜๋Œ๋ฆฌ๊ฑฐ๋‚˜, ์†๋„๋ฅผ ์กฐ์ ˆํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑฐ์•ผ! ์ด๊ฒŒ ๋ฐ”๋กœ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด์•ผ. ๐ŸŽฏ

์˜ˆ๋ฅผ ๋“ค์–ด ์‚ฌ์šฉ์ž๊ฐ€ ํ™”๋ฉด์„ ๋“œ๋ž˜๊ทธํ•˜๋Š” ์ •๋„์— ๋”ฐ๋ผ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ง„ํ–‰๋„๋ฅผ ์กฐ์ ˆํ•  ์ˆ˜ ์žˆ์–ด. ์•ฑ ์Šคํ† ์–ด์˜ ์นด๋“œ ํ™•์žฅ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ฐ™์€ ๊ฑฐ!
class InteractiveViewController: UIViewController {
    var animator: UIViewPropertyAnimator?
    let cardView = UIView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupGesture()
    }
    
    func setupGesture() {
        let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
        cardView.addGestureRecognizer(pan)
    }
    
    @objc func handlePan(_ gesture: UIPanGestureRecognizer) {
        let translation = gesture.translation(in: view)
        let progress = translation.y / view.bounds.height
        
        switch gesture.state {
        case .began:
            // ์• ๋‹ˆ๋ฉ”์ดํ„ฐ ์ƒ์„ฑ (์ž๋™ ์‹œ์ž‘ ์•ˆ ํ•จ)
            animator = UIViewPropertyAnimator(duration: 0.5, curve: .easeOut) {
                self.cardView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
                self.cardView.center.y += 200
            }
            animator?.pauseAnimation()  // ์ผ์‹œ์ •์ง€ ์ƒํƒœ๋กœ ์‹œ์ž‘
            
        case .changed:
            // ๋“œ๋ž˜๊ทธ ์ง„ํ–‰๋„์— ๋”ฐ๋ผ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ง„ํ–‰
            animator?.fractionComplete = max(0, min(1, progress))
            
        case .ended:
            let velocity = gesture.velocity(in: view).y
            
            if velocity > 0 && progress > 0.5 {
                // ์•„๋ž˜๋กœ ๋น ๋ฅด๊ฒŒ ๋“œ๋ž˜๊ทธ โ†’ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์™„๋ฃŒ
                animator?.continueAnimation(withTimingParameters: nil, durationFactor: 0)
            } else {
                // ๊ทธ ์™ธ โ†’ ์›๋ž˜๋Œ€๋กœ ๋˜๋Œ๋ฆผ
                animator?.isReversed = true
                animator?.continueAnimation(withTimingParameters: nil, durationFactor: 0)
            }
            
        default:
            break
        }
    }
}
์ด ์ฝ”๋“œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์นด๋“œ๋ฅผ ๋“œ๋ž˜๊ทธํ•˜๋Š” ๋งŒํผ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ง„ํ–‰๋˜๊ณ , ์†์„ ๋–ผ๋ฉด ์†๋„์™€ ์œ„์น˜์— ๋”ฐ๋ผ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์™„๋ฃŒํ•˜๊ฑฐ๋‚˜ ๋˜๋Œ๋ ค. ์ •๋ง ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ๋ฐ˜์‘์„ฑ ์ข‹์€ UI๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ๐ŸŽจ

๐ŸŒŸ ํŽ˜์ด๋“œ ์ธ/์•„์›ƒ - ๊ฐ€์žฅ ๋งŽ์ด ์“ฐ๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜

์‹ค์ „์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ค‘ ํ•˜๋‚˜๊ฐ€ ํŽ˜์ด๋“œ ํšจ๊ณผ์•ผ. ๋ทฐ๋ฅผ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋‚˜ํƒ€๋‚˜๊ฒŒ ํ•˜๊ฑฐ๋‚˜ ์‚ฌ๋ผ์ง€๊ฒŒ ํ•  ์ˆ˜ ์žˆ์–ด! โœจ
// ํŽ˜์ด๋“œ ์ธ
func fadeIn(view: UIView, duration: TimeInterval = 0.3) {
    view.alpha = 0
    view.isHidden = false
    
    UIView.animate(withDuration: duration) {
        view.alpha = 1
    }
}

// ํŽ˜์ด๋“œ ์•„์›ƒ
func fadeOut(view: UIView, duration: TimeInterval = 0.3, completion: (() -> Void)? = nil) {
    UIView.animate(withDuration: duration, animations: {
        view.alpha = 0
    }) { _ in
        view.isHidden = true
        completion?()
    }
}

// ํฌ๋กœ์Šค ํŽ˜์ด๋“œ (ํ•œ ๋ทฐ๋Š” ์‚ฌ๋ผ์ง€๊ณ  ๋‹ค๋ฅธ ๋ทฐ๋Š” ๋‚˜ํƒ€๋‚จ)
func crossFade(from: UIView, to: UIView, duration: TimeInterval = 0.3) {
    to.alpha = 0
    to.isHidden = false
    
    UIView.animate(withDuration: duration) {
        from.alpha = 0
        to.alpha = 1
    } completion: { _ in
        from.isHidden = true
    }
}

๐ŸŽฏ ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ๋งŒ๋“ค๊ธฐ

Core Animation์„ ํ™œ์šฉํ•ด์„œ ์ปค์Šคํ…€ ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ํšŒ์ „ํ•˜๋Š” ์› ๋ชจ์–‘์˜ ๋กœ๋”ฉ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด์•ผ. ๐Ÿ”„
class CustomLoadingView: UIView {
    private let circleLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupCircle()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupCircle()
    }
    
    private func setupCircle() {
        // ์›ํ˜• ๊ฒฝ๋กœ ๋งŒ๋“ค๊ธฐ
        let center = CGPoint(x: bounds.midX, y: bounds.midY)
        let radius = min(bounds.width, bounds.height) / 2 - 10
        let path = UIBezierPath(
            arcCenter: center,
            radius: radius,
            startAngle: 0,
            endAngle: .pi * 1.5,
            clockwise: true
        )
        
        // ๋ ˆ์ด์–ด ์„ค์ •
        circleLayer.path = path.cgPath
        circleLayer.fillColor = UIColor.clear.cgColor
        circleLayer.strokeColor = UIColor.systemBlue.cgColor
        circleLayer.lineWidth = 4
        circleLayer.lineCap = .round
        
        layer.addSublayer(circleLayer)
    }
    
    func startAnimating() {
        // ํšŒ์ „ ์• ๋‹ˆ๋ฉ”์ด์…˜
        let rotation = CABasicAnimation(keyPath: "transform.rotation")
        rotation.fromValue = 0
        rotation.toValue = CGFloat.pi * 2
        rotation.duration = 1.0
        rotation.repeatCount = .infinity
        
        circleLayer.add(rotation, forKey: "rotationAnimation")
    }
    
    func stopAnimating() {
        circleLayer.removeAllAnimations()
    }
}
์ด๋ ‡๊ฒŒ ๋งŒ๋“  ๋กœ๋”ฉ ๋ทฐ๋Š” startAnimating()์„ ํ˜ธ์ถœํ•˜๋ฉด ๋ฌดํ•œํžˆ ํšŒ์ „ํ•ด. ์‹œ์Šคํ…œ ๊ธฐ๋ณธ ์ธ๋””์ผ€์ดํ„ฐ๋ณด๋‹ค ํ›จ์”ฌ ๋ฉ‹์ง€์ง€? ๐Ÿ˜Ž

๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™” - ๋ถ€๋“œ๋Ÿฌ์šด 60fps ์œ ์ง€ํ•˜๊ธฐ

์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋งŒ๋“ค ๋•Œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ฑด ์„ฑ๋Šฅ์ด์•ผ. ์•„๋ฌด๋ฆฌ ๋ฉ‹์ง„ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด๋ผ๋„ ๋ฒ„๋ฒ…๊ฑฐ๋ฆฌ๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ๋ง๊ฐ€์ง€๊ฑฐ๋“ ! ๐Ÿ“Š

iOS๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ 60fps(์ดˆ๋‹น 60ํ”„๋ ˆ์ž„)๋กœ ํ™”๋ฉด์„ ์—…๋ฐ์ดํŠธํ•ด. ์ฆ‰, 16.67ms๋งˆ๋‹ค ํ•œ ํ”„๋ ˆ์ž„์„ ๊ทธ๋ ค์•ผ ํ•ด. ์ด ์‹œ๊ฐ„ ์•ˆ์— ๋ชจ๋“  ๊ณ„์‚ฐ๊ณผ ๋ Œ๋”๋ง์ด ๋๋‚˜์•ผ ๋ถ€๋“œ๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๊ฐ€๋Šฅํ•˜์ง€!

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ๋“ค

1. ๋ ˆ์ด์–ด ์†์„ฑ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‚ฌ์šฉํ•˜๊ธฐ ๐ŸŽจ

๊ฐ€๋Šฅํ•˜๋ฉด ๋ ˆ์ด์–ด์˜ ์†์„ฑ์„ ์ง์ ‘ ์• ๋‹ˆ๋ฉ”์ด์…˜ํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ํŠนํžˆ transform, opacity, backgroundColor ๊ฐ™์€ ์†์„ฑ๋“ค์€ GPU์—์„œ ์ฒ˜๋ฆฌ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์—„์ฒญ ๋นจ๋ผ!
// โœ… ์ข‹์€ ์˜ˆ: transform ์‚ฌ์šฉ (GPU ๊ฐ€์†)
UIView.animate(withDuration: 0.3) {
    myView.transform = CGAffineTransform(translationX: 100, y: 0)
}

// โŒ ๋‚˜์œ ์˜ˆ: frame ์ง์ ‘ ๋ณ€๊ฒฝ (CPU ์‚ฌ์šฉ)
UIView.animate(withDuration: 0.3) {
    var frame = myView.frame
    frame.origin.x += 100
    myView.frame = frame
}
2. ๊ทธ๋ฆผ์ž ์ตœ์ ํ™”ํ•˜๊ธฐ ๐ŸŒ‘

๊ทธ๋ฆผ์ž๋Š” ์„ฑ๋Šฅ์— ํฐ ์˜ํ–ฅ์„ ์ค˜. ํŠนํžˆ ๋ณต์žกํ•œ ๋ชจ์–‘์˜ ๋ทฐ์— ๊ทธ๋ฆผ์ž๋ฅผ ์ฃผ๋ฉด ๋งค ํ”„๋ ˆ์ž„๋งˆ๋‹ค ๊ณ„์‚ฐํ•ด์•ผ ํ•ด์„œ ๋А๋ ค์งˆ ์ˆ˜ ์žˆ์–ด.
// โŒ ๋А๋ฆฐ ๋ฐฉ๋ฒ•: ์ž๋™ ๊ทธ๋ฆผ์ž ๊ฒฝ๋กœ ๊ณ„์‚ฐ
layer.shadowOpacity = 0.5
layer.shadowRadius = 5

// โœ… ๋น ๋ฅธ ๋ฐฉ๋ฒ•: ๊ทธ๋ฆผ์ž ๊ฒฝ๋กœ ๋ฏธ๋ฆฌ ์ง€์ •
layer.shadowOpacity = 0.5
layer.shadowRadius = 5
layer.shadowPath = UIBezierPath(rect: layer.bounds).cgPath

// ๋” ๋‚˜์€ ๋ฐฉ๋ฒ•: ๋ž˜์Šคํ„ฐํ™” ์‚ฌ์šฉ
layer.shadowOpacity = 0.5
layer.shadowRadius = 5
layer.shadowPath = UIBezierPath(rect: layer.bounds).cgPath
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
๐Ÿ’ก shouldRasterize์˜ ๋งˆ๋ฒ•!
shouldRasterize = true๋ฅผ ์„ค์ •ํ•˜๋ฉด ๋ ˆ์ด์–ด๋ฅผ ๋น„ํŠธ๋งต์œผ๋กœ ์บ์‹ฑํ•ด. ๋ณต์žกํ•œ ๋ ˆ์ด์–ด๋ฅผ ๋งค๋ฒˆ ๋‹ค์‹œ ๊ทธ๋ฆฌ์ง€ ์•Š์•„๋„ ๋˜๋‹ˆ๊นŒ ์„ฑ๋Šฅ์ด ์ข‹์•„์ ธ! ํ•˜์ง€๋งŒ ๋ ˆ์ด์–ด๊ฐ€ ์ž์ฃผ ๋ณ€ํ•˜๋ฉด ์˜คํžˆ๋ ค ์„ฑ๋Šฅ์ด ๋‚˜๋น ์งˆ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ฃผ์˜ํ•ด์•ผ ํ•ด.
3. ๋ชจ์„œ๋ฆฌ ๋‘ฅ๊ธ€๊ฒŒ ๋งŒ๋“ค๊ธฐ ์ตœ์ ํ™” ๐Ÿ”ฒ

cornerRadius์™€ masksToBounds๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ์˜คํ”„์Šคํฌ๋ฆฐ ๋ Œ๋”๋ง์ด ๋ฐœ์ƒํ•ด์„œ ๋А๋ ค์งˆ ์ˆ˜ ์žˆ์–ด.
// โŒ ๋А๋ฆด ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•
imageView.layer.cornerRadius = 10
imageView.layer.masksToBounds = true

// โœ… ๋” ๋‚˜์€ ๋ฐฉ๋ฒ•: ์ด๋ฏธ์ง€๋ฅผ ๋ฏธ๋ฆฌ ๋‘ฅ๊ธ€๊ฒŒ ๋งŒ๋“ค๊ธฐ
extension UIImage {
    func rounded(cornerRadius: CGFloat) -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(size, false, scale)
        defer { UIGraphicsEndImageContext() }
        
        let rect = CGRect(origin: .zero, size: size)
        UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).addClip()
        draw(in: rect)
        
        return UIGraphicsGetImageFromCurrentImageContext()
    }
}

// ์‚ฌ์šฉ
imageView.image = originalImage.rounded(cornerRadius: 10)
4. ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ค‘ ๋ ˆ์ด์•„์›ƒ ๊ณ„์‚ฐ ํ”ผํ•˜๊ธฐ ๐Ÿ“

์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ธ”๋ก ์•ˆ์—์„œ layoutIfNeeded()๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์ „์ฒด ๋ทฐ ๊ณ„์ธต์˜ ๋ ˆ์ด์•„์›ƒ์„ ๋‹ค์‹œ ๊ณ„์‚ฐํ•ด์•ผ ํ•ด์„œ ๋А๋ ค์งˆ ์ˆ˜ ์žˆ์–ด.
// โŒ ๋А๋ฆฐ ๋ฐฉ๋ฒ•
UIView.animate(withDuration: 0.3) {
    self.view.layoutIfNeeded()  // ์ „์ฒด ๋ ˆ์ด์•„์›ƒ ์žฌ๊ณ„์‚ฐ
}

// โœ… ๋” ๋‚˜์€ ๋ฐฉ๋ฒ•: ํ•„์š”ํ•œ ๋ทฐ๋งŒ ์• ๋‹ˆ๋ฉ”์ด์…˜
UIView.animate(withDuration: 0.3) {
    myView.transform = CGAffineTransform(translationX: 100, y: 0)
}

๐Ÿ” ์„ฑ๋Šฅ ์ธก์ •ํ•˜๊ธฐ

์• ๋‹ˆ๋ฉ”์ด์…˜ ์„ฑ๋Šฅ์„ ์ธก์ •ํ•˜๋ ค๋ฉด Xcode์˜ Instruments๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด. ํŠนํžˆ Core Animation ํ…œํ”Œ๋ฆฟ์ด ์œ ์šฉํ•ด! ๐Ÿ“Š

์ฃผ์š” ์ธก์ • ์ง€ํ‘œ:
โ€ข Frame Rate - ์ดˆ๋‹น ํ”„๋ ˆ์ž„ ์ˆ˜ (60fps๊ฐ€ ๋ชฉํ‘œ)
โ€ข GPU Usage - GPU ์‚ฌ์šฉ๋ฅ 
โ€ข Offscreen Rendering - ์˜คํ”„์Šคํฌ๋ฆฐ ๋ Œ๋”๋ง ๋ฐœ์ƒ ์—ฌ๋ถ€
โ€ข Blended Layers - ๋ ˆ์ด์–ด ๋ธ”๋ Œ๋”ฉ ํšŸ์ˆ˜
๐ŸŽฏ ๋””๋ฒ„๊น… ํŒ!
์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์˜ Debug ๋ฉ”๋‰ด์—์„œ "Color Blended Layers"์™€ "Color Offscreen-Rendered" ์˜ต์…˜์„ ์ผœ๋ฉด ์„ฑ๋Šฅ ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š” ๋ถ€๋ถ„์„ ์‹œ๊ฐ์ ์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด. ๋นจ๊ฐ„์ƒ‰์œผ๋กœ ํ‘œ์‹œ๋˜๋Š” ๋ถ€๋ถ„์ด ์ตœ์ ํ™”๊ฐ€ ํ•„์š”ํ•œ ๊ณณ์ด์•ผ!

๐ŸŽจ ์‹ค์ „ ํ”„๋กœ์ ํŠธ - ์นด๋“œ ์Šค์™€์ดํ”„ ์• ๋‹ˆ๋ฉ”์ด์…˜

์ด์ œ ๋ฐฐ์šด ๊ฒƒ๋“ค์„ ์ข…ํ•ฉํ•ด์„œ ์‹ค์ „ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! Tinder ์Šคํƒ€์ผ์˜ ์นด๋“œ ์Šค์™€์ดํ”„ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด์•ผ. ๐Ÿƒ

์ด ํ”„๋กœ์ ํŠธ์—์„œ๋Š” UIView ์• ๋‹ˆ๋ฉ”์ด์…˜, Core Animation, ์ œ์Šค์ฒ˜ ์ธ์‹, ๊ทธ๋ฆฌ๊ณ  ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋ชจ๋‘ ์‚ฌ์šฉํ•  ๊ฑฐ์•ผ!
class SwipeableCardView: UIView {
    // MARK: - Properties
    private var originalCenter: CGPoint = .zero
    private let threshold: CGFloat = 100  // ์Šค์™€์ดํ”„ ์ž„๊ณ„๊ฐ’
    
    // ์นด๋“œ ์ œ๊ฑฐ ์ฝœ๋ฐฑ
    var onSwipeLeft: (() -> Void)?
    var onSwipeRight: (() -> Void)?
    
    // MARK: - Initialization
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
        setupGesture()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupView()
        setupGesture()
    }
    
    // MARK: - Setup
    private func setupView() {
        // ์นด๋“œ ์Šคํƒ€์ผ ์„ค์ •
        backgroundColor = .white
        layer.cornerRadius = 12
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOpacity = 0.2
        layer.shadowOffset = CGSize(width: 0, height: 2)
        layer.shadowRadius = 8
        
        // ์„ฑ๋Šฅ ์ตœ์ ํ™”
        layer.shadowPath = UIBezierPath(
            roundedRect: bounds,
            cornerRadius: layer.cornerRadius
        ).cgPath
    }
    
    private func setupGesture() {
        let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
        addGestureRecognizer(pan)
    }
    
    // MARK: - Gesture Handling
    @objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
        let translation = gesture.translation(in: superview)
        let velocity = gesture.velocity(in: superview)
        
        switch gesture.state {
        case .began:
            originalCenter = center
            
            // ์นด๋“œ๋ฅผ ์‚ด์ง ํ™•๋Œ€
            UIView.animate(withDuration: 0.2) {
                self.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
            }
            
        case .changed:
            // ์นด๋“œ ์ด๋™
            center = CGPoint(
                x: originalCenter.x + translation.x,
                y: originalCenter.y + translation.y
            )
            
            // ํšŒ์ „ ํšจ๊ณผ (์ด๋™ ๊ฑฐ๋ฆฌ์— ๋น„๋ก€)
            let rotationAngle = translation.x / 500
            let rotationTransform = CGAffineTransform(rotationAngle: rotationAngle)
            let scaleTransform = CGAffineTransform(scaleX: 1.05, y: 1.05)
            transform = rotationTransform.concatenating(scaleTransform)
            
            // ํˆฌ๋ช…๋„ ์กฐ์ ˆ (๋ฉ€๋ฆฌ ๊ฐˆ์ˆ˜๋ก ํˆฌ๋ช…ํ•ด์ง)
            let distance = abs(translation.x)
            alpha = 1 - (distance / 300)
            
        case .ended:
            // ์Šค์™€์ดํ”„ ํŒ์ •
            let shouldSwipe = abs(translation.x) > threshold || abs(velocity.x) > 1000
            
            if shouldSwipe {
                // ์Šค์™€์ดํ”„ ์™„๋ฃŒ
                let direction: CGFloat = translation.x > 0 ? 1 : -1
                swipeAway(direction: direction)
            } else {
                // ์›๋ž˜ ์œ„์น˜๋กœ ๋ณต๊ท€
                resetPosition()
            }
            
        default:
            resetPosition()
        }
    }
    
    // MARK: - Animations
    private func swipeAway(direction: CGFloat) {
        let offScreenX = (superview?.bounds.width ?? 0) * 1.5 * direction
        
        UIView.animate(
            withDuration: 0.4,
            delay: 0,
            usingSpringWithDamping: 0.8,
            initialSpringVelocity: 0.5,
            options: .curveEaseOut,
            animations: {
                self.center.x = self.originalCenter.x + offScreenX
                self.alpha = 0
                
                // ํšŒ์ „ ์ฆ๊ฐ€
                let rotation = CGFloat.pi / 4 * direction
                self.transform = CGAffineTransform(rotationAngle: rotation)
            },
            completion: { _ in
                // ์ฝœ๋ฐฑ ์‹คํ–‰
                if direction > 0 {
                    self.onSwipeRight?()
                } else {
                    self.onSwipeLeft?()
                }
                
                self.removeFromSuperview()
            }
        )
    }
    
    private func resetPosition() {
        UIView.animate(
            withDuration: 0.5,
            delay: 0,
            usingSpringWithDamping: 0.6,
            initialSpringVelocity: 0.5,
            options: .curveEaseOut,
            animations: {
                self.center = self.originalCenter
                self.transform = .identity
                self.alpha = 1
            }
        )
    }
    
    // MARK: - Public Methods
    func swipeRight() {
        swipeAway(direction: 1)
    }
    
    func swipeLeft() {
        swipeAway(direction: -1)
    }
}
์ด ์ฝ”๋“œ๋Š” ์ •๋ง ๋งŽ์€ ๊ฒƒ๋“ค์„ ๋‹ด๊ณ  ์žˆ์–ด! ํ•˜๋‚˜์”ฉ ์‚ดํŽด๋ณด์ž:

1. ์ œ์Šค์ฒ˜ ์ธ์‹ ๐Ÿ–๏ธ
UIPanGestureRecognizer๋กœ ์‚ฌ์šฉ์ž์˜ ๋“œ๋ž˜๊ทธ๋ฅผ ๊ฐ์ง€ํ•ด. began, changed, ended ์ƒํƒœ์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•˜์ง€.

2. ์‹ค์‹œ๊ฐ„ ๋ณ€ํ™˜ ๐Ÿ”„
๋“œ๋ž˜๊ทธํ•˜๋Š” ๋™์•ˆ ์นด๋“œ์˜ ์œ„์น˜, ํšŒ์ „, ํˆฌ๋ช…๋„๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธํ•ด. ์ด๊ฒŒ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ํ•ต์‹ฌ์ด์•ผ!

3. ์Šคํ”„๋ง ์• ๋‹ˆ๋ฉ”์ด์…˜ ๐ŸŒŠ
์นด๋“œ๋ฅผ ๋†“์•˜์„ ๋•Œ ์Šคํ”„๋ง ํšจ๊ณผ๋กœ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์›๋ž˜ ์œ„์น˜๋กœ ๋Œ์•„๊ฐ€๊ฑฐ๋‚˜ ํ™”๋ฉด ๋ฐ–์œผ๋กœ ๋‚ ์•„๊ฐ€. damping: 0.6์œผ๋กœ ์ ๋‹นํ•œ ํŠ•๊น€์„ ์คฌ์–ด.

4. ์ฝœ๋ฐฑ ํŒจํ„ด ๐Ÿ“ž
onSwipeLeft์™€ onSwipeRight ํด๋กœ์ €๋กœ ์Šค์™€์ดํ”„ ์ด๋ฒคํŠธ๋ฅผ ์™ธ๋ถ€์— ์•Œ๋ ค์ค˜. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด!

๐ŸŽฏ ์‚ฌ์šฉ ์˜ˆ์ œ

class CardViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        setupCards()
    }
    
    private func setupCards() {
        let cardView = SwipeableCardView(frame: CGRect(
            x: 50,
            y: 200,
            width: view.bounds.width - 100,
            height: 400
        ))
        
        // ์นด๋“œ ๋‚ด์šฉ ์ถ”๊ฐ€
        let imageView = UIImageView(image: UIImage(named: "profile"))
        imageView.frame = cardView.bounds
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        cardView.addSubview(imageView)
        
        // ์Šค์™€์ดํ”„ ์ฝœ๋ฐฑ ์„ค์ •
        cardView.onSwipeRight = { [weak self] in
            print("์ข‹์•„์š”! โค๏ธ")
            self?.loadNextCard()
        }
        
        cardView.onSwipeLeft = { [weak self] in
            print("๋ณ„๋กœ... ๐Ÿ‘Ž")
            self?.loadNextCard()
        }
        
        view.addSubview(cardView)
    }
    
    private func loadNextCard() {
        // ๋‹ค์Œ ์นด๋“œ ๋กœ๋“œ
        print("๋‹ค์Œ ์นด๋“œ ๋กœ๋”ฉ...")
    }
}
์นด๋“œ ์Šค์™€์ดํ”„ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋™์ž‘ ๊ณผ์ • 1. ์ดˆ๊ธฐ ์ƒํƒœ ๐Ÿ˜Š ์นด๋“œ ๋Œ€๊ธฐ์ค‘ 2. ๋“œ๋ž˜๊ทธ ์ค‘ ๐Ÿค” ํšŒ์ „ + ์ด๋™ 3. ์Šค์™€์ดํ”„ ์™„๋ฃŒ โค๏ธ ํ™”๋ฉด ๋ฐ–์œผ๋กœ ํ•ต์‹ฌ ๊ธฐ์ˆ  UIPanGestureRecognizer CGAffineTransform ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ ์Šคํ”„๋ง ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšŒ์ „ + ํˆฌ๋ช…๋„ ๋ณ€ํ™”

๐ŸŽ“ ๊ณ ๊ธ‰ ํ…Œํฌ๋‹‰ - ๋” ๊นŠ์ด ๋“ค์–ด๊ฐ€๊ธฐ

๊ธฐ๋ณธ์„ ๋„˜์–ด์„œ ๋” ๊ณ ๊ธ‰ ๊ธฐ์ˆ ๋“ค์„ ์•Œ์•„๋ณด์ž! ์ด๊ฒƒ๋“ค์„ ๋งˆ์Šคํ„ฐํ•˜๋ฉด ์ •๋ง ํ”„๋กœํŽ˜์…”๋„ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด. ๐Ÿš€

๐ŸŽญ CADisplayLink - ํ”„๋ ˆ์ž„ ๋‹จ์œ„ ์ œ์–ด

CADisplayLink๋Š” ํ™”๋ฉด ๋ฆฌํ”„๋ ˆ์‹œ ์ฃผ๊ธฐ์— ๋งž์ถฐ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜. 60fps๋กœ ๋™์ž‘ํ•˜๋Š” ๊ฒŒ์ž„์ด๋‚˜ ๋ณต์žกํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋งŒ๋“ค ๋•Œ ์œ ์šฉํ•ด! ๐ŸŽฎ
class CustomAnimationView: UIView {
    private var displayLink: CADisplayLink?
    private var startTime: CFTimeInterval = 0
    private let duration: CFTimeInterval = 2.0
    
    func startAnimation() {
        startTime = CACurrentMediaTime()
        
        displayLink = CADisplayLink(target: self, selector: #selector(updateAnimation))
        displayLink?.add(to: .main, forMode: .common)
    }
    
    @objc private func updateAnimation() {
        let elapsed = CACurrentMediaTime() - startTime
        let progress = min(elapsed / duration, 1.0)
        
        // ์ปค์Šคํ…€ ์ด์ง• ํ•จ์ˆ˜ ์ ์šฉ
        let easedProgress = easeInOutCubic(progress)
        
        // ์• ๋‹ˆ๋ฉ”์ด์…˜ ์—…๋ฐ์ดํŠธ
        layer.position.x = 50 + (300 * easedProgress)
        
        // ์™„๋ฃŒ ์ฒดํฌ
        if progress >= 1.0 {
            displayLink?.invalidate()
            displayLink = nil
        }
    }
    
    // ์ปค์Šคํ…€ ์ด์ง• ํ•จ์ˆ˜
    private func easeInOutCubic(_ t: Double) -> Double {
        if t < 0.5 {
            return 4 * t * t * t
        } else {
            let f = 2 * t - 2
            return 0.5 * f * f * f + 1
        }
    }
    
    deinit {
        displayLink?.invalidate()
    }
}
์ด ๋ฐฉ์‹์€ ๋งค ํ”„๋ ˆ์ž„๋งˆ๋‹ค ์ •ํ™•ํ•œ ์ œ์–ด๊ฐ€ ํ•„์š”ํ•  ๋•Œ ์‚ฌ์šฉํ•ด. ํ•˜์ง€๋งŒ ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ UIView ์• ๋‹ˆ๋ฉ”์ด์…˜์ด๋‚˜ Core Animation์œผ๋กœ ์ถฉ๋ถ„ํ•˜๋‹ˆ๊นŒ ๊ผญ ํ•„์š”ํ•  ๋•Œ๋งŒ ์‚ฌ์šฉํ•˜์ž! ๐Ÿ˜Š

๐ŸŒŠ CATransaction - ์•”์‹œ์  ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ œ์–ด

Core Animation์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์•”์‹œ์  ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ œ๊ณตํ•ด. ๋ ˆ์ด์–ด ์†์„ฑ์„ ๋ณ€๊ฒฝํ•˜๋ฉด ์ž๋™์œผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜๋˜๋Š” ๊ฑฐ์ง€. ํ•˜์ง€๋งŒ ๋•Œ๋กœ๋Š” ์ด๊ฑธ ์ œ์–ดํ•˜๊ณ  ์‹ถ์„ ๋•Œ๊ฐ€ ์žˆ์–ด!
// ์•”์‹œ์  ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋น„ํ™œ์„ฑํ™”
CATransaction.begin()
CATransaction.setDisableActions(true)
layer.backgroundColor = UIColor.red.cgColor
CATransaction.commit()

// ์•”์‹œ์  ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•
CATransaction.begin()
CATransaction.setAnimationDuration(0.5)
CATransaction.setAnimationTimingFunction(
    CAMediaTimingFunction(name: .easeInEaseOut)
)
CATransaction.setCompletionBlock {
    print("์• ๋‹ˆ๋ฉ”์ด์…˜ ์™„๋ฃŒ!")
}

layer.cornerRadius = 20
layer.backgroundColor = UIColor.blue.cgColor

CATransaction.commit()

๐ŸŽจ CAGradientLayer - ๊ทธ๋ผ๋””์–ธํŠธ ์• ๋‹ˆ๋ฉ”์ด์…˜

๊ทธ๋ผ๋””์–ธํŠธ๋„ ์• ๋‹ˆ๋ฉ”์ด์…˜ํ•  ์ˆ˜ ์žˆ์–ด! ์ƒ‰์ƒ์ด ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋ณ€ํ•˜๋Š” ํšจ๊ณผ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์ง€. ๐ŸŒˆ
class GradientAnimationView: UIView {
    private let gradientLayer = CAGradientLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupGradient()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupGradient()
    }
    
    private func setupGradient() {
        gradientLayer.frame = bounds
        gradientLayer.colors = [
            UIColor.systemBlue.cgColor,
            UIColor.systemPurple.cgColor
        ]
        gradientLayer.startPoint = CGPoint(x: 0, y: 0)
        gradientLayer.endPoint = CGPoint(x: 1, y: 1)
        
        layer.addSublayer(gradientLayer)
    }
    
    func animateColors() {
        let animation = CABasicAnimation(keyPath: "colors")
        animation.fromValue = gradientLayer.colors
        animation.toValue = [
            UIColor.systemPink.cgColor,
            UIColor.systemOrange.cgColor
        ]
        animation.duration = 2.0
        animation.autoreverses = true
        animation.repeatCount = .infinity
        
        gradientLayer.add(animation, forKey: "colorChange")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        gradientLayer.frame = bounds
    }
}

๐ŸŽฏ CAShapeLayer - ๊ฒฝ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜

CAShapeLayer๋Š” ๋ฒกํ„ฐ ๊ทธ๋ž˜ํ”ฝ์„ ๊ทธ๋ฆด ์ˆ˜ ์žˆ๋Š” ๋ ˆ์ด์–ด์•ผ. ๊ฒฝ๋กœ๋ฅผ ๋”ฐ๋ผ ๊ทธ๋ ค์ง€๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด์„œ ์ •๋ง ๋ฉ‹์ง„ ํšจ๊ณผ๋ฅผ ๋‚ผ ์ˆ˜ ์žˆ์–ด! โœ๏ธ
class DrawingAnimationView: UIView {
    private let shapeLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupShape()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupShape()
    }
    
    private func setupShape() {
        // ํ•˜ํŠธ ๋ชจ์–‘ ๊ฒฝ๋กœ ๋งŒ๋“ค๊ธฐ
        let path = createHeartPath()
        
        shapeLayer.path = path.cgPath
        shapeLayer.strokeColor = UIColor.systemPink.cgColor
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.lineWidth = 3
        shapeLayer.lineCap = .round
        shapeLayer.lineJoin = .round
        
        layer.addSublayer(shapeLayer)
    }
    
    private func createHeartPath() -> UIBezierPath {
        let path = UIBezierPath()
        let width = bounds.width
        let height = bounds.height
        
        // ํ•˜ํŠธ ๋ชจ์–‘ ๊ทธ๋ฆฌ๊ธฐ (๊ฐ„๋‹จํ•œ ๋ฒ„์ „)
        path.move(to: CGPoint(x: width/2, y: height * 0.3))
        path.addCurve(
            to: CGPoint(x: width/2, y: height * 0.8),
            controlPoint1: CGPoint(x: width * 0.2, y: height * 0.5),
            controlPoint2: CGPoint(x: width * 0.2, y: height * 0.7)
        )
        path.addCurve(
            to: CGPoint(x: width/2, y: height * 0.3),
            controlPoint1: CGPoint(x: width * 0.8, y: height * 0.7),
            controlPoint2: CGPoint(x: width * 0.8, y: height * 0.5)
        )
        
        return path
    }
    
    func animateDrawing() {
        // ๊ทธ๋ ค์ง€๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.fromValue = 0
        animation.toValue = 1
        animation.duration = 2.0
        animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
        
        shapeLayer.add(animation, forKey: "drawAnimation")
    }
    
    func animateFill() {
        // ์ฑ„์›Œ์ง€๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜
        shapeLayer.fillColor = UIColor.systemPink.cgColor
        
        let animation = CABasicAnimation(keyPath: "fillColor")
        animation.fromValue = UIColor.clear.cgColor
        animation.toValue = UIColor.systemPink.cgColor
        animation.duration = 0.5
        
        shapeLayer.add(animation, forKey: "fillAnimation")
    }
}
strokeEnd ์†์„ฑ์„ ์• ๋‹ˆ๋ฉ”์ด์…˜ํ•˜๋ฉด ๊ฒฝ๋กœ๊ฐ€ ๊ทธ๋ ค์ง€๋Š” ํšจ๊ณผ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด. ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ๋‚˜ ์ฒดํฌ๋งˆํฌ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ฐ™์€ ๊ฑธ ๋งŒ๋“ค ๋•Œ ์ •๋ง ์œ ์šฉํ•ด! โœ…

๐Ÿ’ก ์‹ค๋ฌด ํŒ๊ณผ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

๋งˆ์ง€๋ง‰์œผ๋กœ ์‹ค๋ฌด์—์„œ ์ •๋ง ์ค‘์š”ํ•œ ํŒ๋“ค์„ ๊ณต์œ ํ• ๊ฒŒ! ์ด๊ฒƒ๋“ค๋งŒ ์ž˜ ์ง€์ผœ๋„ ํ›จ์”ฌ ์ „๋ฌธ์ ์ธ ์•ฑ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด. ๐Ÿ’ผ

๐Ÿ“ฑ ์ ‘๊ทผ์„ฑ ๊ณ ๋ คํ•˜๊ธฐ

์• ๋‹ˆ๋ฉ”์ด์…˜์€ ๋ฉ‹์ง€์ง€๋งŒ, ๋ชจ๋“  ์‚ฌ์šฉ์ž์—๊ฒŒ ์ข‹์€ ๊ฑด ์•„๋‹ˆ์•ผ. ํŠนํžˆ ์ „์ • ์žฅ์• ๊ฐ€ ์žˆ๋Š” ์‚ฌ์šฉ์ž๋Š” ๊ณผ๋„ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋•Œ๋ฌธ์— ์–ด์ง€๋Ÿฌ์›€์„ ๋А๋‚„ ์ˆ˜ ์žˆ์–ด. iOS๋Š” ์ด๋ฅผ ์œ„ํ•œ ์„ค์ •์„ ์ œ๊ณตํ•ด! โ™ฟ
// ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ฐ์†Œ ์„ค์ • ํ™•์ธ
if UIAccessibility.isReduceMotionEnabled {
    // ์• ๋‹ˆ๋ฉ”์ด์…˜ ์—†์ด ์ฆ‰์‹œ ๋ณ€๊ฒฝ
    myView.alpha = 0
} else {
    // ์ผ๋ฐ˜ ์• ๋‹ˆ๋ฉ”์ด์…˜
    UIView.animate(withDuration: 0.3) {
        myView.alpha = 0
    }
}

// ๋” ๋‚˜์€ ๋ฐฉ๋ฒ•: ํ™•์žฅ ํ•จ์ˆ˜๋กœ ๋งŒ๋“ค๊ธฐ
extension UIView {
    func animateIfAllowed(
        duration: TimeInterval,
        animations: @escaping () -> Void,
        completion: ((Bool) -> Void)? = nil
    ) {
        if UIAccessibility.isReduceMotionEnabled {
            animations()
            completion?(true)
        } else {
            UIView.animate(
                withDuration: duration,
                animations: animations,
                completion: completion
            )
        }
    }
}

// ์‚ฌ์šฉ
myView.animateIfAllowed(duration: 0.3) {
    myView.alpha = 0
}

โšก ๋ฐฐํ„ฐ๋ฆฌ ํšจ์œจ ๊ณ ๋ คํ•˜๊ธฐ

์• ๋‹ˆ๋ฉ”์ด์…˜์€ ๋ฐฐํ„ฐ๋ฆฌ๋ฅผ ์†Œ๋ชจํ•ด. ํŠนํžˆ ๋ณต์žกํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด๋‚˜ ๋ฌดํ•œ ๋ฐ˜๋ณต ์• ๋‹ˆ๋ฉ”์ด์…˜์€ ์ฃผ์˜ํ•ด์•ผ ํ•ด! ๐Ÿ”‹
๋ฐฐํ„ฐ๋ฆฌ ํšจ์œจ์„ ์œ„ํ•œ ํŒ:

โ€ข ํ™”๋ฉด์ด ๋ณด์ด์ง€ ์•Š์„ ๋•Œ๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ค‘์ง€
โ€ข ๋ฌดํ•œ ๋ฐ˜๋ณต ์• ๋‹ˆ๋ฉ”์ด์…˜์€ ๊ผญ ํ•„์š”ํ•œ ๊ฒฝ์šฐ๋งŒ ์‚ฌ์šฉ
โ€ข ๋ณต์žกํ•œ ๊ทธ๋ฆผ์ž๋‚˜ ๋ธ”๋Ÿฌ ํšจ๊ณผ๋Š” ์‹ ์ค‘ํ•˜๊ฒŒ ์‚ฌ์šฉ
โ€ข ๊ฐ€๋Šฅํ•˜๋ฉด GPU ๊ฐ€์† ์†์„ฑ ์‚ฌ์šฉ (transform, opacity ๋“ฑ)
class BatteryEfficientViewController: UIViewController {
    private var animator: UIViewPropertyAnimator?
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        startAnimations()
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        stopAnimations()
    }
    
    private func startAnimations() {
        // ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹œ์ž‘
        animator = UIViewPropertyAnimator.runningPropertyAnimator(
            withDuration: 2.0,
            delay: 0,
            options: [.repeat, .autoreverse],
            animations: {
                self.pulsingView.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
            }
        )
    }
    
    private func stopAnimations() {
        // ํ™”๋ฉด์ด ์‚ฌ๋ผ์งˆ ๋•Œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ค‘์ง€
        animator?.stopAnimation(true)
        animator = nil
    }
}

๐ŸŽฏ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํƒ€์ด๋ฐ ๊ฐ€์ด๋“œ

์• ๋‹ˆ๋ฉ”์ด์…˜ ์ง€์† ์‹œ๊ฐ„์€ ์ •๋ง ์ค‘์š”ํ•ด! ๋„ˆ๋ฌด ๋น ๋ฅด๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ๋†“์น˜๊ณ , ๋„ˆ๋ฌด ๋А๋ฆฌ๋ฉด ๋‹ต๋‹ตํ•ด. ์• ํ”Œ์˜ ํœด๋จผ ์ธํ„ฐํŽ˜์ด์Šค ๊ฐ€์ด๋“œ๋ผ์ธ์„ ์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์•„! โฑ๏ธ
์• ๋‹ˆ๋ฉ”์ด์…˜ ํƒ€์ž… ๊ถŒ์žฅ ์‹œ๊ฐ„ ์„ค๋ช…
๋ฒ„ํŠผ ํƒญ 0.1 - 0.2์ดˆ ์ฆ‰๊ฐ์ ์ธ ํ”ผ๋“œ๋ฐฑ
ํŽ˜์ด๋“œ ์ธ/์•„์›ƒ 0.2 - 0.3์ดˆ ๋ถ€๋“œ๋Ÿฌ์šด ์ „ํ™˜
ํ™”๋ฉด ์ „ํ™˜ 0.3 - 0.5์ดˆ ๋งฅ๋ฝ ์ œ๊ณต
๋ชจ๋‹ฌ ํ‘œ์‹œ 0.4 - 0.6์ดˆ ์ฃผ์˜ ์ง‘์ค‘
๋ณต์žกํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ 0.5 - 1.0์ดˆ ์—ฌ๋Ÿฌ ๋‹จ๊ณ„ ํฌํ•จ
๐Ÿ’ก ํ™ฉ๊ธˆ ๊ทœ์น™!
๋Œ€๋ถ€๋ถ„์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์€ 0.2~0.4์ดˆ ์‚ฌ์ด๊ฐ€ ๊ฐ€์žฅ ์ž์—ฐ์Šค๋Ÿฌ์›Œ. ์ด ๋ฒ”์œ„๋ฅผ ๊ธฐ๋ณธ์œผ๋กœ ํ•˜๊ณ , ํ•„์š”์— ๋”ฐ๋ผ ์กฐ์ ˆํ•˜์ž!

๐Ÿ”ง ๋””๋ฒ„๊น…๊ณผ ํ…Œ์ŠคํŠธ

์• ๋‹ˆ๋ฉ”์ด์…˜์„ ํ…Œ์ŠคํŠธํ•  ๋•Œ๋Š” ๋‹ค์–‘ํ•œ ์ƒํ™ฉ์„ ๊ณ ๋ คํ•ด์•ผ ํ•ด! ๐Ÿงช
ํ…Œ์ŠคํŠธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ:
  • โœ… ๋‹ค์–‘ํ•œ ๊ธฐ๊ธฐ ํฌ๊ธฐ์—์„œ ํ…Œ์ŠคํŠธ (iPhone SE๋ถ€ํ„ฐ Pro Max๊นŒ์ง€)
  • โœ… ๋А๋ฆฐ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ชจ๋“œ๋กœ ํ™•์ธ (์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ Debug ๋ฉ”๋‰ด)
  • โœ… ์ ‘๊ทผ์„ฑ ์„ค์ • ํ…Œ์ŠคํŠธ (์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ฐ์†Œ ์˜ต์…˜)
  • โœ… ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ํ™•์ธ (Instruments์˜ Leaks ๋„๊ตฌ)
  • โœ… ํ”„๋ ˆ์ž„ ๋“œ๋กญ ํ™•์ธ (60fps ์œ ์ง€ ์—ฌ๋ถ€)
  • โœ… ๋‹คํฌ ๋ชจ๋“œ์—์„œ๋„ ํ™•์ธ
// ๋””๋ฒ„๊ทธ์šฉ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์†๋„ ์กฐ์ ˆ
#if DEBUG
extension UIView {
    static var debugAnimationSpeed: CGFloat = 1.0
    
    static func debugAnimate(
        withDuration duration: TimeInterval,
        animations: @escaping () -> Void,
        completion: ((Bool) -> Void)? = nil
    ) {
        let adjustedDuration = duration * debugAnimationSpeed
        animate(
            withDuration: adjustedDuration,
            animations: animations,
            completion: completion
        )
    }
}
#endif

๐ŸŽจ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋งŒ๋“ค๊ธฐ

๊ฐ™์€ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์—ฌ๋Ÿฌ ๊ณณ์—์„œ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด, ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ค์ž! ์žฌ๋Šฅ๋„ท์—์„œ iOS ๊ฐœ๋ฐœ์„ ๋ฐฐ์šฐ๊ฑฐ๋‚˜ ๊ฐ€๋ฅด์น  ๋•Œ๋„ ์ด๋Ÿฐ ํŒจํ„ด์„ ์•Œ์•„๋‘๋ฉด ์ •๋ง ์œ ์šฉํ•ด. ๐Ÿ“š
// ์• ๋‹ˆ๋ฉ”์ด์…˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค
enum AnimationStyle {
    case bounce
    case fade
    case slide
    case scale
}

class AnimationHelper {
    // ๋ฐ”์šด์Šค ์• ๋‹ˆ๋ฉ”์ด์…˜
    static func bounce(view: UIView, completion: (() -> Void)? = nil) {
        UIView.animate(
            withDuration: 0.1,
            animations: {
                view.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
            },
            completion: { _ in
                UIView.animate(
                    withDuration: 0.3,
                    delay: 0,
                    usingSpringWithDamping: 0.5,
                    initialSpringVelocity: 0.5,
                    options: [],
                    animations: {
                        view.transform = .identity
                    },
                    completion: { _ in
                        completion?()
                    }
                )
            }
        )
    }
    
    // ํŽ˜์ด๋“œ ์ธ ์• ๋‹ˆ๋ฉ”์ด์…˜
    static func fadeIn(view: UIView, duration: TimeInterval = 0.3, completion: (() -> Void)? = nil) {
        view.alpha = 0
        view.isHidden = false
        
        UIView.animate(
            withDuration: duration,
            animations: {
                view.alpha = 1
            },
            completion: { _ in
                completion?()
            }
        )
    }
    
    // ์Šฌ๋ผ์ด๋“œ ์ธ ์• ๋‹ˆ๋ฉ”์ด์…˜
    static func slideIn(
        view: UIView,
        from direction: UIRectEdge,
        duration: TimeInterval = 0.4,
        completion: (() -> Void)? = nil
    ) {
        let originalTransform = view.transform
        
        // ์‹œ์ž‘ ์œ„์น˜ ์„ค์ •
        switch direction {
        case .left:
            view.transform = CGAffineTransform(translationX: -view.bounds.width, y: 0)
        case .right:
            view.transform = CGAffineTransform(translationX: view.bounds.width, y: 0)
        case .top:
            view.transform = CGAffineTransform(translationX: 0, y: -view.bounds.height)
        case .bottom:
            view.transform = CGAffineTransform(translationX: 0, y: view.bounds.height)
        default:
            break
        }
        
        view.isHidden = false
        
        UIView.animate(
            withDuration: duration,
            delay: 0,
            usingSpringWithDamping: 0.8,
            initialSpringVelocity: 0.5,
            options: .curveEaseOut,
            animations: {
                view.transform = originalTransform
            },
            completion: { _ in
                completion?()
            }
        )
    }
    
    // ์Šค์ผ€์ผ ์• ๋‹ˆ๋ฉ”์ด์…˜
    static func scale(
        view: UIView,
        to scale: CGFloat,
        duration: TimeInterval = 0.3,
        completion: (() -> Void)? = nil
    ) {
        UIView.animate(
            withDuration: duration,
            delay: 0,
            usingSpringWithDamping: 0.7,
            initialSpringVelocity: 0.5,
            options: .curveEaseInOut,
            animations: {
                view.transform = CGAffineTransform(scaleX: scale, y: scale)
            },
            completion: { _ in
                completion?()
            }
        )
    }
}

// ์‚ฌ์šฉ ์˜ˆ์ œ
AnimationHelper.bounce(view: myButton) {
    print("๋ฐ”์šด์Šค ์™„๋ฃŒ!")
}

AnimationHelper.slideIn(view: menuView, from: .left) {
    print("๋ฉ”๋‰ด ํ‘œ์‹œ ์™„๋ฃŒ!")
}

๐ŸŽ“ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€! ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์ง€? ๐Ÿ˜… ์šฐ๋ฆฌ๋Š” UIView ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ๊ธฐ์ดˆ๋ถ€ํ„ฐ Core Animation์˜ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๊นŒ์ง€, ๊ทธ๋ฆฌ๊ณ  ์‹ค์ „ ํ”„๋กœ์ ํŠธ์™€ ์ตœ์ ํ™” ๊ธฐ๋ฒ•๊นŒ์ง€ ๋ชจ๋‘ ์‚ดํŽด๋ดค์–ด.

์• ๋‹ˆ๋ฉ”์ด์…˜์€ ๋‹จ์ˆœํžˆ "์˜ˆ์˜๊ฒŒ ๋ณด์ด๋ ค๊ณ " ํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ์•ผ. ์‚ฌ์šฉ์ž์™€ ์•ฑ ์‚ฌ์ด์˜ ๋Œ€ํ™”๋ฅผ ๋งŒ๋“œ๋Š” ๊ฑฐ์ง€. ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ์˜ ํ”ผ๋“œ๋ฐฑ, ํ™”๋ฉด ์ „ํ™˜ ์‹œ์˜ ๋งฅ๋ฝ ์ œ๊ณต, ๋กœ๋”ฉ ์ค‘์ž„์„ ์•Œ๋ฆฌ๋Š” ์ธ๋””์ผ€์ดํ„ฐ... ์ด ๋ชจ๋“  ๊ฒŒ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๋งŒ๋“ค์–ด๊ฐ€๋Š” ์ค‘์š”ํ•œ ์š”์†Œ๋“ค์ด์•ผ. ๐ŸŽจ

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

1. ์‹œ์ž‘์€ ๊ฐ„๋‹จํ•˜๊ฒŒ ๐Ÿ“ฑ
UIView ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ์‹œ์ž‘ํ•ด. ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ์ด๊ฒƒ๋งŒ์œผ๋กœ๋„ ์ถฉ๋ถ„ํ•ด!

2. ํ•„์š”ํ•  ๋•Œ Core Animation ๐ŸŽฏ
๋” ๋ณต์žกํ•œ ์ œ์–ด๊ฐ€ ํ•„์š”ํ•˜๊ฑฐ๋‚˜ UIView๋กœ ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ฒƒ๋“ค์„ ํ•  ๋•Œ Core Animation์„ ์‚ฌ์šฉํ•ด.

3. ์„ฑ๋Šฅ์„ ํ•ญ์ƒ ๊ณ ๋ คํ•ด โšก
60fps๋ฅผ ์œ ์ง€ํ•˜๋Š” ๊ฒŒ ๋ชฉํ‘œ์•ผ. GPU ๊ฐ€์† ์†์„ฑ์„ ํ™œ์šฉํ•˜๊ณ , ๋ถˆํ•„์š”ํ•œ ์˜คํ”„์Šคํฌ๋ฆฐ ๋ Œ๋”๋ง์„ ํ”ผํ•˜์ž.

4. ์ ‘๊ทผ์„ฑ์„ ์žŠ์ง€ ๋ง์ž โ™ฟ
๋ชจ๋“  ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ ์•ฑ์„ ๋งŒ๋“ค์–ด์•ผ ํ•ด. ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ฐ์†Œ ์„ค์ •์„ ์กด์ค‘ํ•˜์ž.

5. ํƒ€์ด๋ฐ์ด ์ค‘์š”ํ•ด โฑ๏ธ
๋Œ€๋ถ€๋ถ„์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์€ 0.2~0.4์ดˆ๊ฐ€ ์ ๋‹นํ•ด. ๋„ˆ๋ฌด ๋น ๋ฅด๊ฑฐ๋‚˜ ๋А๋ฆฌ๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ๋‚˜๋น ์ ธ.

6. ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ค์ž ๐Ÿ”ง
์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์€ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋‚˜ ํด๋ž˜์Šค๋กœ ๋งŒ๋“ค์–ด์„œ ์žฌ์‚ฌ์šฉํ•ด.

iOS ๊ฐœ๋ฐœ์„ ๋ฐฐ์šฐ๊ณ  ์žˆ๋‹ค๋ฉด, ์• ๋‹ˆ๋ฉ”์ด์…˜์€ ์ •๋ง ์ค‘์š”ํ•œ ์Šคํ‚ฌ์ด์•ผ. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค๊ณผ ์ง€์‹์„ ๋‚˜๋ˆ„๋ฉด์„œ ํ•จ๊ป˜ ์„ฑ์žฅํ•  ์ˆ˜๋„ ์žˆ๊ณ ! ๐Ÿ’ช

์ฒ˜์Œ์—๋Š” ๋ณต์žกํ•ด ๋ณด์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ํ•˜๋‚˜์”ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์—ฐ์Šตํ•˜๋‹ค ๋ณด๋ฉด ์–ด๋А์ƒˆ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ฉ‹์ง„ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋งŒ๋“ค๊ณ  ์žˆ๋Š” ์ž์‹ ์„ ๋ฐœ๊ฒฌํ•˜๊ฒŒ ๋  ๊ฑฐ์•ผ. ์‹ค์ œ๋กœ ์•ฑ์„ ๋งŒ๋“ค๋ฉด์„œ ๋‹ค์–‘ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์‹œ๋„ํ•ด๋ณด๊ณ , ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์•„๋ณด๋ฉด์„œ ๊ฐ๊ฐ์„ ํ‚ค์›Œ๊ฐ€๋Š” ๊ฒŒ ๊ฐ€์žฅ ์ข‹์€ ํ•™์Šต ๋ฐฉ๋ฒ•์ด์•ผ! ๐ŸŽ“

์• ๋‹ˆ๋ฉ”์ด์…˜์€ ์˜ˆ์ˆ ์ด์ž ๊ณผํ•™์ด์•ผ. ๊ธฐ์ˆ ์ ์ธ ๊ตฌํ˜„๋„ ์ค‘์š”ํ•˜์ง€๋งŒ, ์‚ฌ์šฉ์ž์˜ ๊ฐ์ •๊ณผ ๊ฒฝํ—˜์„ ์ดํ•ดํ•˜๋Š” ๊ฒƒ๋„ ๋˜‘๊ฐ™์ด ์ค‘์š”ํ•ด. ํ•ญ์ƒ "์ด ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์‚ฌ์šฉ์ž์—๊ฒŒ ์–ด๋–ค ๊ฐ€์น˜๋ฅผ ์ฃผ๋Š”๊ฐ€?"๋ฅผ ์ƒ๊ฐํ•˜๋ฉด์„œ ๋งŒ๋“ค์ž! ๐ŸŽจ

์ž, ์ด์ œ ๋‹น์‹  ์ฐจ๋ก€์•ผ! ์˜ค๋Š˜ ๋ฐฐ์šด ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ๋ฉ‹์ง„ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ UI๋ฅผ ๋งŒ๋“ค์–ด๋ด. ์‹œ์ž‘์€ ์ž‘์€ ๋ฒ„ํŠผ ์• ๋‹ˆ๋ฉ”์ด์…˜๋ถ€ํ„ฐ๋ผ๋„ ๊ดœ์ฐฎ์•„. ์ค‘์š”ํ•œ ๊ฑด ๊ณ„์† ์‹œ๋„ํ•˜๊ณ  ๊ฐœ์„ ํ•ด๋‚˜๊ฐ€๋Š” ๊ฑฐ๋‹ˆ๊นŒ! ๐Ÿ’ซ

ํ™”์ดํŒ…! ๐Ÿš€โœจ
๐ŸŽ‰ ์—ฌ๊ธฐ๊นŒ์ง€ ์ฝ์–ด์ค˜์„œ ๊ณ ๋งˆ์›Œ!

์งˆ๋ฌธ์ด๋‚˜ ํ”ผ๋“œ๋ฐฑ์ด ์žˆ๋‹ค๋ฉด ์–ธ์ œ๋“  ํ™˜์˜์ด์•ผ!
ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜์ž! ๐Ÿ’ช๐Ÿ˜Š
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0