ํ๋ก์ ํธ ์งํ๊ธฐ๊ฐ : 2020๋ 12์ 26์ผ ~ 2021๋ 01์ 15์ผ
๊ธฐ์ ์๊ฐ์ด ๋ฌ๋ผ์ง๋ค๋ฉด, ๋น์ ๋ ๋ณํ ์ ์์ต๋๋ค.
'๋ด'๊ฐ ๋ ๋จ๋ ์๊ฐ์ด ์๋, 'ํด'๊ฐ ๋จ๋ ์๊ฐ๋ถํฐ ํ๋ฃจ๋ฅผ ์์ํ๋ ๋ฏธ๋ผํด ๋ชจ๋.
๋ฏธ๋์ ํตํด ๋ฏธ๋ผํด ๋ชจ๋์ ๋์ ํ๋ฉฐ ๋น์ ๋ง์ ์๋ฏธ์๋ ์์นจ์ ๋ง๋ค์ด ๋๊ฐ๋ณด์ธ์.
์ผ์ฐ ์ผ์ด๋๋ ์ต๊ด์ผ๋ก ํ๋ฃจ๋ฅผ ๊ธธ๊ฒ ๋ณด๋ด๋ฉด, ์ฑ์ฅ์ ๋ฐํ์ ๋ง๋ จํ ์ ์์ต๋๋ค.
๋ฏธ๋๊ณผ ํจ๊ป ์ฒด๊ณ์ ์ธ ๊ณํ์ ์ธ์ฐ๊ณ ์ด๋ฅผ ๊ท์น์ ์ผ๋ก ์ค์ฒํ๋ฉด์ ์ฑ์ทจ๊ฐ์ ์ป์ด๋ณด์ธ์.
์ฑ์ฅ์งํฅ์ ์ธ ๊ทธ๋ฃน์๋ค๊ณผ ๋ชฉํ๋ฅผ ๊ณต์ ํ๋ค๋ฉด ์ฐ๋ฆฌ๋ ํจ๊ป, ๋ ๋ฉ๋ฆฌ ๊ฐ ์ ์์ต๋๋ค.
Swift 4 Xcode swift iOS COCOAPODS
Moya Alamofire Kingfisher lottie-ios
๐ป meaning โฃ ๐ Global โ โฃ ๐ Extension โ โ โ ๐ Fonts+Extension.swift โ โฃ ๐ Model โ โ โ ๐ GenericResponse.swift โ โ ๐ Service โ โ โ ๐NetworkResult.swift โฃ ๐ Screen โ โฃ ๐ Home โ โ โฃ ๐ Cell โ โ โ โ ๐ CardListCell.swift โ โ โฃ ๐ Storyboard โ โ โ โ ๐ Home.storyboard โ โ โ ๐ ViewController โ โ โ โ ๐ HomeVC.swift โ โ ๐ Login โ โ โฃ ๐ Storyboard โ โ โ โ ๐ Login.storyboard โ โ โ ๐ ViewController โ โ โ โ ๐ LoginVC.swift โ ๐ Support โ โฃ ๐ Font โ โฃ ๐ Assets.xcassets โ โฃ ๐ LaunchScreen.storyboard โ โฃ ๐ AppDelegate.swift โ โฃ ๐ SceneDelegate.swift โ โ ๐ Info.plist โ ๐ meaning.xcodeproj
- TapBar : ์ปค์คํ ํญ๋ฐ
- Login : ์คํ๋์ฌ
- Login : ๋ก๊ทธ์ธ
- Onboarding : ๋๋ค์ ๋ฐ ๊ธฐ์์๊ฐ ์ ๋ ฅ
- Home : ํ, ์บ๋ฆฐ๋ ํ๋ฉด
- Camera : ํ์์คํฌํ
- Mission : ๋ฏธ์ ์นด๋
- MyPage : ๋ง์ดํ์ด์ง
- GroupList : ๊ทธ๋ฃนํญ(๊ทธ๋ฃน ๋ชฉ๋ก + ๊ทธ๋ฃน ์์ฑ)
- GroupFeed : ๊ทธ๋ฃนSNS(๊ทธ๋ฃน ๊ธ ๋ชฉ๋ก + ๊ธ ์์ธํ๋ณด๊ธฐ + ๊ทธ๋ฃน ์ค์ )
- ๊ธฐ์ค iPhone : ์์ดํฐse2, ์์ดํฐ12mini, ์์ดํฐ12Pro
- ํ
์คํธ ๊ณ์ : ์์ด๋ - iOS@meaning.com / ๋น๋ฐ๋ฒํธ - iosmeaning
| ํ๋ฉด | |
|---|---|
| ๋ฏธ์ ์ด ์๋ฃ๋๋ฉด ์์ฐจ์ ์ผ๋ก ํด๋น ๋ฏธ์ ์ด ์๋ฃ๋์ด ํ์์ ํ์ธํ ์ ์์ต๋๋ค. ํ๋ฒ ์๋ฃ๋ ๋ฏธ์ ์ ๋ค์ ํ ์ ์์ต๋๋ค. |
| ํ๋ฃจ๋ค์ง ๋ฏธ์ ํ๋ฉด | ํ๊ณ ์ผ๊ธฐ ๋ฏธ์ ํ๋ฉด | ํ๊ณ ์ผ๊ธฐ ์์ฑ ํ๋ฉด |
|---|---|---|
| ์งง์๋ ์ ๋ฏธ์ ํ๋ฉด | ์งง์๋ ์ ์์ฑ ํ๋ฉด |
|---|---|
| ํ์์คํฌํ ๋ฏธ์ ํ๋ฉด | ํ์์คํฌํ ์์ฑ ํ๋ฉด |
|---|---|
| ํ๋ฉด | |
|---|---|
| ๋ค์ํ ๊ทธ๋ฃน์ ๊ตฌ๊ฒฝํ ์ ์๋ ๋ชฉ๋ก ์ฐฝ ์ ๋๋ค. ์ข์ฐ collectionview ๋ก ํ์ธํ ์ ์์ผ๋ฉฐ, ๋ ๊ทธ ์๋๋ก๋ ํ ์ด๋ธ๋ทฐ๋ก๋ ์ ๋ณด๊ฐ ์ ๊ณต๋ฉ๋๋ค. |
| ๊ทธ๋ฃน ์์ธ๋ณด๊ธฐ | ์ฐธ๊ฐ๋ฒํผ ๋๋ฅธ ํ | ์ฐธ๊ฐ ํ ๊ทธ๋ฃน ๋ชฉ๋ก ์์ ์ด ์ฐธ๊ฐํ ๊ทธ๋ฃน ๋์ด์ ๋ณด์ด์ง ์์ |
|---|---|---|
| ๊ทธ๋ฃน ํผ๋ ๋น์์ ๋ ํ๋ฉด | ๊ทธ๋ฃน ํผ๋ ๋ด์ฉ ํ๋ฉด |
|---|---|
| ์ค์ ํ๋ฉด | ๊ทธ๋ฃน ํผ๋ ์์ธ๋ณด๊ธฐ ํ๋ฉด |
|---|---|
| ์ฐ์ ์์ | ๊ธฐ๋ฅ๋ช | ์ค๋ช | ๊ตฌํ์ฌ๋ถ | ๋ด๋น์ |
|---|---|---|---|---|
| P1 | ์คํ๋์ฌ | ์ฑ ์คํ์ ์คํ๋์ฌ๊ฐ ๋ณด์ฌ์ง๋ค. | ๐ฃ | ์ ๋ฏผ์น |
| P1 | ๋ก๊ทธ์ธ | ๋ก๊ทธ์ธ์ ํ์ฌ ๋ฏธ๋ ์ฑ์ ์ฌ์ฉํ๋ค. | ๐ฃ | ์ ๋ฏผ์น |
| P1 | ์จ๋ณด๋ฉ(๋๋ค์) | ์ฌ์ฉ์๊ฐ ์ํ๋ ๋๋ค์์ ์ ๋ ฅํ๋ค. | ๐ฃ | ๊น๋ฏผํฌ |
| P1 | ์จ๋ณด๋ฉ(๊ธฐ์์๊ฐ) | ์ค์ 5์๋ถํฐ ์ค์ 8์ ์ฌ์ด์ ๋ชฉํ ๊ธฐ์์๊ฐ์ ์ค์ ํ๋ค. | ๐ฃ | ๊น๋ฏผํฌ |
| P1 | ์จ๋ณด๋ฉ(ํ์๊ธ) | ์ฌ์ฉ์๋ฅผ ํ์ํ๋ฉฐ, ํ์ผ๋ก ์ฐ๊ฒฐ๋๋ค. | ๐ฃ | ๊น๋ฏผํฌ |
| P1 | ์ปค์คํ ํญ๋ฐ | ๊ฐ์ด๋ฐ ์นด๋ฉ๋ผ ๋ฒํผ์ ์ํ์ผ๋ก ํญ๋ฐ๋ฅผ ์ปค์คํ ํ๋ค. ํญ๋ฐ ์์ดํ ์ ํด๋ฆญํ์ฌ, ํด๋น ๋ทฐ๋ก ์ด๋ํ๋ค. | ๐ฃ | ๋ฐ์ธ์ |
| P1 | ์นด๋ฉ๋ผ (ํ์์คํฌํ) | ํ์ฌ ์๊ฐ์ด ์ฆ๊ฐ ๋ฐ์๋์ด ์ด๋ฏธ์ง์ ํจ๊ป ์ดฌ์์ด ๋๋ฉฐ, ๊ฐค๋ฌ๋ฆฌ์ ์ ์ฅ๋๋ค. | ๐ฃ | ๊น๋ฏผํฌ |
| P1 | ํ | ๋ฏธ์
์ ์ข์ฐ ์ฌ๋ผ์ด๋๊ฐ ๋๋๋กํ๋ฉฐ, ์๋จ ๋ ์ง๋ฅผ ํด๋ฆญํ๋ฉด ์บ๋ฆฐ๋๋ก ๋์ด๊ฐ๋ค. ๋ฏธ์ ์ ์๋ฃํ๋ฉด, ๋ฏธ์ ์๋ฃ ํ ์คํธ๊ฐ ๋ณด์ฌ์ง๋ ์นด๋๋ก ๋ณํ๋ค. ๋ฏธ์ ์ ์์ฐจ์ ์ผ๋ก ์ํํ์ง ์์ ๊ฒฝ์ฐ, ์ด์ ๋จผ์ ํด๋ฌ๋ผ๋ ํ ์คํธ ์๋ฆผ์ ๋ณด์ฌ์ค๋ค. |
๐ฃ | ๊น๋ฏผํฌ |
| P1 | ์บ๋ฆฐ๋ | ๋ฉ์ธ ํ์์ ์๋จ ๋ ์ง๋ฅผ ๋๋ฅด๋ฉด ์บ๋ฆฐ๋๊ฐ ๋ณด์ธ๋ค. ๋ฏธ์ ์๋ฃ ์ ํด๋น์ผ์ ๋ณ์ด ์ฑ์์ง๋ค. |
๐ก | ๊น๋ฏผํฌ |
| P1 | ํผ๋ ์ ๋ก๋ (์ฌ์ง ์ ๋ก๋) | ์ฌ์ง์ ๋ง์ด ํผ๋์ ๊ฐ์ ๋ ๊ทธ๋ฃน ํผ๋์ ์ ๋ก๋ ํ๋ค. | ๐ก | ๊น๋ฏผํฌ, ์ ๋ฏผ์น |
| P2 | ๋ฏธ์ ์นด๋(์ค๋ ํ๋ฃจ ๋ค์ง) | ๋ชจ๋๋ฏธ๋ผํด๊ณผ ๊ด๋ จ๋ ๊ธ๊ท๋ฅผ ๋งค์ผ ์ค๋ณต์ ํผํ๋ฉด์ ๋ณด์ฌ์ค๋ค. | ๐ก | ์ ๋ฏผ์น |
| P2 | ๋ฏธ์ ์นด๋(์๊ธฐํ๊ณ /์ผ๊ธฐ) | 200์ ์ด๋ด๋ก ์๊ธฐํ๊ณ ๋ฅผ ํ ์ ์๋ ํ ์คํธํ๋๊ฐ ์๋ค. | ๐ก | ์ ๋ฏผ์น |
| P2 | ๋ฏธ์ ์นด๋(์ฑ ํ์คํ) | ์ฑ ์ ์ฝ๊ณ 200์ ์ด๋ด๋ก ๊ฐ์ํ์ด๋ ํ์คํ์ ๋จ๊ธธ ์ ์๋ ํ ์คํธ๊ฐ ์๋ค. | ๐ก | ์ ๋ฏผ์น |
| P2 | ๋ง์ดํผ๋ | ๊ทธ๋์ ๋ด๊ฐ ์ฌ๋ฆฐ ๋ฏธ๋ผํด ๋ชจ๋ ์ธ์ฆ์ท์ ์ธ๋ก ์คํฌ๋กค๋ก ๋ด๋ ค ๋ณผ ์ ์๊ณ , ๋์ ๋ฌ์ฑ ํ์๋ฅผ ๋ณด์ฌ์ค๋ค. | ๐ก | ์ ๋ฏผ์น, ๊น๋ฏผํฌ |
| P2 | ๊ทธ๋ฃน ๋ชฉ๋ก | ๋ด๊ฐ ๊ฐ์ ํ ๊ทธ๋ฃน, ๋ค๋ฅธ ๊ทธ๋ฃน๋ค์ ์ดํด๋ณผ ์ ์๋ค. | ๐ก | ๋ฐ์ธ์, ๊น๋ฏผํฌ |
| P2 | ๊ทธ๋ฃน ์์ธ๋ณด๊ธฐ | ๊ทธ๋ฃน ๋ชฉ๋ก์์ ๊ทธ๋ฃน์ ํด๋ฆญํ๋ฉด ๊ทธ๋ฃน์ด๋ฆ, ๊ทธ๋ฃน ์ ๋ณด, ์ธ์์ ๋ฐ ์ฐธ๊ฐ์ธ์์ ํ์ธํ ์ ์๋ค. | ๐ก | ๋ฐ์ธ์ |
| P2 | ๊ทธ๋ฃน ์์ฑ | ๊ทธ๋ฃน์ ์ง์ ๋ง๋ค์ด์ ๊ทธ๋ฃน์ ๊ด๋ฆฌํ ์ ์๋ค. ์ด๋ฏธ ๋ด ๊ทธ๋ฃน์ด ์๊ฑฐ๋, ์ด๋ฏธ ์๋ ์ด๋ฆ์ผ ๊ฒฝ์ฐ ์์ฑ์ด ๋ถ๊ฐํ๋ค. | ๐ก | ๋ฐ์ธ์ |
| P2 | ๊ทธ๋ฃน ์ฐธ์ฌ | ๊ทธ๋ฃน ์ฐธ์ฌํ๊ธฐ ๋ฒํผ์ ๋๋ ์ ๋ 1) ๊ฐ์ ํ ๊ทธ๋ฃน์ด ์๋ ๊ฒฝ์ฐ, ๊ฐ์ ์ด ์๋ฃ๋๋ค. 2) ๊ฐ์ ํ ๊ทธ๋ฃน์ด ์๋ ๊ฒฝ์ฐ, ์ด๋ฏธ ๊ฐ์ ๋ ๊ทธ๋ฃน์ด ์๋ค๋ ํ์ ์ด ๋ณด์ธ๋ค. |
๐ก | ๋ฐ์ธ์ |
| P2 | ๊ทธ๋ฃน ํผ๋ | ๊ทธ๋์ ๊ทธ๋ฃน ๋ฉค๋ฒ๋ค์ด ์ฌ๋ฆฐ ๋ฏธ๋ผํด ๋ชจ๋ ์ธ์ฆ์ท์ ์ธ๋ก ์คํฌ๋กค๋ก ๋ด๋ ค ๋ณผ ์ ์๊ณ , ์ผ๋ง๋ ๋ง์ ๊ทธ๋ฃน์๋ค์ด ์ฐธ์ฌํ๊ณ ์๋์ง๋ฅผ ๋ณด์ฌ์ค๋ค. ๊ทธ๋ฃน์ ๊ธ์ด ์ฌ๋ผ์ค์ง ์์ ๊ฒฝ์ฐ, ๊ฒ์๋ฌผ์ด ์๋ค๋ ๋ฉํธ์ ํจ๊ป [ํ์ผ๋ก ๋์๊ฐ๊ธฐ] ๋ฒํผ์ ๋ณด์ฌ์ค๋ค. |
๐ก | ๊น๋ฏผํฌ |
| P3 | ์ธ์ฆ๊ธ ์์ธ๋ณด๊ธฐ | ๊ทธ๋ฃน์์ ๋ค๋ฅธ ์ฌ๋์ ์ธ์ฆ๊ธ์ ํด๋ฆญํ๋ฉด ์ธ์ฆ๊ธ์ ๋ณผ ์ ์๋ค. | ๐ข | ๊น๋ฏผํฌ |
| P3 | ๊ทธ๋ฃน ์ค์ | ๊ทธ๋ฃน ์ ๋ณด ๋ฐ ๊ทธ๋ฃน์ ์ ๋ณด๋ฅผ ๋ณด์ฌ์ค๋ค. | ๐ข | ๋ฐ์ธ์ |
Meaning iOS ํ์ ๋์๋ ๋์ ์ ๋๋ ค์ํ์ง ์์ต๋๋ค. ์ด๋ฒ ํ๋ก์ ํธ์์ ๊ฐ์ ํด๋ณด์ง ์์๋ ์๋ก์ด ๊ธฐ์ ๋ค์ ๋์ ํ๊ณ ๊ณต๋ถํ๋ ์๊ฐ์ ๊ฐ์ ธ๋ณด์์ต๋๋ค.
Moya ํ๋ ์ ์ํฌ ์ด์ฉํ๊ธฐ
- ์ถ์ํ ๋คํธ์ํน ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- URLSession๊ณผ Alamofire๋ฅผ ํ๋ฒ ๋ ๊ฐ์ผ API
- moya๊ฐ ์ ์ํ๋ ๊ธฐ๋ณธ ๊ตฌํ ๋ฐฉ์์ ๋ฌธ์ ์ ์?
1. ์๋ก์ด ์ฑ์ ์ฐ๊ธฐ ํ๋ค๊ฒ ๋ง๋ ๋ค.
2. ์ฑ์ ์ ์งํ๊ธฐ ์ด๋ ต๊ฒ ๋ง๋ ๋ค.
3. unit ํ
์คํธ๋ฅผ ํ๊ธฐ ์ด๋ ต๊ฒ ๋ง๋ ๋ค.
- ๊ทธ๋ผ moya๋ ๋ญ๊ฐ ๋ ์ข์๊น์?
- moya๋ ์ด๊ฑฐํ(enum)์ ์ฌ์ฉํ์ฌ ๋คํธ์ํฌ ์์ฒญ ๋ฐฉ์์ type-safeํ ๋ฐฉ์์ผ๋ก ์บก์ํ ํ๋๋ฐ ์ด์ฒจ์ ๋ง์ถ ํ๋ ์์ํฌ
- moya๋ ์์ฒด์ ์ธ ๋คํธ์ํฌ ์ํ์ X, Alamofire์ ๋คํธ์ํน ์๋น์ค๋ฅผ ์ฌ์ฉํ๊ณ , ์ถ์ํ ํ๊ธฐ ์ํ ๊ธฐ๋ฅ๋ค์ ์ ๊ณตํ๋ค. โ ๊ฒฐ๋ก : Alamofire ์ง์ ์ฌ์ฉX, Alamofire๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ๊ณ ์๋ Moya๋ฅผ ๊ฑฐ์ณ ์ฌ์ฉ O!
- pod ์ ์ค์นํ๊ธฐ โ Moya๋ฅผ ์ค์นํ๋ฉด ์๋์ผ๋ก Alamofire๋ ์ค์น๋๋ ํํ์ ๋๋ค.
-
์๋ฒ ํต์ ์ ํ์ํ API๋ฅผ enum์ ์ด์ฉํด case๋ณ๋ก ์ถ์ํํฉ๋๋ค.
- case ๋ณ๋ก ๋๋ ์ ์ถ์ํ ํจ์ผ๋ก์จ ํ๋์ api ๋ณ ํต์ ์ ํ์ํ type์ ๋ณผ ์ ์๊ณ , ์์ ํ๊ธฐ ํธ๋ฆฌํฉ๋๋ค.
import Foundation import Moya enum APITarget { // case ๋ณ๋ก api๋ฅผ ๋๋ ์ค๋๋ค case onboard(token: String, nickName: String, wakeUpTime: String) // ์จ๋ณด๋ case timestamp(token: String, dateTime: String, timeStampContents: String, image: UIImage) // ํ์์คํฌํ ์์ฑ case groupEdit(token: String, groupid: Int) // ๊ทธ๋ฃน ์ค์ } // MARK: TargetType Protocol ๊ตฌํ extension APITarget: TargetType { var baseURL: URL { // baseURL - ์๋ฒ์ ๋๋ฉ์ธ return URL(string: "[์๋ฒ ๋๋ฉ์ธ]")! } var path: String { // path - ์๋ฒ์ ๋๋ฉ์ธ ๋ค์ ์ถ๊ฐ ๋ ๊ฒฝ๋ก switch self { case .onboard: return "/user/onboard" case .timestamp: return "/timestamp" case .groupEdit(_, let groupid): return "/group/\(groupid)/edit" } } var method: Moya.Method { // method - ํต์ method (get, post, put, delete ...) switch self { case .timestamp: return .post case .onboard: return .put case .groupEdit: return .get } } var sampleData: Data { // sampleDAta - ํ ์คํธ์ฉ Mock Data return Data() } var task: Task { // task - ๋ฆฌํ์คํธ์ ์ฌ์ฉ๋๋ ํ๋ผ๋ฏธํฐ ์ค์ switch self { case .onboard( _, let nickName, let wakeUpTime): // ํ๋ผ๋ฏธํฐ ์กด์ฌ์ return .requestParameters(parameters: ["nickName" : nickName, "wakeUpTime": wakeUpTime], encoding: JSONEncoding.default) case .timestamp(_, let dateTime, let timeStampContents, let image): // multipart/form-data ์ฌ์ฉ์ let dateTimeData = MultipartFormData(provider: .data(dateTime.data(using: .utf8)!), name: "dateTime") let timeStampContentsData = MultipartFormData(provider: .data(timeStampContents.data(using: .utf8)!), name: "timeStampContents") let imageData = MultipartFormData(provider: .data(image.jpegData(compressionQuality: 1.0)!), name: "image", fileName: "jpeg", mimeType: "image/jpeg") let multipartData = [dateTimeData, timeStampContentsData, imageData] return .uploadMultipart(multipartData) case .groupEdit: // ํ๋ผ๋ฏธํฐ๊ฐ ์กด์ฌํ์ง ์์ ์ return .requestPlain } } var validationType: Moya.ValidationType { // validationType - ํ์ฉํ response์ ํ์ return .successAndRedirectCodes // successAndRedirectCodes - Array(200..<400) } var headers: [String : String]? { // headers - HTTP header switch self { case .onboard(let token, _, _), .groupEdit(let token, _): return ["Content-Type" : "application/json", "token" : token] case .timestamp(let token, _, _, _): return ["Content-Type" : "multipart/form-data", "token" : token] } } }
-
๋ฐ์ดํฐ ํต์ ๋ถ๊ธฐ์ฒ๋ฆฌ๋ฅผ ์ํ ๋ชจ๋ธ์ ๋ง๋ญ๋๋ค.
import Foundation import Moya struct APIService { static let shared = APIService() // ์ฑ๊ธํค ๊ฐ์ฒด ์์ฑ let provider = MoyaProvider<APITarget>() // MoyaProvider(->์์ฒญ ๋ณด๋ด๋ ํด๋์ค) ์ธ์คํด์ค ์์ฑ func timestamp(_ token: String, _ dateTime: String, _ timeStampContents: String, _ image: UIImage, completion: @escaping (NetworkResult<TimestampData>)->(Void)) { // ํ์์คํฌํ๋ฅผ ์ ๋ก๋ ํ๋ ํจ์๋ฅผ ๋ง๋ค์ด ๋ด ๋๋ค. // TimestampData๋ ์๋ฒ์์ ๋ฐ์์จ data๋ฅผ ๋ฃ์ด์ค ๊ตฌ์กฐ์ฒด ์ ๋๋ค. let target: APITarget = .timestamp(token: token, dateTime: dateTime, timeStampContents: timeStampContents, image: image) // APITarget์์ ๋ง๋ค์ด์ค case ์ค ํ๋๋ฅผ ์ ํํฉ๋๋ค! judgeObject(target, completion: completion) } // requestํ๊ณ decode ํ๋ ์ฝ๋๋ฅผ ๋ฐ๋ณตํด์ ์ฌ์ฉํ ์ ์๊ฒ ํจ์๋ก ์ ์ํด๋ณด์์ต๋๋ค func judgeObject<T: Codable>(_ target: APITarget, completion: @escaping (NetworkResult<T>) -> Void) { provider.request(target) { response in switch response { case .success(let result): do { let decoder = JSONDecoder() let body = try decoder.decode(GenericResponse<T>.self, from: result.data) if let data = body.data { completion(.success(data)) } } catch { print("๊ตฌ์กฐ์ฒด๋ฅผ ํ์ธํด๋ณด์ธ์") } case .failure(let error): completion(.failure(error.response!.statusCode)) } } } }
-
์ํ๋ ViewController ์์ ์๋ฒ ํต์ ํจ์๋ฅผ ๋ถ๋ฌ์ต๋๋ค
var timestampData: TimestampData? func uploadPictrue(_ token: String, _ dateTime: String, _ timeStampContents: String, _ image: UIImage) { APIService.shared.timestamp(token, dateTime, timeStampContents, image) { [self] result in switch result { case .success(let data): data = timestampData // ์ฑ๊ณต ์ ์ฒ๋ฆฌ ๋ก์ง case .failure(let error): if error == 400 { } else if error = 404 { } } } }
- meaning์์๋ ํ์ ์คํฌํ ๊ธฐ๋ฅ์ ์ํด ์นด๋ฉ๋ผ ์์ ํ์ฌ ์๊ฐ๊ณผ ๋ฏธ๋์ ๋ก๊ณ ๋ฅผ ์ฌ๋ ค ํจ๊ป ์ดฌ์ํฉ๋๋ค.
- ํธ๋ํฐ์์ ๋ณดํต ์ฌ์ฉํ๋ ๊ธฐ๋ณธ ์นด๋ฉ๋ผ UIImagePickerController๊ฐ ์๋ AVFoundation๋ฅผ ์ฌ์ฉํด ์๋ก์ด ์นด๋ฉ๋ผ ํ๋ฉด์ ๊ตฌํํด์ฃผ์์ต๋๋ค.
import UIKit import AVFoundation class TimeStampVC: UIViewController { // MARK: Variable Part var captureSession: AVCaptureSession! // ์ค์๊ฐ ์บก์ณ๋ฅผ ์ํ ์ธ์ var stillImageOutput: AVCapturePhotoOutput! // ์บก์ณํ ์ด๋ฏธ์ง๋ฅผ ์ถ๋ ฅ var videoPreviewLayer: AVCaptureVideoPreviewLayer! // ์บก์ณ๋ ๋น๋์ค๋ฅผ ํ์ํด์ฃผ๋ Layer var timeStampImage: UIImage? var rootView: String? // MARK: Life Cycle Part override func viewDidLoad() { super.viewDidLoad() setCameraView() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) self.captureSession.stopRunning() } override func viewDidAppear(_ animated: Bool) { setCaptureSession() } } // MARK: Extension extension TimeStampVC { // MARK: Function func setupLivePreview() { videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession) // captureSession๋ฅผ ์ฌ์ฉํด ์บก์ณํ ๋น๋์ค๋ฅผ ํ์ํด์ค videoPreviewLayer.videoGravity = .resizeAspectFill // videoGravity: ์ฝํ ์ธ ๋ฅผ ํ์ํ๋ ๋ฐฉ๋ฒ -> resizeAspectFill: ๋น์จ์ ์ ์งํ๋ฉด์ ์ฑ์ฐ๊ธฐ videoPreviewLayer.connection?.videoOrientation = .portrait // portrait - ์ธ๋ก, landscape - ๊ฐ๋ก๋ชจ๋ cameraView.layer.addSublayer(videoPreviewLayer) // cameraView์ ์์น์ videoPreviewLayer๋ฅผ ๋์ } func setCaptureSession() { captureSession = AVCaptureSession() captureSession.sessionPreset = .high // ์บก์ณ ํ์ง์ high๋ก ์ค์ // default video ์ฅ์น๋ฅผ ์ฐพ๋๋ค guard let backCamera = AVCaptureDevice.default(for: AVMediaType.video) else { print("Unable to access back camera!") return } do { // ์ฐพ์ video ์ฅ์น๋ฅผ ์บก์ณ ์ฅ์น์ ๋ฃ์ let input = try AVCaptureDeviceInput(device: backCamera) stillImageOutput = AVCapturePhotoOutput() // ์ฃผ์ด์ง ์ธ์ ์ ์บก์ณ์ ์ฌ์ฉํ ์ ์๋์ง + ์ธ์ ์ ์ถ๊ฐํ ์ ์๋์ง ๋จผ์ ํ์ ํ๋ค if captureSession.canAddInput(input) && captureSession.canAddOutput(stillImageOutput) { // ์ฃผ์ด์ง ์ ๋ ฅ์ ์ถ๊ฐํ๋ค captureSession.addInput(input) // ์ฃผ์ด์ง ์ถ๋ ฅ ์ถ๊ฐ captureSession.addOutput(stillImageOutput) setupLivePreview() } } catch let error { print(error.localizedDescription) } // startRunning๋ ์๊ฐ์ด ๊ฑธ๋ฆด ์ ์๋ ํธ์ถ์ด๋ฏ๋ก main queue๊ฐ ๋ฐฉํด๋์ง ์๊ฒ serial queue์์ ์คํํด์ค๋ค DispatchQueue.global(qos: .userInitiated).async { // ์ธ์ ์คํ ์์ self.captureSession.startRunning() // ์ฝ๋ฐฑ ํด๋ก์ ๋ฅผ ํตํด ์ธ์ ์คํ์ด ์์ํ๋ ์์ ์ด ๋๋๋ค๋ฉด // cameraView์ AVCaptureVideoPreviewLayer๋ฅผ ๋์ฐ๊ฒ ๋ง๋ ๋ค DispatchQueue.main.async { self.videoPreviewLayer.frame = self.cameraView.bounds } } } }
- ์ด์ ํ๋ฉด์ ๋์จ ์ด๋ฏธ์ง๋ฅผ ์ดฌ์(์บก์ณ)ํ๋ ์ญํ ์ด ๋จ์์ต๋๋ค. ๊ธฐ์กด์ ์นด๋ฉ๋ผ ์ดฌ์ ๋ฒํผ์ ์ญํ ์ ๊ตฌํํด์ฃผ์ด์ผํฉ๋๋ค. AVCapturePhotoCaptureDelegate๋ฅผ ์ด์ฉํด ์ฌ์ง์ ์บก์ณํ ํ์ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ต๋๋ค.
// MARK: IBAction @IBAction func shootingButtonDidTap(_ sender: Any) { // ์นด๋ฉ๋ผ ์ดฌ์ ๋ฒํผ ํด๋ฆญ ์ Action let settings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg]) // jpeg ํ์ผ ํ์์ผ๋ก format stillImageOutput.capturePhoto(with: settings, delegate: self) // AVCapturePhotoCaptureDelegate ์์ } extension TimeStampVC: AVCapturePhotoCaptureDelegate { func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { guard let imageData = photo.fileDataRepresentation() else { return } let image = UIImage(data: imageData) timeStampImage = image?.cropToBounds(width: Double(cameraView.layer.frame.width), height: Double(cameraView.layer.frame.width)) // cropToBounds ๋ผ๋ Extesnion์ ํตํด ์ ๋ฐฉํ ํฌ๊ธฐ๋ก ํฌ๋กญํด์ฃผ์๋ค. guard let checkVC = self.storyboard?.instantiateViewController(identifier: "PhotoCheckVC") as? PhotoCheckVC else { return } // ๋ค์ ๋ทฐ๋ก ์ด๋ฏธ์ง๋ฅผ ๋๊ฒจ์ฃผ์๋ค. checkVC.photoImage = timeStampImage self.navigationController?.pushViewController(checkVC, animated: true) } }
- ์บก์ณ ์ด๋ฏธ์ง๋ ๋ด๊ฐ ์ํ๋ ํฌ๊ธฐ๋ก ์บก์ณ๊ฐ ๋์ง ์์ต๋๋ค. ๋จ์ํ ์ปค์คํ ํ ์นด๋ฉ๋ผ ํ๋ฉด์ ๋ณด์ฌ์ง๋ ํน์ ๋ทฐ์์์ user์๊ฒ ๋ณด์ฌ์ง๋ ํฌ๊ธฐ์ด๊ณ , ์บก์ณ ์ด๋ฏธ์ง๋ ์ผ๋ฐ ์นด๋ฉ๋ผ์ ๋น์จ์ 4:3์ผ๋ก ๋์ค๊ฒ ๋ฉ๋๋ค.
- ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ cropToBounds ๋ผ๋ Extension์ ๋ง๋ค์ด ์ฌ์ง์ ์ํ๋ ํฌ๊ธฐ๋ก ์๋ผ์ฃผ์์ต๋๋ค.
import UIKit extension UIImage { func cropToBounds(width: Double, height: Double) -> UIImage { // ์ด๋ฏธ์ง๋ฅผ ์ํ๋ ํฌ๊ธฐ๋ก ์๋ผ์ค๋๋ค let cgimage = self.cgImage! let contextImage: UIImage = UIImage(cgImage: cgimage) let contextSize: CGSize = contextImage.size var posX: CGFloat = 0.0 var posY: CGFloat = 0.0 var cgwidth: CGFloat = CGFloat(width) var cgheight: CGFloat = CGFloat(height) // width์ height ์ค ๋ ํฐ ๊ธธ์ด๋ฅผ ์ค์ฌ์ผ๋ก ์๋ฅธ๋ค. if contextSize.width > contextSize.height { posX = ((contextSize.width - contextSize.height) / 2) posY = 0 cgwidth = contextSize.height cgheight = contextSize.height } else { posX = 0 posY = ((contextSize.height - contextSize.width) / 2) cgwidth = contextSize.width cgheight = contextSize.width } let rect: CGRect = CGRect(x: posX, y: posY, width: cgwidth, height: cgheight) // rect๋ฅผ ์ด์ฉํด์ bitmap ์ด๋ฏธ์ง๋ฅผ ์์ฑํ๋ค. let imageRef: CGImage = cgimage.cropping(to: rect)! // imageRef ์ด๋ฏธ์ง๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ ์ด๋ฏธ์ง๋ฅผ ๋ง๋ ํ, ์๋ ๋ฐฉํฅ์ผ๋ก ๋ค์ ๋๋ ค์ค๋ค. let image: UIImage = UIImage(cgImage: imageRef, scale: self.scale, orientation: self.imageOrientation) return image } }
- ๋ํ ํ์์คํฌํ ์นด๋ฉ๋ผ ์์์ ์๊ฐ์ด ์ง๋๋ฉด ์๋์ผ๋ก ์๊ฐ์ด ํ๋ฅด๋๋ก ํ๊ธฐ ์ํด Timer๋ฅผ ์ด์ฉํด 1์ด๋ง๋ค ํ์ฌ ์๊ฐ์ ๊ฒ์ฌํด ๋ถ(minutes) ์ด ๋ฐ๋๋ค๋ฉด ๋ผ๋ฒจ์ ์๊ฐ์ ์์ ํด์ค๋๋ค.
Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(nowTimeLabel), userInfo: nil, repeats: true) @objc func nowTimeLabel() { // ํ์ฌ ์๊ฐ์ ๊ธฐ๋ฐ์ผ๋ก time๊ณผ ๋ ์ง๋ฅผ label์ ๋ฃ์ด์ค stampTimeLabel.text = Date().datePickerToString().recordTime() stampDateLabel.text = Date().datePickerToString().recordDate() + " (\(Date().weekDay()))" }
- ํ์์ ์นด๋๋ฅผ ๋๊ธธ ๋ CollectionView๋ฅผ ์ฌ์ฉํด์ ๊ตฌํ์ ํ๋๋ฐ, ๋จ์กฐ๋ก์ด ๋๋์ ํผํ๊ธฐ ์ํด ๊ฐ์ด๋ฐ ์ค๋ cell์ ๊ฐ์กฐํด์ฃผ๋ carousel ํจ๊ณผ(ํน์ ํ์ ๋ชฉ๋ง ํจ๊ณผ)์ Animation์ ๊ตฌํํด๋ณด์์ต๋๋ค.
- UICollectionViewFlowLayout๋ผ๋ ๊ฒ์ ์ฒ์ ์ฌ์ฉํด๋ณด์์ต๋๋ค. UICollectionViewFlowLayout๋ฅผ ์ฌ์ฉํ๋ฉด cell์ ์ํ๋ ํํ๋ก ์ ๋ ฌํ ์ ์๊ฒ ๋์์ค๋๋ค.
let customLayout = AnimationFlowLayout() missonCardCollectionView.collectionViewLayout = customLayout // ์ํ๋ CollectionView์ ์ ์ธํด์ ์ฌ์ฉํฉ๋๋ค.
import UIKit class AnimationFlowLayout: UICollectionViewFlowLayout { // ์ ์ด ์ด์ ํ๋ฆ(์ธ๋ก, ๊ฐ๋ก)์ ๋ฐ๋ผ ์ด๋ ํ ๋ ๋ณด์ฌ์ง๋ ๊ฒ์ ๋ด๋นํ๋ค // MARK: Variable Part private var firstTime: Bool = false // ์ด๊ธฐ ํ๋ฒ๋ง ์ค์ ๋๊ธฐ ์ํด ๋ณ์๋ฅผ ์ ์ธ override func prepare() { super.prepare() guard !firstTime else { return } guard let collectionView = self.collectionView else { return } let collectionViewSize = collectionView.bounds itemSize = CGSize(width: collectionViewSize.width-50*2, height: 100) // itemSize - ์ ์ ๊ธฐ๋ณธ ํฌ๊ธฐ let xInset = (collectionViewSize.width-itemSize.width) / 2 - 50 self.sectionInset = UIEdgeInsets(top: 0, left: xInset, bottom: 0, right: xInset) // sectionInset - ์น์ ๊ฐ์ ์ฌ๋ฐฑ scrollDirection = .horizontal // ๊ฐ๋ก ์คํฌ๋กค์ ์ฌ์ฉํ ๊ฒ์ด๋ผ๋ ๊ฑธ ์๋ ค์ค๋ค minimumLineSpacing = 10 - (itemSize.width - itemSize.width*0.7)/2 // minimumLineSpacing - ํ ์ฌ์ด์ ์ฌ์ฉํ ์ต์ ๊ฐ๊ฒฉ // ์ ์ด ์์์ง๋ฉด ๋ ๋ฉ๋ฆฌ ์๊ฒ ๋ณด์ด๊ธฐ ๋๋ฌธ์ ๋ถ์ฌ์ฃผ๊ธฐ ์ํด์ ์ฌ์ฉ firstTime = true // ํ๋ฒ ์ค์ ์ ํ์ผ๋ฉด ๋ค์ ์ ์ธ๋์ง ์๊ธฐ ์ํด ๋ฐ๊ฟ์ค๋ค } override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { // ๋ ์ด์์ ๋ณ๊ฒฝ์ด ํ์ํ์ง ๋ฌป๋ ํจ์ return true } }
- CGAffineTransform๋ฅผ ์ด์ฉํด 2D ๊ทธ๋ํฝ์ ๊ทธ๋ ค ์ ๋๋ฉ์ด์ ์ ํ๋ฉด์ ๋ณด์ฌ์ค๋๋ค. ๊ฐ์ด๋ฐ ์๋ Cell์ ๊ธฐ์ค์ผ๋ก ์ ์์ Cell์ ๊ฐ์ด๋ฐ Cell๋ณด๋ค ์์์ก๋ค๊ฐ ๊ฐ์ด๋ฐ๋ก ๋๋ฌํ์ ๋, scale์์ identify๋ก ์ปค์ง๋ ์ ๋๋ฉ์ด์ ์ ์ฃผ์์ต๋๋ค.
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { // ๋ ์ด์์ ์์๋ฅผ ๊ฐ์ ธ์์ ์กฐ์ ํ๋ ํจ์ let superAttributes = super.layoutAttributesForElements(in: rect) superAttributes?.forEach { attributes in guard let collectionView = self.collectionView else { return } let collectionViewCenter = collectionView.frame.size.width / 2 // collectionVIewCenter - ์ปฌ๋ ์ ๋ทฐ์ ์ค์๊ฐ์ผ๋ก ๋ณํ์ง ์๋ ๊ณ ์ ๊ฐ let offsetX = collectionView.contentOffset.x // offsetX - ์ฌ์ฉ์๊ฐ ์คํฌ๋กคํ ๋ ๊ธฐ์ค์ ์ผ๋ก๋ถํฐ ์ด๋ํ ๊ฑฐ๋ฆฌ(x์ถ) let center = attributes.center.x - offsetX // center - ๊ฐ ์ ๋ค์ ์ค์๊ฐ // ๊ธฐ๋ณธ center๊ฐ์ ์ฒ์์ collectionView๊ฐ ๋ก๋๋ ๋ ๊ฐ์ด๋ฏ๋ก ์ฌ๊ธฐ์ offsetX ๋นผ์ค์ ๋์ ์ผ๋ก ๊ณ์ฐํ๋ค let maxDistance = self.itemSize.width + self.minimumLineSpacing // maxDistance - ์์ดํ ์ค์๊ณผ ์์ดํ ์ค์ ์ฌ์ด์ ๊ฑฐ๋ฆฌ let dis = min(abs(collectionViewCenter-center), maxDistance) // ํ์ฌ CollectionView์ ๊ฐ์ด๋ฐ์์ cell์ ๊ฐ์ด๋ฐ ๊ฐ์ ๋นผ์ ๊ฐ์ด๋ฐ 0์ ๊ธฐ์ค์ผ๋ก 1๊น์ง ๊ณ์ฐํ๊ธฐ ์ํด ๊ณ์ฐํ๋ ๊ฐ let ratio = (maxDistance - dis)/maxDistance // ๋น์จ์ ๊ตฌํด์ ์ ๋๋ฉ์ด์ ์ ์ฃผ๊ธฐ ์ํ ๊ฐ let scale = ratio * (1-0.7) + 0.7 attributes.transform = CGAffineTransform(scaleX: scale, y: scale) // scale์์ identify๋ก ์ปค์ง๋ ์ ๋๋ฉ์ด์ ์ ์ค๋ค } return superAttributes }
ํ๋ฒ๋ ํด๋ณด์ง๋ ์์์ง๋ง, ์ธ์ ๋ iOS ์ฃผ๋์ด ๊ฐ๋ฐ์๋ก์ ๋์ ํด๋ณด๊ณ ์ถ์๋ ์์ฒด animation ๊ตฌํ์ ๋์ ํด๋ณด์์ต๋๋ค.
-
๋ถํ๋ฐ์ ์ ๋๋ฉ์ด์ ์ ๋ํ ์ค๋ช
๋จผ์ ์ด ๋์์ธ์ ๋์์ด๋๋ถ์ด ์ ์ํด์ฃผ์ ์์คํ ์์ด๋์ด์์ต๋๋ค. ๋ก๊ทธ์ธ ๋ฒํผ์ ๋๋ ์ ๋, ์์ฐ์ค๋ฝ๊ฒ ์์ด๋, ๋น๋ฐ๋ฒํธ ์์ฑ๋์ด ์ฒ์ฒํ ์ฌ๋ผ์ค๋ ๋ฐฉ์์ผ๋ก ํ๋ฉด์ ๊ทธ๋ ค์ง๋ ์ ๋๋ฉ์ด์ ์ด์์ต๋๋ค.
-
์ ๋๋ฉ์ด์ ์ด ๋ค์ด๊ฐ ๋ถ๋ถ
์ด ์ ๋๋ฉ์ด์ ์ ์์ ์ ๋ก๊ทธ์ธ ๋ฒํผ์ด ๋๋ฌ์ง ์์ ๋ถํฐ ์ ๋๋ค. ๋ฐ๋ผ์@IBAction์ ๋ก๊ทธ์ธ๋ฒํผ์ ์ค์ ํด๋๊ณ , ๊ทธIBAction๋ด๋ถ์์ ์ ๋๋ฉ์ด์ ์ ์ฉ์ ํ์์ต๋๋ค. -
์ ๋๋ฉ์ด์ ์ฝ๋
UIView.animate(withDuration: 1, delay: 0, options: UIView.AnimationOptions.transitionFlipFromTop, animations: { /* codes */ }, completion: { finished in /* codes */ })
ํํ ์ฌ์ฉํ๋
UIView.animate()๋ฅผ ์ด์ฉํ์์ต๋๋ค.๋จ์ํ ์์ฐ์ค๋ฝ๊ฒ ๋ํ๋๋ ์ ๋๋ฉ์ด์ ์
alpha๊ฐ ์ฆ, ํฌ๋ช ๋๋ฅผ ์ด์ฉํ์ต๋๋ค.//๋ค๋ก ๊ฐ๊ธฐ ๋ฒํผ ๋ํ๋๊ธฐ self.backBtn.alpha = 1 self.backBtn.isHidden = false
์์๋๋ก ์์ง์ด๋ ์ ๋๋ฉ์ด์ ์ ๊ฒฝ์ฐ์๋
.center.y์ถ์ ์ด์ฉํ์ต๋๋ค.//ํ์๊ฐ์ ๋ฒํผ ์๋๋ก ๋ด๋ ค๊ฐ๊ธฐ self.signUpBtn.center.y += self.view.bounds.height
-
์ด๊ธฐ ์์น ์ค์
์๋์์ ์ ๋ก ์์ง์ฌ์ผ ํ๋ ์ ๋๋ฉ์ด์ ์ด์๊ธฐ ๋๋ฌธ์ ์ฒ์๋ถํฐ autolayout์ 200 ๋งํผ ์๋๋ก ์์น๋ฅผ ์ก์์ต๋๋ค. ๊ทธ๋ฆฌ๊ณ ๋ฒํผ์ด ๋๋ฌ์ก์ ๋ ์ ๋๋ฉ์ด์ ์ฝ๋๋ฅผ ํตํด ๋ค์ 200๋งํผ ์ฌ๋ผ์ค๋๋ก ํด์ฃผ์์ต๋๋ค. -
์กฐ๊ฑด๋ฌธ ์ค์
ํ๊ฐ์ง ์์ธ์ฒ๋ฆฌ๋ฅผ ํด์ฃผ์ด์ผ ํ์ต๋๋ค. ๋ก๊ทธ์ธ ๋ฒํผ์ ์ฒ์์ผ๋ก ๋๋ฌ ๋ค์ด์ค๋ฉด์ ์ ๋๋ฉ์ด์ ์ด ์๋๋๊ณ , ๊ทธ ๋ค์๋ถํฐ๋ ๋ฒํผ์ ๋๋ฌ๋ ์ ๋๋ฉ์ด์ ์ด ์๋ํ๋ฉด ์๋์์ต๋๋ค. (๊ทธ๋ ๊ฒ ๋๋ฉด ๋ก๊ทธ์ธ ๋ฒํผ์ ๋๋ฅผ ๋๋ง๋ค ์์ด๋ ๋น๋ฐ๋ฒํธ ๋์ด 200์ฉ ์๋ก ์ฌ๋ผ๊ฐํ ๋๊น์..) ๊ทธ๋์loginBtnFirstPressed: Bool์ ํ๋ ์ ์ธํด์ฃผ์ด์ ๋ก๊ทธ์ธ ๋ฒํผ์ด ์ฒ์์ผ๋ก ๋๋ฆด ๋true์ฒ๋ฆฌ๋ฅผ ํด์ฃผ๊ณ , ๊ทธ ๋ค์๋ถํฐ๋ ์๋ฒํต์ ์ด ๋๊ณ ์ ๋๋ฉ์ด์ ์ ์๋์ด ์๋๋๋ก ์ฒ๋ฆฌํด์ฃผ์์ต๋๋ค. -
๋ค๋ก ๋์๊ฐ๋ ๋ฒํผ์ ๋๋ ์ ๋
๋ก๊ทธ์ธ ๋ฒํผ์ ๋๋ฌ ์์ด๋ ๋น๋ฐ๋ฒํธ๋ฅผ ์น๋ค๊ฐ, ๋ค๋ก ๋์๊ฐ๋ ๊ฒฝ์ฐ๊ฐ ์์ต๋๋ค. ์ด ๊ฒฝ์ฐ์๋ ๋๊ฐ์ด ์ ๋๋ฉ์ด์ ์๋์ ๋ฃ์ด์ฃผ์ด์ ๋ค์ ๋ด๋ ค๊ฐ๋ ์ ๋๋ฉ์ด์ ์ ์ ์ฉํด์ฃผ์์ต๋๋ค.
UIRefreshControl์ ํ ์ด๋ธ ๋ทฐ๋ฅผ ์๋ ๋ฐฉํฅ์ผ๋ก ์ฌ๋ผ์ด๋ ํด์ ํ๋ฉด์ ๊ฐฑ์ ํ๋ ๊ธฐ๋ฅ์ผ๋ก, ํ๋ฉด์ ์๋ก ๊ณ ์นจ ํ ๋ ๋ง์ด ์ฌ์ฉ๋ฉ๋๋ค.
์ฐ์ ์ฌ์ฉํ๊ณ ์ ํ๋ ๋ทฐ์ปจํธ๋กค๋ฌ์ ๋ค์๊ณผ ๊ฐ์ ๊ตฌ๋ฌธ์ ์ ์ธํด์ค๋๋ค!
lazy var refreshControl: UIRefreshControl = { // Add the refresh control to your UIScrollView object. let refreshControl = UIRefreshControl() refreshControl.addTarget(self, action: #selector(handleRefresh(_:)), for: UIControl.Event.valueChanged) refreshControl.tintColor = UIColor.meaningNavy return refreshControl }()
refreshControl ์์ฑ์ UIRefreshControl๋ฅผ ํ ๋นํฉ๋๋ค. ์๋ก ๊ณ ์นจ ์ค์ผ ๋ ๋์ํ ๋ฉ์๋๋ฅผ addTarget๋ฅผ ์ด์ฉํด์ ์ฐ๊ฒฐํด์ค๋๋ค.
๊ทธ๋ฆฌ๊ณ ๋ทฐ์ ์ถ๊ฐ๋ฅผ ์์ผ์ค๋๋ค. ํ ์ด๋ธ ๋ทฐ๋ฅผ ๋ด๋ฆฌ๋ฉด ๋ฆฌ๋ก๋ ์ํค๊ณ ์ถ์ด์ ํ ์ด๋ธ ๋ทฐ์ ์ถ๊ฐ๋ฅผ ํด์ฃผ์์ต๋๋ค.
groupTableView.addSubview(self.refreshControl)
์๋์์ ํจ์๋ refreshControl ์ ์ธ์ ํ๊ฒ ์ก์ ์ ๊ฑธ์ด์ค ํจ์์ด๊ธฐ ๋๋ฌธ์ , ํ๋ฉด์ ๋น๊ฒจ์ ๋ด๋ฆด ๋๋ง๋ค ํจ์๊ฐ ์คํ๋ฉ๋๋ค. ๋ฐ๋ผ์, handleRefresh ํจ์ ์์ ํ๊ณ ์ ํ๋ ์ก์ ์ ์ถ๊ฐํ๋ฉด ์ฝ๊ฒ ๊ตฌํํ ์ ์์ต๋๋ค.
UIRefreshControl ๊ฐ์ฒด๋ beginRefreshing() ๋ฉ์๋๋ฅผ ํตํด ์คํ์ด ์์๋๊ณ endRefreshing() ๋ฉ์๋๋ฅผ ํตํด ์ข ๋ฃ๋ฉ๋๋ค. ํ๋ฉด ๋น๊น์ด ์๊ณ์ ์ ๋๊ฒ ๋๋ฉด, ์๋์ผ๋ก beginRefreshing() ๋ฉ์๋๋ ํธ์ถ๋ฉ๋๋ค.
๋ฐ๋ผ์ ์๋ก ๊ณ ์นจ์ด ์๋ฃ๋๋ฉด endRefreshing()๋ง ํธ์ถํด ์ฃผ๋ฉด ๋ฉ๋๋ค. (endRefreshing() ๋ฉ์๋๋ฅผ ํธ์ถํ์ง ์์ผ๋ฉด ์๋ก ๊ณ ์นจ ์ปจํธ๋กค์ด ๋ฉ์ถ์ง ์๊ฒ ๋ฉ๋๋ค.)
//์๋ก๊ณ ์นจ ํจ์ @objc func handleRefresh(_ refreshControl: UIRefreshControl) { //์๋ก๊ณ ์นจ ์ ๊ฐฑ์ ๋์ด์ผ ํ ๋ด์ฉ groupList(token: UserDefaults.standard.string(forKey: "accesstoken")!) checkMyGroup(UserDefaults.standard.string(forKey: "accesstoken")!) //๋น๊ฒจ์ ์๋ก๊ณ ์นจ ์ข ๋ฃ refreshControl.endRefreshing() }
UITabBarController์ ๊ฐ์ด๋ฐ ์นด๋ฉ๋ผ ๋ฒํผ์ ์ฝ๋๋ก ๋ง๋ค์ด์ addSubView ํ๋ ๋ฐฉ์์ผ๋ก ๋ง๋ค์ด์คฌ์ต๋๋ค.
var cameraButton: UIButton = { //๋ฒํผ์ ๊ฐ์ฒด ์์ฑ let button = UIButton() //๋ฒํผ์ ์ด๋ฏธ์ง๋ฅผ ๋ฃ์ด์ค๋๋ค. button.setBackgroundImage(UIImage(named:"navItemCamera"), for: .normal) //์์ฑํ ๋ฒํผ์ ์ด๋ฒคํธ๋ฅผ ์ง์ ํด์ค๋๋ค. button.addTarget(self, action: #selector(TabBarVC.buttonClicked(sender:)), for: .touchUpInside) return button }()
ํญ๋ฐ ์ปจํธ๋กค๋ฌ์ ๊ธฐ๋ณธ ์ค์ ๋๋ก ํ๊ฒ ๋๋ฉด, ํญ๋ฐ ์์ด์ฝ๋ค์ด ์ผ์ชฝ์ ์ฌ์ง๊ณผ ๊ฐ์ด ๊ฐ์ด๋ฐ๋ก ์ ๋ ค๋ณด์ธ๋ค๋ ๊ฒ์ ๋ณผ ์ ์๋ค!
๋ฐ๋ผ์ UIEdgeInsets๋ก ์ด๋ฏธ์ง์ ์ธ์ ์ ์กฐ์ ํด์ค๋๋ค.
func setTabBar() { //ํญ๋ฐ ์ค์ let homeStoryboard = UIStoryboard.init(name: "Home", bundle: nil) guard let homeVC = homeStoryboard.instantiateViewController(identifier: "HomeNavigationController") as? HomeNavigationController else { return } let groupStoryboard = UIStoryboard.init(name: "GroupList", bundle: nil) guard let groupVC = groupStoryboard.instantiateViewController(identifier: "GroupListNavigationController") as? GroupListNavigationController else { return } //ํญ๋ฐ ์์ดํ ์ด๋ฏธ์ง ์ธ์ ์กฐ์ homeVC.tabBarItem.imageInsets = UIEdgeInsets(top: 0, left: -20, bottom: -5, right: 0) homeVC.tabBarItem.image = UIImage(named: "tabBarHomeIcInactive") homeVC.tabBarItem.selectedImage = UIImage(named: "tabBarHomeIcActive") homeVC.title = "" groupVC.tabBarItem.imageInsets = UIEdgeInsets(top: 0, left: 0, bottom: -5, right: -20) groupVC.tabBarItem.image = UIImage(named: "tabBarGroupIcInactive") groupVC.tabBarItem.selectedImage = UIImage(named: "tabBarGroupIcActive") groupVC.title = "" setViewControllers([homeVC, groupVC], animated: true) }
์นด๋ฉ๋ผ ๋ฒํผ์ ํฌ๊ธฐ์ ์์น๋ฅผ ์ ํด์ฃผ๊ณ , ํญ๋ฐ์ addSubView ํด์ค๋๋ค.
func setTabBar() { //์นด๋ฉ๋ผ ๋ฒํผ์ ํฌ๊ธฐ์ ์์น๋ฅผ ์ ์ ํด์ค๋๋ค. let width: CGFloat = 70/375 * self.view.frame.width let height: CGFloat = 70/375 * self.view.frame.width let posX: CGFloat = self.view.frame.width/2 - width/2 let posY: CGFloat = -32 cameraButton.frame = CGRect(x: posX, y: posY, width: width, height: height) //๋ง๋ค์ด์ค ์นด๋ฉ๋ผ ๋ฒํผ์ ํญ๋ฐ์ ์ถ๊ฐํด์ค๋๋ค. tabBar.addSubview(self.cameraButton) }
textField์ ์ ๋ ฅ๋ ๊ฐ์ด ์กด์ฌํ๊ฑฐ๋ ์ฌ๋ฐ๋ฅด์ง ์์ ๊ฒฝ์ฐ, ๋ฏธ์ ์ํ ์์๊ฐ ์ฌ๋ฐ๋ฅด์ง ๋ชป ํ ๊ฒฝ์ฐ, ์ฌ์ฉ์์๊ฒ ์๋ฆผ์ ์ฃผ๋ ํ ์คํธ ํ์ ์ extension ์ผ๋ก ๋ง๋ค์ด์ ์ฌ์ฉํ์ต๋๋ค.
// MARK: Toast Alert Extension // ์ฌ์ฉ๋ฒ: showToast(message : "์ํ๋ ๋ฉ์ธ์ง ๋ด์ฉ", font: UIFont.spoqaRegular(size: 15), width: 188, bottomY: 181) func showToast(message : String, font: UIFont, width: Int, bottomY: Int) { let guide = view.safeAreaInsets.bottom let y = self.view.frame.size.height-guide //ํ ์คํธ ๋ผ๋ฒจ์ ํฌ๊ธฐ์ ์์น๋ฅผ ์ ์ ํด์ค๋๋ค. let toastLabel = UILabel( frame: CGRect( x: self.view.frame.size.width/2 - CGFloat(width)/2, y: y-CGFloat(bottomY), width: CGFloat(width), height: 30 ) ) toastLabel.backgroundColor = UIColor.gray4 toastLabel.textColor = UIColor.gray6 toastLabel.font = font toastLabel.textAlignment = .center toastLabel.text = message toastLabel.alpha = 1.0 toastLabel.layer.cornerRadius = 6 toastLabel.clipsToBounds = true //๋ทฐ์ ํ ์คํธ ๋ผ๋ฒจ์ ์ถ๊ฐ์์ผ์ค๋๋ค. self.view.addSubview(toastLabel) //์ ๋๋ฉ์ด์ ์ ์ค์ ํด์ค๋๋ค. UIView.animate(withDuration: 3.0, delay: 0.1, options: .curveEaseOut, animations: { toastLabel.alpha = 0.0 }, completion: {(isCompleted) in toastLabel.removeFromSuperview() }) }
ํ์ฌ UIView์ ์ ๋๋ฉ์ด์ ์ต์ ์ curveEaseOut ์ผ๋ก ์ค์ ํด๋๋๋ฐ, ์ด๋ ๋น ๋ฅด๊ฒ ์งํ๋ฌ๋ค๊ฐ ์๋ฃ๋ฌ์๋ ์ฒ์ฒํ ์งํ๋๋ ์ ๋๋ฉ์ด์ ํจ๊ณผ์ ๋๋ค.
์ด์ ๊ฐ์ด ์ ๋๋ฉ์ด์ ์ ์ค์ ํ ์ ์๋ ์ต์ ์ผ๋ก๋ curveEaseInOut, curveEaseIn, curveEaseOut ๊ฐ ์์ต๋๋ค.
static var curveEaseInOut: UIView.AnimationOptions
- ๊ธฐ๋ณธ๊ฐ
- ์ฒ์ฒํ ์งํ๋ฌ๋ค๊ฐ duration์ ์ค๊ฐ์ฏค์ ๋นจ๋ผ์ง๊ณ , ์๋ฃ๋๊ธฐ ์ ์ ๋ค์ ์ฒ์ฒํ ์งํ๋๋ ์ต์
static var curveEaseIn: UIView.AnimationOptions
- ์ ๋๋ฉ์ด์ ์ด ๋๋ฆฌ๊ฒ ์์๋ ๋ค์, ์งํ์ ๋ฐ๋ผ ์ ๋๋ฉ์ด์ ์๋๊ฐ ๋นจ๋ผ์ง.
static var curveEaseOut: UIView.AnimationOptions
- ์ ๋๋ฉ์ด์ ์ด ๋น ๋ฅด๊ฒ ์์๋๊ณ ์๋ฃ ๋ ์ฏค ๋๋ ค์ง.
๋ง์ดํผ๋์ ๊ทธ๋ฃนํผ๋์ ๊ฒ์๋ฌผ ์์ฑ ์๊ฐ์ด ํ์ฌ๋ก๋ถํฐ ์ผ๋ง ์ ์ธ์ง ํ์ํด์ฃผ๋ extension์ ๋๋ค.
์ฌ์ฉ๋ฐฉ๋ฒ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
var createTime = "2021ๅนด01ๆ13ๆฅ 14:00:00" createTime.StringToDate().timeAgoSince() // 1. createTime์ StringToDate๋ฅผ ํตํด Stringํ์ ์์ Date ํ์ ์ผ๋ก ๋ฐ๊ฟ์ค // 2. timeAgoSince๋ฅผ ํตํด ์ด ์๊ฐ์ด ํ์ฌ ์๊ฐ์ ๊ธฐ์ค์ผ๋ก ์ผ๋ง์ ์ธ์ง ๊ตฌํด์ฃผ๊ธฐ
์์ธํ ์์๋ณด๊ธฐ ์ด์ ์, ๋ ์ง ๊ณ์ฐ์ ํ์ํ NSCalendar ์ ๋ํด ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค!
์ฝ๊ฒ ๋งํด์ NSCalendar ๊ฐ์ฒด๋ ์ค์ง์ ์ธ ๋ ์ง ๊ณ์ฐ์ ์ํํ๋ ํด๋์ค์
๋๋ค.
๋ฌ๋ ฅ์ ์ด์ฉํด์ ํน์ ์์ ์ ๋ ์ง ๋จ์๋ก ๋ณ๊ฒฝํ๋ฉด ์ด ๋ ์ง๋ ์ฌ๋ฌ ๊ตฌ์ฑ ์์๋ก ๋๋์ด ๋
, ์, ์ผ, ์์ผ, ๋ช ์งธ ์ฃผ์ธ์ง ๋ฑ์ ์ ๋ณด๊ฐ ๋์ค๊ฒ ๋ฉ๋๋ค. ์ด๋ฌํ ์ ๋ณด๋ฅผ ๋ชจ์์ ํ์ํ ์ ์๋๋ก ํด์ฃผ๋ ๊ฐ์ฒด๊ฐ components ์
๋๋ค.
๋ ์ง ๊ตฌ์ฑ ์์๋ก ์ง์ ๋ ์์ ๋ ์ง์ ์ข
๋ฃ ๋ ์ง์ ์ฐจ์ด๋ฅผ ๋ฐํํ๋ components ๊ด๋ จ ๋ฉ์๋๋ฅผ ์์๋ณด๊ฒ ์ต๋๋ค.
func components(_ unitFlags: NSCalendar.Unit, from startingDateComp: DateComponents, to resultDateComp: DateComponents, options: NSCalendar.Options = []) -> DateComponents
๊ฐ ํ๋ผ๋ฏธํฐ๋ฅผ ์ดํด๋ณด๋ฉด, ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
unitFlags : ๋ฐํ ๋ NSDateComponents ๊ฐ์ฒด์ ๊ตฌ์ฑ ์์๋ฅผ ์ง์ ํฉ๋๋ค.
startingDateComp : NSDateComponents ๊ฐ์ฒด๋ก ๊ณ์ฐ์ ์์ ๋ ์ง์
๋๋ค.
resultDateComp : NSDateComponents ๊ฐ์ฒด๋ก ๊ณ์ฐ์ ์ข
๋ฃ ๋ ์ง์
๋๋ค.
option : ์ต์
๋งค๊ฐ ๋ณ์๋ ํ์ฌ ์ฌ์ฉ๋์ง ์์ต๋๋ค.
์ด๋ฌํ components ๋ฉ์๋๋ฅผ ๋ฐํ์ผ๋ก ๊ฒ์๋ฌผ์ ์์ฑ ์๊ฐ์ด ํ์ฌ๋ณด๋ค ์ผ๋ง ์ ์ธ์ง ๊ณ์ฐํ ์ ์์ต๋๋ค.
func timeAgoSince() -> String { //์ ์ ์ ์บ๋ฆฐ๋์์ ํ์ฌ์์ ์ ๊ฐ์ ธ์ต๋๋ค. let calendar = Calendar.current //date๋ฅผ string์ผ๋ก ๋ฐ๊พธ๊ณ , stringํ์ ์ dateํ์ ์ผ๋ก ๋ฐ๊ฟ์ค๋๋ค. let now = Date().datePickerToString().stringToDate() //์ฐ๋, ์, ์ผ ๋ฐ ์๊ฐ๊ณผ ๊ฐ์ ๋ฌ๋ ฅ ๋จ์๋ฅผ ์๋ณํด์ ๋ฃ์ด์ค๋๋ค. let unitFlags: NSCalendar.Unit = [.second, .minute, .hour, .day, .weekOfYear, .month, .year] //๊ฒ์๋ฌผ ์์ฑ๋ ์ง์ ํ์ฌ ๋ ์ง์ ์ฐจ์ด๋ฅผ ๋ ์ง ๊ตฌ์ฑ ์์๋ก ๋ฐํํฉ๋๋ค. let components = (calendar as NSCalendar).components(unitFlags, from: self, to: now, options: []) if let year = components.year, year >= 1 { return "\(year)๋ ์ " } if let month = components.month, month >= 1 { return "\(month)๋ฌ ์ " } if let week = components.weekOfYear, week >= 1 { return "\(week)์ฃผ ์ " } if let day = components.day, day >= 1 { return "\(day)์ผ ์ " } if let hour = components.hour, hour >= 1 { return "\(hour)์๊ฐ ์ " } if let minute = components.minute, minute >= 1 { return "\(minute)๋ถ ์ " } if let second = components.second, second >= 3 { return "\(second)์ด ์ " } return "์ง๊ธ" }
"๋ฏธ๋์ iOS ๊ฐ๋ฐ์๋ค์ ์ฝ๋๋ฆฌ๋ทฐ์ ํจ์จ์ ์ธ ํ์ ์ผ๋ก ํจ๊ป ์ฑํ๋ ์ฑ๊ฐ๋ฐ์ ์งํฅํฉ๋๋ค."
| ๋ฏผํฌ | ๋ฏผ์น | ์ธ์ |
|---|---|---|
| contact : xwoud@naver.com github: xwoud |
contact : seonminseung@naver.com github: MinseungSeon |
contact : hotpigtomato@gmail.com github: pk33n |
| ํ์์คํฌํ, ํ ํ๋ฉด ๋ด๋น | ์คํ๋์ ๋ฐ ๋ก๊ทธ์ธ, ๋ฏธ์ ํ๋ฉด ๋ด๋น | ๊ทธ๋ฃน ๋ฐ ์ปค์คํ ํญ๋ฐ ๋ด๋น |