Ver3.0
์ ์ฒด ์ด๋
Ver3.0 ๐จ Core Animation๊ณผ UIView ์ ๋๋ฉ์ด์ ์ผ๋ก ๋ง๋๋ ์ด์์๋ ์ธํฐ๋ํฐ๋ธ UI ์ธ๊ณ

๐จ Core Animation๊ณผ UIView ์ ๋๋ฉ์ด์ ์ผ๋ก ๋ง๋๋ ์ด์์๋ ์ธํฐ๋ํฐ๋ธ UI ์ธ๊ณ
iOS ์ฑ์ ์๋ช
์ ๋ถ์ด๋ฃ๋ ์ ๋๋ฉ์ด์
๋ง์คํฐ ๊ฐ์ด๋ โจ
์๋
! ์ค๋์ iOS ๊ฐ๋ฐ์์ ์ ๋ง ์ค์ํ๋ฉด์๋ ์ฌ๋ฏธ์๋ ์ฃผ์ ๋ฅผ ๋ค๋ค๋ณผ ๊ฑฐ์ผ. ๋ฐ๋ก Core Animation๊ณผ UIView ์ ๋๋ฉ์ด์
์ด์ผ! ๐
ํน์ ์ฑ์ ์ฐ๋ค๊ฐ "์, ์ด ์์ง์ ์ง์ง ๋ถ๋๋ฝ๋ค" ๋๋ "์ด ํ๋ฉด ์ ํ ๋๋ฌด ์์ฐ์ค๋ฌ์ด๋ฐ?"๋ผ๊ณ ๋๋ ์ ์์ง? ๊ทธ๊ฒ ๋ฐ๋ก ์ค๋ ์ฐ๋ฆฌ๊ฐ ๋ฐฐ์ธ ์ ๋๋ฉ์ด์ ์ ํ์ด์ผ. ๋จ์ํ ํ๋ฉด์ ๋ญ๊ฐ๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ฒ๊ณผ ์๋๊ฐ ์๊ฒ ์์ง์ด๋ฉฐ ๋ณด์ฌ์ฃผ๋ ๊ฒ์ ์์ ํ ๋ค๋ฅธ ์ฌ์ฉ์ ๊ฒฝํ์ ๋ง๋ค์ด๋ด๊ฑฐ๋ .
iOS ๊ฐ๋ฐ์ ๋ฐฐ์ฐ๊ณ ์๊ฑฐ๋, ๋ ๋์ UI/UX๋ฅผ ๋ง๋ค๊ณ ์ถ์ ๊ฐ๋ฐ์๋ผ๋ฉด ์ด ๊ธ์ด ์์ฒญ ๋์์ด ๋ ๊ฑฐ์ผ. ์ฌ๋ฅ๋ท์์๋ iOS ๊ฐ๋ฐ ๊ด๋ จ ์ฌ๋ฅ์ ๊ณต์ ํ๋ ๋ถ๋ค์ด ๋ง์๋ฐ, ์ ๋๋ฉ์ด์ ์ ์ ๋ง ํ์ ์คํฌ์ด๋ผ๊ณ ํ ์ ์์ด! ๐
ํน์ ์ฑ์ ์ฐ๋ค๊ฐ "์, ์ด ์์ง์ ์ง์ง ๋ถ๋๋ฝ๋ค" ๋๋ "์ด ํ๋ฉด ์ ํ ๋๋ฌด ์์ฐ์ค๋ฌ์ด๋ฐ?"๋ผ๊ณ ๋๋ ์ ์์ง? ๊ทธ๊ฒ ๋ฐ๋ก ์ค๋ ์ฐ๋ฆฌ๊ฐ ๋ฐฐ์ธ ์ ๋๋ฉ์ด์ ์ ํ์ด์ผ. ๋จ์ํ ํ๋ฉด์ ๋ญ๊ฐ๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ฒ๊ณผ ์๋๊ฐ ์๊ฒ ์์ง์ด๋ฉฐ ๋ณด์ฌ์ฃผ๋ ๊ฒ์ ์์ ํ ๋ค๋ฅธ ์ฌ์ฉ์ ๊ฒฝํ์ ๋ง๋ค์ด๋ด๊ฑฐ๋ .
iOS ๊ฐ๋ฐ์ ๋ฐฐ์ฐ๊ณ ์๊ฑฐ๋, ๋ ๋์ UI/UX๋ฅผ ๋ง๋ค๊ณ ์ถ์ ๊ฐ๋ฐ์๋ผ๋ฉด ์ด ๊ธ์ด ์์ฒญ ๋์์ด ๋ ๊ฑฐ์ผ. ์ฌ๋ฅ๋ท์์๋ iOS ๊ฐ๋ฐ ๊ด๋ จ ์ฌ๋ฅ์ ๊ณต์ ํ๋ ๋ถ๋ค์ด ๋ง์๋ฐ, ์ ๋๋ฉ์ด์ ์ ์ ๋ง ํ์ ์คํฌ์ด๋ผ๊ณ ํ ์ ์์ด! ๐
๐ฌ ์ ๋๋ฉ์ด์ , ์ ์ค์ํ ๊น?
๋จผ์ ์ ๋๋ฉ์ด์
์ด ์ ์ค์ํ์ง๋ถํฐ ์ด์ผ๊ธฐํด๋ณด์. ๋จ์ํ "์์๊ฒ ๋ณด์ด๋ ค๊ณ "๋ง์ ์๋์ผ!
1. ์ฌ์ฉ์ ๊ฒฝํ ํฅ์ ๐
์ ๋๋ฉ์ด์ ์ ์ฌ์ฉ์์๊ฒ ํผ๋๋ฐฑ์ ์ ๊ณตํด. ๋ฒํผ์ ๋๋ ์ ๋ ์ด์ง ๋๋ฆฌ๋ ํจ๊ณผ, ํ๋ฉด์ด ์ ํ๋ ๋ ์ฌ๋ผ์ด๋ ํจ๊ณผ ๋ฑ์ ์ฌ์ฉ์์๊ฒ "๋ค, ๋น์ ์ ํ๋์ด ์ ๋๋ก ์ธ์๋์ด์"๋ผ๊ณ ๋งํด์ฃผ๋ ๊ฑฐ์ง.
2. ๋งฅ๋ฝ ์ ๊ณต ๐
ํ๋ฉด์ด ๊ฐ์๊ธฐ ๋ฐ๋๋ฉด ์ฌ์ฉ์๋ ํผ๋์ค๋ฌ์ํด. ํ์ง๋ง ๋ถ๋๋ฌ์ด ์ ํ ์ ๋๋ฉ์ด์ ์ด ์์ผ๋ฉด "์, ์ด ํ๋ฉด์์ ์ ํ๋ฉด์ผ๋ก ๋์ด๊ฐ๋๊ตฌ๋"๋ผ๊ณ ์์ฐ์ค๋ฝ๊ฒ ์ดํดํ ์ ์์ด.
3. ์ฃผ์ ์ง์ค ๐
์ค์ํ ์ ๋ณด๋ ์๋ก์ด ์์๊ฐ ๋ํ๋ ๋ ์ ๋๋ฉ์ด์ ์ ์ฌ์ฉํ๋ฉด ์ฌ์ฉ์์ ์์ ์ ์์ฐ์ค๋ฝ๊ฒ ์ ๋ํ ์ ์์ด. ๊ฐ์๊ธฐ ๋ํ๋๋ ๊ฒ๋ณด๋ค ํ์ด๋ ์ธ์ด๋ ์ฌ๋ผ์ด๋ ์ธ ํจ๊ณผ๊ฐ ํจ์ฌ ํจ๊ณผ์ ์ด์ง!
4. ๋ธ๋๋ ์ ์ฒด์ฑ ๐จ
๋ ํนํ ์ ๋๋ฉ์ด์ ์คํ์ผ์ ์ฑ์ ๊ฐ์ฑ์ ๋ง๋ค์ด์ค. ์ ํ์ ๋ถ๋๋ฌ์ด ์ ๋๋ฉ์ด์ , ๊ตฌ๊ธ์ ๋จธํฐ๋ฆฌ์ผ ๋์์ธ ์ ๋๋ฉ์ด์ ์ฒ๋ผ ๋ง์ด์ผ.
1. ์ฌ์ฉ์ ๊ฒฝํ ํฅ์ ๐
์ ๋๋ฉ์ด์ ์ ์ฌ์ฉ์์๊ฒ ํผ๋๋ฐฑ์ ์ ๊ณตํด. ๋ฒํผ์ ๋๋ ์ ๋ ์ด์ง ๋๋ฆฌ๋ ํจ๊ณผ, ํ๋ฉด์ด ์ ํ๋ ๋ ์ฌ๋ผ์ด๋ ํจ๊ณผ ๋ฑ์ ์ฌ์ฉ์์๊ฒ "๋ค, ๋น์ ์ ํ๋์ด ์ ๋๋ก ์ธ์๋์ด์"๋ผ๊ณ ๋งํด์ฃผ๋ ๊ฑฐ์ง.
2. ๋งฅ๋ฝ ์ ๊ณต ๐
ํ๋ฉด์ด ๊ฐ์๊ธฐ ๋ฐ๋๋ฉด ์ฌ์ฉ์๋ ํผ๋์ค๋ฌ์ํด. ํ์ง๋ง ๋ถ๋๋ฌ์ด ์ ํ ์ ๋๋ฉ์ด์ ์ด ์์ผ๋ฉด "์, ์ด ํ๋ฉด์์ ์ ํ๋ฉด์ผ๋ก ๋์ด๊ฐ๋๊ตฌ๋"๋ผ๊ณ ์์ฐ์ค๋ฝ๊ฒ ์ดํดํ ์ ์์ด.
3. ์ฃผ์ ์ง์ค ๐
์ค์ํ ์ ๋ณด๋ ์๋ก์ด ์์๊ฐ ๋ํ๋ ๋ ์ ๋๋ฉ์ด์ ์ ์ฌ์ฉํ๋ฉด ์ฌ์ฉ์์ ์์ ์ ์์ฐ์ค๋ฝ๊ฒ ์ ๋ํ ์ ์์ด. ๊ฐ์๊ธฐ ๋ํ๋๋ ๊ฒ๋ณด๋ค ํ์ด๋ ์ธ์ด๋ ์ฌ๋ผ์ด๋ ์ธ ํจ๊ณผ๊ฐ ํจ์ฌ ํจ๊ณผ์ ์ด์ง!
4. ๋ธ๋๋ ์ ์ฒด์ฑ ๐จ
๋ ํนํ ์ ๋๋ฉ์ด์ ์คํ์ผ์ ์ฑ์ ๊ฐ์ฑ์ ๋ง๋ค์ด์ค. ์ ํ์ ๋ถ๋๋ฌ์ด ์ ๋๋ฉ์ด์ , ๊ตฌ๊ธ์ ๋จธํฐ๋ฆฌ์ผ ๋์์ธ ์ ๋๋ฉ์ด์ ์ฒ๋ผ ๋ง์ด์ผ.
๐ก ์ฌ๋ฏธ์๋ ์ฌ์ค!
์ธ๊ฐ์ ๋์ ์ด๋น ์ฝ 10-12๊ฐ์ ์ด๋ฏธ์ง๋ฅผ ๋ณผ ์ ์๋๋ฐ, 24fps(์ด๋น ํ๋ ์) ์ด์์ด ๋๋ฉด ์ฐ๋ฆฌ๋ ๊ทธ๊ฒ์ ์ฐ์์ ์ธ ์์ง์์ผ๋ก ์ธ์ํด. iOS๋ ๊ธฐ๋ณธ์ ์ผ๋ก 60fps๋ก ์ ๋๋ฉ์ด์ ์ ๋ ๋๋งํ๊ธฐ ๋๋ฌธ์ ์์ฒญ ๋ถ๋๋ฝ๊ฒ ๋๊ปด์ง๋ ๊ฑฐ์ผ!
์ธ๊ฐ์ ๋์ ์ด๋น ์ฝ 10-12๊ฐ์ ์ด๋ฏธ์ง๋ฅผ ๋ณผ ์ ์๋๋ฐ, 24fps(์ด๋น ํ๋ ์) ์ด์์ด ๋๋ฉด ์ฐ๋ฆฌ๋ ๊ทธ๊ฒ์ ์ฐ์์ ์ธ ์์ง์์ผ๋ก ์ธ์ํด. iOS๋ ๊ธฐ๋ณธ์ ์ผ๋ก 60fps๋ก ์ ๋๋ฉ์ด์ ์ ๋ ๋๋งํ๊ธฐ ๋๋ฌธ์ ์์ฒญ ๋ถ๋๋ฝ๊ฒ ๋๊ปด์ง๋ ๊ฑฐ์ผ!
๐ฑ UIView ์ ๋๋ฉ์ด์ - ์์์ ๊ฐ๋จํ๊ฒ!
iOS์์ ์ ๋๋ฉ์ด์
์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ํฌ๊ฒ ๋ ๊ฐ์ง์ผ. ๋จผ์ ๋ ๊ฐ๋จํ๊ณ ์ง๊ด์ ์ธ UIView ์ ๋๋ฉ์ด์
๋ถํฐ ์์๋ณด์! ๐ฏ
UIView ์ ๋๋ฉ์ด์ ์ UIKit์์ ์ ๊ณตํ๋ ๊ณ ์์ค(high-level) API์ผ. "๊ณ ์์ค"์ด๋ผ๋ ๋ง์ ๋ณต์กํ ๋ด๋ถ ๋์์ ์ ๊ฒฝ ์ฐ์ง ์๊ณ ๋ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๋ค๋ ๋ป์ด์ง. ๋ง์น ์๋์ฐจ๋ฅผ ์ด์ ํ ๋ ์์ง์ ์ธ๋ถ ์๋ ์๋ฆฌ๋ฅผ ๋ชฐ๋ผ๋ ์ด์ ํ ์ ์๋ ๊ฒ์ฒ๋ผ!
UIView ์ ๋๋ฉ์ด์ ์ UIKit์์ ์ ๊ณตํ๋ ๊ณ ์์ค(high-level) API์ผ. "๊ณ ์์ค"์ด๋ผ๋ ๋ง์ ๋ณต์กํ ๋ด๋ถ ๋์์ ์ ๊ฒฝ ์ฐ์ง ์๊ณ ๋ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๋ค๋ ๋ป์ด์ง. ๋ง์น ์๋์ฐจ๋ฅผ ์ด์ ํ ๋ ์์ง์ ์ธ๋ถ ์๋ ์๋ฆฌ๋ฅผ ๋ชฐ๋ผ๋ ์ด์ ํ ์ ์๋ ๊ฒ์ฒ๋ผ!
๐จ ๊ธฐ๋ณธ UIView ์ ๋๋ฉ์ด์ ๋ฉ์๋
UIView ํด๋์ค๋ ์ ๋๋ฉ์ด์
์ ์ํ ์ฌ๋ฌ ํด๋์ค ๋ฉ์๋๋ฅผ ์ ๊ณตํด. ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ๊ฒ๋ถํฐ ์ดํด๋ณด์!
1. animate(withDuration:animations:) ๐
๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ์ ๋๋ฉ์ด์ ๋ฉ์๋์ผ. ์ง์ ํ ์๊ฐ ๋์ ๋ทฐ์ ์์ฑ์ ๋ณ๊ฒฝํ ์ ์์ด.
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:) โ
์ ๋๋ฉ์ด์ ์ด ๋๋ ํ ์ถ๊ฐ ์์ ์ ํ๊ณ ์ถ์ ๋ ์ฌ์ฉํด.
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 - ์ ๋๋ฉ์ด์ ์ ์ญ๋ฐฉํฅ์ผ๋ก๋ ์คํ
.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 ๊ฐ์ ๋ง์ด ์ฌ์ฉํด. ์์ฐ์ค๋ฌ์ฐ๋ฉด์๋ ์๋๊ฐ์ด ์๊ฑฐ๋ !
โข 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์ผ๋ก ๋ง๋ค์ด์ ๋น์ทํ ํจ๊ณผ๋ฅผ ๋ผ ์ ์์ง!
๋ชจ๋ ์์ฑ์ด ์ ๋๋ฉ์ด์ ๊ฐ๋ฅํ ๊ฑด ์๋์ผ. ์๋ฅผ ๋ค์ด 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 ์ ๋๋ฉ์ด์ ์ผ๋ก๋ ๋ถ๊ฐ๋ฅํ ๊ฒ๋ค์ ํ ์ ์์ด!
Core Animation์ iOS์ ๋ชจ๋ ์ ๋๋ฉ์ด์ ์ ๊ธฐ๋ฐ์ด ๋๋ ํ๋ ์์ํฌ์ผ. ์ฌ์ค UIView ์ ๋๋ฉ์ด์ ๋ ๋ด๋ถ์ ์ผ๋ก๋ Core Animation์ ์ฌ์ฉํ๊ณ ์์ด. ํ์ง๋ง Core Animation์ ์ง์ ์ฌ์ฉํ๋ฉด UIView ์ ๋๋ฉ์ด์ ์ผ๋ก๋ ๋ถ๊ฐ๋ฅํ ๊ฒ๋ค์ ํ ์ ์์ด!
๐จ CALayer - ์ ๋๋ฉ์ด์ ์ ์ค์ ์ฃผ์ธ๊ณต
Core Animation์ ํต์ฌ์ CALayer์ผ. ๋ชจ๋ UIView๋ ๋ด๋ถ์ 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)
โข ๋ง์คํน๊ณผ ํด๋ฆฌํ
โข ๋ณต์กํ ๊ฒฝ๋ก ์ ๋๋ฉ์ด์
โข ๊ทธ๋ผ๋์ธํธ ํจ๊ณผ
โข ๋ชจ์๋ฆฌ ๋ฅ๊ธ๊ฒ ๋ง๋ค๊ธฐ (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 ์ ๋๋ฉ์ด์ ๊ณผ์ ํฐ ์ฐจ์ด์ ์ด์ผ.
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
์ด๋ ๊ฒ ํ๋ฉด ๋ค ๊ฐ์ง ์ ๋๋ฉ์ด์
์ด ๋์์ ๋ถ๋๋ฝ๊ฒ ์คํ๋ผ. ์ ๋ง ๋ฉ์ง ํจ๊ณผ๋ฅผ ๋ง๋ค ์ ์์ด! โจ
๐ฏ ์ค์ ํ ํฌ๋ - ํ๋ก์ฒ๋ผ ์ ๋๋ฉ์ด์ ํ๊ธฐ
์ด์ ๊ธฐ๋ณธ์ ์์์ผ๋, ์ค์ ์์ ์ ๋ง ์ ์ฉํ ํ
ํฌ๋๋ค์ ์์๋ณด์! ์ด๊ฒ๋ค๋ง ์์๋ ๋น์ ์ ์ฑ์ด ํจ์ฌ ํ๋กํ์
๋ํด ๋ณด์ผ ๊ฑฐ์ผ. ๐
โฑ๏ธ ํ์ด๋ฐ ํจ์์ ๋ง๋ฒ
์ ๋๋ฉ์ด์
์ ๋๋์ ํ์ด๋ฐ ํจ์์ ๋ฐ๋ผ ์์ ํ ๋ฌ๋ผ์ ธ. ๊ฐ์ ์ด๋ ์ ๋๋ฉ์ด์
์ด๋ผ๋ ํ์ด๋ฐ ํจ์์ ๋ฐ๋ผ ๊ธํ๊ฒ ๋ณด์ด๊ฑฐ๋ ์ฌ์ ๋กญ๊ฒ ๋ณด์ผ ์ ์์ด! โฐ
// 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๊ณผ ๋น์ท)
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๋ง๋ค ํ ํ๋ ์์ ๊ทธ๋ ค์ผ ํด. ์ด ์๊ฐ ์์ ๋ชจ๋ ๊ณ์ฐ๊ณผ ๋ ๋๋ง์ด ๋๋์ผ ๋ถ๋๋ฌ์ด ์ ๋๋ฉ์ด์ ์ด ๊ฐ๋ฅํ์ง!
iOS๋ ๊ธฐ๋ณธ์ ์ผ๋ก 60fps(์ด๋น 60ํ๋ ์)๋ก ํ๋ฉด์ ์ ๋ฐ์ดํธํด. ์ฆ, 16.67ms๋ง๋ค ํ ํ๋ ์์ ๊ทธ๋ ค์ผ ํด. ์ด ์๊ฐ ์์ ๋ชจ๋ ๊ณ์ฐ๊ณผ ๋ ๋๋ง์ด ๋๋์ผ ๋ถ๋๋ฌ์ด ์ ๋๋ฉ์ด์ ์ด ๊ฐ๋ฅํ์ง!
โก ์ฑ๋ฅ ์ต์ ํ ํ๋ค
1. ๋ ์ด์ด ์์ฑ ์ ๋๋ฉ์ด์
์ฌ์ฉํ๊ธฐ ๐จ
๊ฐ๋ฅํ๋ฉด ๋ ์ด์ด์ ์์ฑ์ ์ง์ ์ ๋๋ฉ์ด์ ํ๋ ๊ฒ ์ข์. ํนํ transform, opacity, backgroundColor ๊ฐ์ ์์ฑ๋ค์ GPU์์ ์ฒ๋ฆฌ๋๊ธฐ ๋๋ฌธ์ ์์ฒญ ๋นจ๋ผ!
๊ฐ๋ฅํ๋ฉด ๋ ์ด์ด์ ์์ฑ์ ์ง์ ์ ๋๋ฉ์ด์ ํ๋ ๊ฒ ์ข์. ํนํ 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๋ฅผ ์ค์ ํ๋ฉด ๋ ์ด์ด๋ฅผ ๋นํธ๋งต์ผ๋ก ์บ์ฑํด. ๋ณต์กํ ๋ ์ด์ด๋ฅผ ๋งค๋ฒ ๋ค์ ๊ทธ๋ฆฌ์ง ์์๋ ๋๋๊น ์ฑ๋ฅ์ด ์ข์์ ธ! ํ์ง๋ง ๋ ์ด์ด๊ฐ ์์ฃผ ๋ณํ๋ฉด ์คํ๋ ค ์ฑ๋ฅ์ด ๋๋น ์ง ์ ์์ผ๋ ์ฃผ์ํด์ผ ํด.
shouldRasterize = true๋ฅผ ์ค์ ํ๋ฉด ๋ ์ด์ด๋ฅผ ๋นํธ๋งต์ผ๋ก ์บ์ฑํด. ๋ณต์กํ ๋ ์ด์ด๋ฅผ ๋งค๋ฒ ๋ค์ ๊ทธ๋ฆฌ์ง ์์๋ ๋๋๊น ์ฑ๋ฅ์ด ์ข์์ ธ! ํ์ง๋ง ๋ ์ด์ด๊ฐ ์์ฃผ ๋ณํ๋ฉด ์คํ๋ ค ์ฑ๋ฅ์ด ๋๋น ์ง ์ ์์ผ๋ ์ฃผ์ํด์ผ ํด.
3. ๋ชจ์๋ฆฌ ๋ฅ๊ธ๊ฒ ๋ง๋ค๊ธฐ ์ต์ ํ ๐ฒ
cornerRadius์ masksToBounds๋ฅผ ํจ๊ป ์ฌ์ฉํ๋ฉด ์คํ์คํฌ๋ฆฐ ๋ ๋๋ง์ด ๋ฐ์ํด์ ๋๋ ค์ง ์ ์์ด.
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()๋ฅผ ํธ์ถํ๋ฉด ์ ์ฒด ๋ทฐ ๊ณ์ธต์ ๋ ์ด์์์ ๋ค์ ๊ณ์ฐํด์ผ ํด์ ๋๋ ค์ง ์ ์์ด.
์ ๋๋ฉ์ด์ ๋ธ๋ก ์์์ 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 - ๋ ์ด์ด ๋ธ๋ ๋ฉ ํ์
์ฃผ์ ์ธก์ ์งํ:
โข Frame Rate - ์ด๋น ํ๋ ์ ์ (60fps๊ฐ ๋ชฉํ)
โข GPU Usage - GPU ์ฌ์ฉ๋ฅ
โข Offscreen Rendering - ์คํ์คํฌ๋ฆฐ ๋ ๋๋ง ๋ฐ์ ์ฌ๋ถ
โข Blended Layers - ๋ ์ด์ด ๋ธ๋ ๋ฉ ํ์
๐ฏ ๋๋ฒ๊น
ํ!
์๋ฎฌ๋ ์ดํฐ์ Debug ๋ฉ๋ด์์ "Color Blended Layers"์ "Color Offscreen-Rendered" ์ต์ ์ ์ผ๋ฉด ์ฑ๋ฅ ๋ฌธ์ ๊ฐ ์๋ ๋ถ๋ถ์ ์๊ฐ์ ์ผ๋ก ํ์ธํ ์ ์์ด. ๋นจ๊ฐ์์ผ๋ก ํ์๋๋ ๋ถ๋ถ์ด ์ต์ ํ๊ฐ ํ์ํ ๊ณณ์ด์ผ!
์๋ฎฌ๋ ์ดํฐ์ Debug ๋ฉ๋ด์์ "Color Blended Layers"์ "Color Offscreen-Rendered" ์ต์ ์ ์ผ๋ฉด ์ฑ๋ฅ ๋ฌธ์ ๊ฐ ์๋ ๋ถ๋ถ์ ์๊ฐ์ ์ผ๋ก ํ์ธํ ์ ์์ด. ๋นจ๊ฐ์์ผ๋ก ํ์๋๋ ๋ถ๋ถ์ด ์ต์ ํ๊ฐ ํ์ํ ๊ณณ์ด์ผ!
๐จ ์ค์ ํ๋ก์ ํธ - ์นด๋ ์ค์์ดํ ์ ๋๋ฉ์ด์
์ด์ ๋ฐฐ์ด ๊ฒ๋ค์ ์ข
ํฉํด์ ์ค์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด์! Tinder ์คํ์ผ์ ์นด๋ ์ค์์ดํ ์ ๋๋ฉ์ด์
์ด์ผ. ๐
์ด ํ๋ก์ ํธ์์๋ UIView ์ ๋๋ฉ์ด์ , Core Animation, ์ ์ค์ฒ ์ธ์, ๊ทธ๋ฆฌ๊ณ ์ธํฐ๋ํฐ๋ธ ์ ๋๋ฉ์ด์ ์ ๋ชจ๋ ์ฌ์ฉํ ๊ฑฐ์ผ!
์ด ํ๋ก์ ํธ์์๋ 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 ํด๋ก์ ๋ก ์ค์์ดํ ์ด๋ฒคํธ๋ฅผ ์ธ๋ถ์ ์๋ ค์ค. ์ด๋ ๊ฒ ํ๋ฉด ๋ทฐ์ปจํธ๋กค๋ฌ์์ ์ฝ๊ฒ ์ฒ๋ฆฌํ ์ ์์ด!
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("๋ค์ ์นด๋ ๋ก๋ฉ...")
}
}
๐ ๊ณ ๊ธ ํ ํฌ๋ - ๋ ๊น์ด ๋ค์ด๊ฐ๊ธฐ
๊ธฐ๋ณธ์ ๋์ด์ ๋ ๊ณ ๊ธ ๊ธฐ์ ๋ค์ ์์๋ณด์! ์ด๊ฒ๋ค์ ๋ง์คํฐํ๋ฉด ์ ๋ง ํ๋กํ์
๋ํ ์ ๋๋ฉ์ด์
์ ๋ง๋ค ์ ์์ด. ๐
๐ญ 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 ๋ฑ)
โข ํ๋ฉด์ด ๋ณด์ด์ง ์์ ๋๋ ์ ๋๋ฉ์ด์ ์ค์ง
โข ๋ฌดํ ๋ฐ๋ณต ์ ๋๋ฉ์ด์ ์ ๊ผญ ํ์ํ ๊ฒฝ์ฐ๋ง ์ฌ์ฉ
โข ๋ณต์กํ ๊ทธ๋ฆผ์๋ ๋ธ๋ฌ ํจ๊ณผ๋ ์ ์คํ๊ฒ ์ฌ์ฉ
โข ๊ฐ๋ฅํ๋ฉด 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์ด ์ฌ์ด๊ฐ ๊ฐ์ฅ ์์ฐ์ค๋ฌ์. ์ด ๋ฒ์๋ฅผ ๊ธฐ๋ณธ์ผ๋ก ํ๊ณ , ํ์์ ๋ฐ๋ผ ์กฐ์ ํ์!
๋๋ถ๋ถ์ ์ ๋๋ฉ์ด์ ์ 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๋ฅผ ๋ง๋ค์ด๋ด. ์์์ ์์ ๋ฒํผ ์ ๋๋ฉ์ด์ ๋ถํฐ๋ผ๋ ๊ด์ฐฎ์. ์ค์ํ ๊ฑด ๊ณ์ ์๋ํ๊ณ ๊ฐ์ ํด๋๊ฐ๋ ๊ฑฐ๋๊น! ๐ซ
ํ์ดํ ! ๐โจ
์ ๋๋ฉ์ด์ ์ ๋จ์ํ "์์๊ฒ ๋ณด์ด๋ ค๊ณ " ํ๋ ๊ฒ ์๋์ผ. ์ฌ์ฉ์์ ์ฑ ์ฌ์ด์ ๋ํ๋ฅผ ๋ง๋๋ ๊ฑฐ์ง. ๋ฒํผ์ ๋๋ ์ ๋์ ํผ๋๋ฐฑ, ํ๋ฉด ์ ํ ์์ ๋งฅ๋ฝ ์ ๊ณต, ๋ก๋ฉ ์ค์์ ์๋ฆฌ๋ ์ธ๋์ผ์ดํฐ... ์ด ๋ชจ๋ ๊ฒ ์ฌ์ฉ์ ๊ฒฝํ์ ๋ง๋ค์ด๊ฐ๋ ์ค์ํ ์์๋ค์ด์ผ. ๐จ
ํต์ฌ ํฌ์ธํธ ์ ๋ฆฌ:
1. ์์์ ๊ฐ๋จํ๊ฒ ๐ฑ
UIView ์ ๋๋ฉ์ด์ ์ผ๋ก ์์ํด. ๋๋ถ๋ถ์ ๊ฒฝ์ฐ ์ด๊ฒ๋ง์ผ๋ก๋ ์ถฉ๋ถํด!
2. ํ์ํ ๋ Core Animation ๐ฏ
๋ ๋ณต์กํ ์ ์ด๊ฐ ํ์ํ๊ฑฐ๋ UIView๋ก ๋ถ๊ฐ๋ฅํ ๊ฒ๋ค์ ํ ๋ Core Animation์ ์ฌ์ฉํด.
3. ์ฑ๋ฅ์ ํญ์ ๊ณ ๋ คํด โก
60fps๋ฅผ ์ ์งํ๋ ๊ฒ ๋ชฉํ์ผ. GPU ๊ฐ์ ์์ฑ์ ํ์ฉํ๊ณ , ๋ถํ์ํ ์คํ์คํฌ๋ฆฐ ๋ ๋๋ง์ ํผํ์.
4. ์ ๊ทผ์ฑ์ ์์ง ๋ง์ โฟ
๋ชจ๋ ์ฌ์ฉ์๋ฅผ ์ํ ์ฑ์ ๋ง๋ค์ด์ผ ํด. ์ ๋๋ฉ์ด์ ๊ฐ์ ์ค์ ์ ์กด์คํ์.
5. ํ์ด๋ฐ์ด ์ค์ํด โฑ๏ธ
๋๋ถ๋ถ์ ์ ๋๋ฉ์ด์ ์ 0.2~0.4์ด๊ฐ ์ ๋นํด. ๋๋ฌด ๋น ๋ฅด๊ฑฐ๋ ๋๋ฆฌ๋ฉด ์ฌ์ฉ์ ๊ฒฝํ์ด ๋๋น ์ ธ.
6. ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ๊ฒ ๋ง๋ค์ ๐ง
์์ฃผ ์ฌ์ฉํ๋ ์ ๋๋ฉ์ด์ ์ ์ ํธ๋ฆฌํฐ ํจ์๋ ํด๋์ค๋ก ๋ง๋ค์ด์ ์ฌ์ฌ์ฉํด.
iOS ๊ฐ๋ฐ์ ๋ฐฐ์ฐ๊ณ ์๋ค๋ฉด, ์ ๋๋ฉ์ด์ ์ ์ ๋ง ์ค์ํ ์คํฌ์ด์ผ. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค๊ณผ ์ง์์ ๋๋๋ฉด์ ํจ๊ป ์ฑ์ฅํ ์๋ ์๊ณ ! ๐ช
์ฒ์์๋ ๋ณต์กํด ๋ณด์ผ ์ ์์ง๋ง, ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์ฐ์ตํ๋ค ๋ณด๋ฉด ์ด๋์ ์์ฐ์ค๋ฝ๊ฒ ๋ฉ์ง ์ ๋๋ฉ์ด์ ์ ๋ง๋ค๊ณ ์๋ ์์ ์ ๋ฐ๊ฒฌํ๊ฒ ๋ ๊ฑฐ์ผ. ์ค์ ๋ก ์ฑ์ ๋ง๋ค๋ฉด์ ๋ค์ํ ์ ๋๋ฉ์ด์ ์ ์๋ํด๋ณด๊ณ , ์ฌ์ฉ์ ํผ๋๋ฐฑ์ ๋ฐ์๋ณด๋ฉด์ ๊ฐ๊ฐ์ ํค์๊ฐ๋ ๊ฒ ๊ฐ์ฅ ์ข์ ํ์ต ๋ฐฉ๋ฒ์ด์ผ! ๐
์ ๋๋ฉ์ด์ ์ ์์ ์ด์ ๊ณผํ์ด์ผ. ๊ธฐ์ ์ ์ธ ๊ตฌํ๋ ์ค์ํ์ง๋ง, ์ฌ์ฉ์์ ๊ฐ์ ๊ณผ ๊ฒฝํ์ ์ดํดํ๋ ๊ฒ๋ ๋๊ฐ์ด ์ค์ํด. ํญ์ "์ด ์ ๋๋ฉ์ด์ ์ด ์ฌ์ฉ์์๊ฒ ์ด๋ค ๊ฐ์น๋ฅผ ์ฃผ๋๊ฐ?"๋ฅผ ์๊ฐํ๋ฉด์ ๋ง๋ค์! ๐จ
์, ์ด์ ๋น์ ์ฐจ๋ก์ผ! ์ค๋ ๋ฐฐ์ด ๋ด์ฉ์ ๋ฐํ์ผ๋ก ๋ฉ์ง ์ธํฐ๋ํฐ๋ธ UI๋ฅผ ๋ง๋ค์ด๋ด. ์์์ ์์ ๋ฒํผ ์ ๋๋ฉ์ด์ ๋ถํฐ๋ผ๋ ๊ด์ฐฎ์. ์ค์ํ ๊ฑด ๊ณ์ ์๋ํ๊ณ ๊ฐ์ ํด๋๊ฐ๋ ๊ฑฐ๋๊น! ๐ซ
ํ์ดํ ! ๐โจ
๐ ์ฌ๊ธฐ๊น์ง ์ฝ์ด์ค์ ๊ณ ๋ง์!
์ง๋ฌธ์ด๋ ํผ๋๋ฐฑ์ด ์๋ค๋ฉด ์ธ์ ๋ ํ์์ด์ผ!
ํจ๊ป ์ฑ์ฅํ๋ ๊ฐ๋ฐ์๊ฐ ๋์! ๐ช๐
์ง๋ฌธ์ด๋ ํผ๋๋ฐฑ์ด ์๋ค๋ฉด ์ธ์ ๋ ํ์์ด์ผ!
ํจ๊ป ์ฑ์ฅํ๋ ๊ฐ๋ฐ์๊ฐ ๋์! ๐ช๐
๊ด๋ จ ํค์๋
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.

๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.