nneaning / meaning_iOS

๐ŸŽ๋ฏธ๋‹์–ธ์ฆˆ๋“ค์€ iOS๊ฐ€ ์ข‹iOS๐Ÿ

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool


๋ฏธ๋ผํด ๋ชจ๋‹์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋‹น์‹ ์˜ ์˜๋ฏธ์žˆ๋Š” ์•„์นจ, meaning iOS


๐ŸŒฑ ์„œ๋น„์Šค ์†Œ๊ฐœ

ํ”„๋กœ์ ํŠธ ์ง„ํ–‰๊ธฐ๊ฐ„ : 2020๋…„ 12์›” 26์ผ ~ 2021๋…„ 01์›” 15์ผ

๋ชจ๋“  ๊ฒƒ์€ ๋ฐ”๋€” ์ˆ˜ ์žˆ๊ณ  ๋‚˜ ์—ญ์‹œ ๋ฌด์–ธ๊ฐ€๋ฅผ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ์ƒ ์‹œ๊ฐ„์ด ๋‹ฌ๋ผ์ง„๋‹ค๋ฉด, ๋‹น์‹ ๋„ ๋ณ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


'๋‚ด'๊ฐ€ ๋ˆˆ ๋œจ๋Š” ์‹œ๊ฐ„์ด ์•„๋‹Œ, 'ํ•ด'๊ฐ€ ๋œจ๋Š” ์‹œ๊ฐ„๋ถ€ํ„ฐ ํ•˜๋ฃจ๋ฅผ ์‹œ์ž‘ํ•˜๋Š” ๋ฏธ๋ผํด ๋ชจ๋‹.

๋ฏธ๋‹์„ ํ†ตํ•ด ๋ฏธ๋ผํด ๋ชจ๋‹์— ๋„์ „ํ•˜๋ฉฐ ๋‹น์‹ ๋งŒ์˜ ์˜๋ฏธ์žˆ๋Š” ์•„์นจ์„ ๋งŒ๋“ค์–ด ๋‚˜๊ฐ€๋ณด์„ธ์š”.

์ผ์ฐ ์ผ์–ด๋‚˜๋Š” ์Šต๊ด€์œผ๋กœ ํ•˜๋ฃจ๋ฅผ ๊ธธ๊ฒŒ ๋ณด๋‚ด๋ฉด, ์„ฑ์žฅ์˜ ๋ฐœํŒ์„ ๋งˆ๋ จํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฏธ๋‹๊ณผ ํ•จ๊ป˜ ์ฒด๊ณ„์ ์ธ ๊ณ„ํš์„ ์„ธ์šฐ๊ณ  ์ด๋ฅผ ๊ทœ์น™์ ์œผ๋กœ ์‹ค์ฒœํ•˜๋ฉด์„œ ์„ฑ์ทจ๊ฐ์„ ์–ป์–ด๋ณด์„ธ์š”.

์„ฑ์žฅ์ง€ํ–ฅ์ ์ธ ๊ทธ๋ฃน์›๋“ค๊ณผ ๋ชฉํ‘œ๋ฅผ ๊ณต์œ ํ•œ๋‹ค๋ฉด ์šฐ๋ฆฌ๋Š” ํ•จ๊ป˜, ๋” ๋ฉ€๋ฆฌ ๊ฐˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.



SETTING


๐Ÿ• ๊ฐœ๋ฐœ ํ™˜๊ฒฝ

Swift 4 Xcode swift iOS COCOAPODS


โž• ์‚ฌ์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

Moya Alamofire Kingfisher lottie-ios


๐Ÿ“ Coding Convention

meaning Coding Convention


๐Ÿฑ How We Use Git

How We Use Git


๐Ÿ—‚ Foldering

๐Ÿ’ป 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

๐Ÿ“ฑ Screen ๋‹จ์œ„

  • TapBar : ์ปค์Šคํ…€ํƒญ๋ฐ”
  • Login : ์Šคํ”Œ๋ž˜์‰ฌ
  • Login : ๋กœ๊ทธ์ธ
  • Onboarding : ๋‹‰๋„ค์ž„ ๋ฐ ๊ธฐ์ƒ์‹œ๊ฐ„ ์ž…๋ ฅ
  • Home : ํ™ˆ, ์บ˜๋ฆฐ๋” ํ™”๋ฉด
  • Camera : ํƒ€์ž„์Šคํƒฌํ”„
  • Mission : ๋ฏธ์…˜์นด๋“œ
  • MyPage : ๋งˆ์ดํŽ˜์ด์ง€
  • GroupList : ๊ทธ๋ฃนํƒญ(๊ทธ๋ฃน ๋ชฉ๋ก + ๊ทธ๋ฃน ์ƒ์„ฑ)
  • GroupFeed : ๊ทธ๋ฃนSNS(๊ทธ๋ฃน ๊ธ€ ๋ชฉ๋ก + ๊ธ€ ์ž์„ธํžˆ๋ณด๊ธฐ + ๊ทธ๋ฃน ์„ค์ •)


Service

๐Ÿ—“ WORKFLOW


๐Ÿ‘ท ์‹คํ–‰ํ™”๋ฉด

- ๊ธฐ์ค€ iPhone : ์•„์ดํฐse2, ์•„์ดํฐ12mini, ์•„์ดํฐ12Pro
- ํ…Œ์ŠคํŠธ ๊ณ„์ • : ์•„์ด๋”” - iOS@meaning.com / ๋น„๋ฐ€๋ฒˆํ˜ธ - iosmeaning


๐Ÿ“ฑ Splash, Login ํ™”๋ฉด


ํ™”๋ฉด

Splash


์Šคํ”Œ๋ž˜์‹œ ํ™”๋ฉด์„ lottie-ios ๋ฅผ ํ†ตํ•ด ์ ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.
ํ•œ Loop ๊ฐ€ ์žฌ์ƒ๋˜๊ณ  ๋กœ๊ทธ์ธ ํ™”๋ฉด์œผ๋กœ ๋„˜์–ด๊ฐ‘๋‹ˆ๋‹ค.
splash ์—์„œ๋Š” ํ† ํฐ์˜ ์„ธ์…˜์ด ๋งŒ๋ฃŒ๋˜์—ˆ๋Š”์ง€์— ๋Œ€ํ•ด ์„œ๋ฒ„ํ†ต์‹ ์„ ํ†ตํ•ด ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
๋งŒ๋ฃŒ๊ฐ€ ๋˜์—ˆ๊ฑฐ๋‚˜ ํ† ํฐ์ด ์—†๋‹ค๋ฉด ๋กœ๊ทธ์ธ ํ™”๋ฉด์œผ๋กœ, ์œ ํšจํ•œ ํ† ํฐ์„ ์†Œ์œ ํ•œ ์œ ์ €๋ผ๋ฉด ๋ฐ”๋กœ ํ™ˆํ™”๋ฉด์œผ๋กœ ๋„˜์–ด๊ฐ‘๋‹ˆ๋‹ค.

Login


๋” ๋‚˜์€ ๋ ˆ์ด์•„์›ƒ์„ ์œ„ํ•ด ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ํ•จ๊ป˜ ์•„์ด๋””, ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ž€์ด ๋ณด์—ฌ์ง‘๋‹ˆ๋‹ค.
์•„์ด๋”” ํ˜น์€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ ์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ์˜ณ์ง€ ์•Š์€ ๊ฐ’์ด ๋“ค์–ด๊ฐ„๋‹ค๋ฉด ๋นจ๊ฐ„ ๊ฒฝ๊ณ  ๊ธ€์”จ๊ฐ€ ๋„์›Œ์ง‘๋‹ˆ๋‹ค.

์ƒ์„ธ ํ™”๋ฉด

๋กœ๊ทธ์ธ ์ง„์ž… ํ™”๋ฉด ๋กœ๊ทธ์ธ ํ™”๋ฉด ๊ฐ’ ์˜ค๋ฅ˜ ํ™”๋ฉด


๐Ÿ“ฑ ์˜จ๋ณด๋”ฉ ํ™”๋ฉด


ํ™”๋ฉด

OnBoarding


๋กœ๊ทธ์ธ ํ™”๋ฉด ์ดํ›„, ๋‹‰๋„ค์ž„๊ณผ ๊ธฐ์ƒ์‹œ๊ฐ„์„ ์ž…๋ ฅํ•˜์ง€ ์•Š์•˜๋˜ ์œ ์ €์—๊ฒŒ ์˜จ๋ณด๋”ฉ ํ™”๋ฉด์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.
๋ชฉํ‘œ ๊ธฐ์ƒ ์‹œ๊ฐ„์˜ ๊ฒฝ์šฐ์—๋Š” pickerview ๋ฅผ ํ†ตํ•ด ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ƒ์„ธ ํ™”๋ฉด

์ง„์ž… ํ™”๋ฉด ๋‹‰๋„ค์ž„ ์ž…๋ ฅ ํ™”๋ฉด
๊ธฐ์ƒ์‹œ๊ฐ„ ํ™”๋ฉด ๊ธฐ์ƒ์‹œ๊ฐ„ ์ž…๋ ฅ ํ›„ ํ™”๋ฉด

๐Ÿ“ฑ ํ™ˆ - ์บ˜๋ฆฐ๋” - ๋ฏธ์…˜ ํ™”๋ฉด


ํ™”๋ฉด

Home - Calendar - Mission


๋กœ๊ทธ์ธ์ด ์™„๋ฃŒ๋˜๋ฉด ํ™ˆํ™”๋ฉด์œผ๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค.
ํ™ˆํ™”๋ฉด์€ ์บ˜๋ฆฐ๋”์™€ ๋ฏธ์…˜์œผ๋กœ ์ด์–ด์ง€๋Š” ๊ตฌ๊ฐ„์ž…๋‹ˆ๋‹ค.
๋ฏธ์…˜์€ ์ขŒ์šฐ ์Šฌ๋ผ์ด๋“œ๊ฐ€ ๋˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€์œผ๋ฉฐ, ์บ˜๋ฆฐ๋”๋Š” ์ƒ๋‹จ ๋‚ ์งœ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ๋„˜์–ด๊ฐ€๋„๋ก ๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค.
์บ˜๋ฆฐ๋”๋Š” ํ•ด๋‹น ๋‹ฌ์— ์œ ์ €๊ฐ€ ๋ฏธ๋ผํด ๋ชจ๋‹์„ ์„ฑ๊ณตํ•œ ๋‚ ๋“ค์„ ๋ณ„๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.
์•„๋ž˜ ์ปค์Šคํ…€ ํƒญ๋ฐ”์˜ ์นด๋ฉ”๋ผ ๋ฒ„ํŠผ์€ ๊ธฐ์ƒ๋ฏธ์…˜ ์ˆ˜ํ–‰ ์™ธ์— ์ธ์ฆ ์นด๋ฉ”๋ผ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.


์ƒ์„ธ ํ™”๋ฉด

ํ™ˆ ํ™”๋ฉด ์บ˜๋ฆฐ๋” ํ™”๋ฉด

๐Ÿ“ฑ ํ™ˆ-์บ˜๋ฆฐ๋”-๋ฏธ์…˜์™„๋ฃŒ ํ›„ ํ™”๋ฉด


ํ™”๋ฉด

After Mission Completed


๋ฏธ์…˜์ด ์™„๋ฃŒ๋˜๋ฉด ์ˆœ์ฐจ์ ์œผ๋กœ ํ•ด๋‹น ๋ฏธ์…˜์ด ์™„๋ฃŒ๋˜์–ด ํ™ˆ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
ํ•œ๋ฒˆ ์™„๋ฃŒ๋œ ๋ฏธ์…˜์€ ๋‹ค์‹œ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

์ƒ์„ธ ํ™”๋ฉด

๋ฏธ์…˜ ์™„๋ฃŒ ํ™”๋ฉด ๋ฏธ์…˜ ์™„๋ฃŒ ํ›„ ๋ณ„์ด ์ฑ„์›Œ์ง„ ์บ˜๋ฆฐ๋”

๐Ÿ“ฑ ํƒ€์ž„์Šคํƒฌํ”„ ํ™”๋ฉด


ํ™”๋ฉด

TimeStamp


๋ฏธ๋‹ ์•ฑ์˜ ๋ฉ”์ธ ๊ธฐ๋Šฅ์ด๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ๋Š” 'ํƒ€์ž„์Šคํƒฌํ”„' ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค.
์นด๋ฉ”๋ผ๋ฅผ ํ‚ค๊ฒŒ ํ•˜์—ฌ ์นด๋ฉ”๋ผ ์œ„์— ํ˜„์žฌ ์‹œ๊ฐ„์ด ๋‚˜์™€์žˆ๋Š” ๋ทฐ๋ฅผ ์˜ฌ๋ ค ํ™”๋ฉด์„ ์บก์ณํ•œ ํ›„ ์ €์žฅํ•˜๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค.

์ƒ์„ธ ํ™”๋ฉด

ํƒ€์ž„์Šคํƒฌํ”„ ํ™”๋ฉด

๐Ÿ“ฑ 4๊ฐœ ๋ฏธ์…˜ ํ™”๋ฉด


ํ™”๋ฉด

Missions


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

์ƒ์„ธ ํ™”๋ฉด

ํ•˜๋ฃจ๋‹ค์ง ๋ฏธ์…˜ ํ™”๋ฉด ํšŒ๊ณ ์ผ๊ธฐ ๋ฏธ์…˜ ํ™”๋ฉด ํšŒ๊ณ ์ผ๊ธฐ ์ž‘์„ฑ ํ™”๋ฉด
์งง์€๋…์„œ ๋ฏธ์…˜ ํ™”๋ฉด ์งง์€๋…์„œ ์ž‘์„ฑ ํ™”๋ฉด
ํƒ€์ž„์Šคํƒฌํ”„ ๋ฏธ์…˜ ํ™”๋ฉด ํƒ€์ž„์Šคํƒฌํ”„ ์ž‘์„ฑ ํ™”๋ฉด

๐Ÿ“ฑ ๋งˆ์ดํ”ผ๋“œ - ๋งˆ์ดํ”ผ๋“œ ์ƒ์„ธ๋ณด๊ธฐ ํ™”๋ฉด


ํ™”๋ฉด

MyFeed


์ž์‹ ์˜ ๊ธฐ์ƒ๋ฏธ์…˜์—์„œ ์ฐ์€ ์‚ฌ์ง„๋“ค์ด ์—…๋กœ๋“œ ๋˜๋Š” ๊ฐœ์ธ๊ณต๊ฐ„์ž…๋‹ˆ๋‹ค.
ํ”ผ๋“œ๋กœ ํ™•์ธ์ด ๊ฐ€๋Šฅํ•˜๊ณ , ์ž์‹ ์ด ๋ช‡๋ฒˆ์งธ ๋ฏธ๋ผํด ๋ชจ๋‹์„ ํ–ˆ๋Š”์ง€ ๊ทธ๋ฆฌ๊ณ  ๊ฐ ํ”ผ๋“œ์˜ ์ƒ์„ธ ํŽ˜์ด์ง€๋„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ƒ์„ธ ํ™”๋ฉด

๋งˆ์ดํ”ผ๋“œ ํ™”๋ฉด ๋งˆ์ดํ”ผ๋“œ ์ƒ์„ธ๋ณด๊ธฐ ํ™”๋ฉด

๐Ÿ“ฑ ๊ทธ๋ฃน ๋ชฉ๋ก ํ™”๋ฉด


ํ™”๋ฉด

Group List


๋‹ค์–‘ํ•œ ๊ทธ๋ฃน์„ ๊ตฌ๊ฒฝํ•  ์ˆ˜ ์žˆ๋Š” ๋ชฉ๋ก ์ฐฝ ์ž…๋‹ˆ๋‹ค.
์ขŒ์šฐ collectionview ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋˜ ๊ทธ ์•„๋ž˜๋กœ๋Š” ํ…Œ์ด๋ธ”๋ทฐ๋กœ๋„ ์ •๋ณด๊ฐ€ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค.

์ƒ์„ธ ํ™”๋ฉด

๊ทธ๋ฃน ๋ชฉ๋ก ํ™”๋ฉด ์ฐธ๊ฐ€ ๊ทธ๋ฃน์ด ์—†์„ ๋•Œ

๐Ÿ“ฑ ๊ทธ๋ฃน ์ƒ์„ธ๋ณด๊ธฐ-๊ฐ€์ž… ํ™”๋ฉด


ํ™”๋ฉด

Joining Clubs


์ฐธ๊ฐ€ํ•˜๊ณ  ์‹ถ์€ ๊ทธ๋ฃน์„ ๋ˆ„๋ฅด๋ฉด ์ƒ์„ธ ์„ค๋ช…์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๊ทธ ์ดํ›„์— ์ฐธ๊ฐ€ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์ฐธ๊ฐ€๊ฐ€ ์™„๋ฃŒ๋˜๊ณ , ๊ทธ๋ฃน ํ™ˆ์—์„œ ์ž์‹ ์ด ์ฐธ๊ฐ€ํ•˜๋Š” ๊ทธ๋ฃน์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ๋”์ด์ƒ ํ™ˆ์—์„œ ์ž์‹ ์ด ์ฐธ๊ฐ€ํ•˜๊ณ  ์žˆ๋Š” ๊ทธ๋ฃน์ด ๋ณด์ด์ง€ ์•Š๊ฒŒ๋ฉ๋‹ˆ๋‹ค.

์ƒ์„ธ ํ™”๋ฉด

๊ทธ๋ฃน ์ƒ์„ธ๋ณด๊ธฐ ์ฐธ๊ฐ€๋ฒ„ํŠผ ๋ˆ„๋ฅธ ํ›„ ์ฐธ๊ฐ€ ํ›„ ๊ทธ๋ฃน ๋ชฉ๋ก
์ž์‹ ์ด ์ฐธ๊ฐ€ํ•œ ๊ทธ๋ฃน
๋”์ด์ƒ ๋ณด์ด์ง€ ์•Š์Œ

๐Ÿ“ฑ ๊ทธ๋ฃน์ƒ์„ฑ ํ™”๋ฉด


ํ™”๋ฉด

Create Group


์ž์‹ ์ด ์†ํ•ด์žˆ๋Š” ๊ทธ๋ฃน์ด ์—†์„ ๋•Œ,
๊ทธ๋ฃน์— ์ฐธ์—ฌํ•ด๋„ ๋˜์ง€๋งŒ, ๊ทธ๋ฃน์„ ์ง์ ‘ ์ƒ์„ฑํ•ด๋„ ๋ฉ๋‹ˆ๋‹ค.
๊ทธ๋ฃน์„ ๋งŒ๋“ค๊ณ  ๊ทธ๋ฃน์˜ ํ”ผ๋“œ๋ฅผ ํ™•์ธํ•˜๋ฉด ๊ฒŒ์‹œ๋ฌผ์ด ์—†๋‹ค๋Š” ๋ธ”๋žญํฌ ๋ทฐ๊ฐ€ ๋‚˜์˜ค๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์ƒ์„ธ ํ™”๋ฉด

๊ทธ๋ฃน ์ƒ์„ฑ ํ™”๋ฉด ๊ทธ๋ฃน ์ƒ์„ฑ ๋‚ด์šฉ ์ž‘์„ฑ ํ™”๋ฉด ์ƒ์„ฑ ์™„๋ฃŒ ํ™”๋ฉด

๐Ÿ“ฑ ๊ทธ๋ฃน ํ”ผ๋“œ-ํ”ผ๋“œ ์ƒ์„ธ๋ณด๊ธฐ-๊ทธ๋ฃน ์„ค์ • ํ™”๋ฉด


ํ™”๋ฉด

Group Feed


๊ทธ๋ฃน ํ”ผ๋“œ๋Š” ์ž์‹ ์ด ์ฐธ๊ฐ€ํ•˜๊ณ  ์žˆ๋Š” ๊ทธ๋ฃน์‚ฌ๋žŒ๋“ค์˜ ์ธ์ฆ ์‚ฌ์ง„๋“ค์„ ํ™•์ธํ•˜๋Š” ๊ณต๊ฐ„์ž…๋‹ˆ๋‹ค.
ํ”ผ๋“œ์—์„œ ์ƒ์„ธ๋ณด๊ธฐ๋กœ ์ด๋™๋„ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ๊ทธ๋ฃน์„ ์„ค์ •ํ•˜๋Š” ํŽ˜์ด์ง€์—์„œ ๊ทธ๋ฃน์— ๋Œ€ํ•œ ์ƒ์„ธ ๋‚ด์šฉ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ƒ์„ธ ํ™”๋ฉด

๊ทธ๋ฃน ํ”ผ๋“œ ๋น„์—ˆ์„ ๋•Œ ํ™”๋ฉด ๊ทธ๋ฃน ํ”ผ๋“œ ๋‚ด์šฉ ํ™”๋ฉด
์„ค์ • ํ™”๋ฉด ๊ทธ๋ฃน ํ”ผ๋“œ ์ƒ์„ธ๋ณด๊ธฐ ํ™”๋ฉด

๐Ÿ›  ๊ธฐ๋Šฅ๋ช…์„ธ์„œ

์šฐ์„ ์ˆœ์œ„ ๊ธฐ๋Šฅ๋ช… ์„ค๋ช… ๊ตฌํ˜„์—ฌ๋ถ€ ๋‹ด๋‹น์ž
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 ํŒ€์€ ๋์—†๋Š” ๋„์ „์„ ๋‘๋ ค์›Œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ ๊ฐ์ž ํ•ด๋ณด์ง€ ์•Š์•˜๋˜ ์ƒˆ๋กœ์šด ๊ธฐ์ˆ ๋“ค์„ ๋„์ „ํ•˜๊ณ  ๊ณต๋ถ€ํ•˜๋Š” ์‹œ๊ฐ„์„ ๊ฐ€์ ธ๋ณด์•˜์Šต๋‹ˆ๋‹ค.

๐Ÿ‘€ ๋ฏผํฌ

1. Moya๊ฐ€ Moya?

Moya ํ”„๋ ˆ์ž„ ์›Œํฌ ์ด์šฉํ•˜๊ธฐ

  • ์ถ”์ƒํ™” ๋„คํŠธ์›Œํ‚น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
  • URLSession๊ณผ Alamofire๋ฅผ ํ•œ๋ฒˆ ๋” ๊ฐ์‹ผ API
  • moya๊ฐ€ ์ œ์‹œํ•˜๋Š” ๊ธฐ๋ณธ ๊ตฌํ˜„ ๋ฐฉ์‹์˜ ๋ฌธ์ œ์ ์€?
1. ์ƒˆ๋กœ์šด ์•ฑ์„ ์“ฐ๊ธฐ ํž˜๋“ค๊ฒŒ ๋งŒ๋“ ๋‹ค.
2. ์•ฑ์„ ์œ ์ง€ํ•˜๊ธฐ ์–ด๋ ต๊ฒŒ ๋งŒ๋“ ๋‹ค.
3. unit ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๊ธฐ ์–ด๋ ต๊ฒŒ ๋งŒ๋“ ๋‹ค.
  • ๊ทธ๋Ÿผ moya๋Š” ๋ญ๊ฐ€ ๋” ์ข‹์„๊นŒ์š”?
  • moya๋Š” ์—ด๊ฑฐํ˜•(enum)์„ ์‚ฌ์šฉํ•˜์—ฌ ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๋ฐฉ์‹์„ type-safeํ•œ ๋ฐฉ์‹์œผ๋กœ ์บก์Šํ™” ํ•˜๋Š”๋ฐ ์ดˆ์ฒจ์„ ๋งž์ถ˜ ํ”„๋ ˆ์ž„์›Œํฌ
  • moya๋Š” ์ž์ฒด์ ์ธ ๋„คํŠธ์›Œํฌ ์ˆ˜ํ–‰์€ X, Alamofire์˜ ๋„คํŠธ์›Œํ‚น ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ์ถ”์ƒํ™” ํ•˜๊ธฐ ์œ„ํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์ œ๊ณตํ•œ๋‹ค. โ†’ ๊ฒฐ๋ก  : Alamofire ์ง์ ‘ ์‚ฌ์šฉX, Alamofire๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๊ณ  ์žˆ๋Š” Moya๋ฅผ ๊ฑฐ์ณ ์‚ฌ์šฉ O!

๐Ÿ˜ณ Moya ๊ทธ๋ž˜์„œ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•ด์š”?

  1. pod ์— ์„ค์น˜ํ•˜๊ธฐ โ†’ Moya๋ฅผ ์„ค์น˜ํ•˜๋ฉด ์ž๋™์œผ๋กœ Alamofire๋„ ์„ค์น˜๋˜๋Š” ํ˜•ํƒœ์ž…๋‹ˆ๋‹ค.
  1. ์„œ๋ฒ„ ํ†ต์‹ ์— ํ•„์š”ํ•œ 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]
             
            }
        }
        
    }

  1. ๋ฐ์ดํ„ฐ ํ†ต์‹  ๋ถ„๊ธฐ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ๋ชจ๋ธ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

    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))
                }
            }
        }
      }

  1. ์›ํ•˜๋Š” 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 {
    										
    			}
                    }
                }
        }

2.AVFoundation ์ด์šฉํ•ด์„œ TimeStamp Camera ๊ตฌํ˜„ํ•˜๊ธฐ

  • 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()))"
}

3. CollectionView Animation

  • ํ™ˆ์—์„œ ์นด๋“œ๋ฅผ ๋„˜๊ธธ ๋•Œ 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
       }

๐Ÿ‘€ ๋ฏผ์Šน

1. Login Animation ๊ตฌํ˜„

ํ•œ๋ฒˆ๋„ ํ•ด๋ณด์ง€๋Š” ์•Š์•˜์ง€๋งŒ, ์–ธ์ œ๋‚˜ iOS ์ฃผ๋‹ˆ์–ด ๊ฐœ๋ฐœ์ž๋กœ์„œ ๋„์ „ํ•ด๋ณด๊ณ  ์‹ถ์—ˆ๋˜ ์ž์ฒด animation ๊ตฌํ˜„์„ ๋„์ „ํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค.

  1. ๋ถ€ํƒ๋ฐ›์€ ์• ๋‹ˆ๋ฉ”์ด์…˜์— ๋Œ€ํ•œ ์„ค๋ช…
    ๋จผ์ € ์ด ๋””์ž์ธ์€ ๋””์ž์ด๋„ˆ๋ถ„์ด ์ œ์•ˆํ•ด์ฃผ์‹  ์†Œ์ค‘ํ•œ ์•„์ด๋””์–ด์˜€์Šต๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ, ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์•„์ด๋””, ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž‘์„ฑ๋ž€์ด ์ฒœ์ฒœํžˆ ์˜ฌ๋ผ์˜ค๋Š” ๋ฐฉ์‹์œผ๋กœ ํ™”๋ฉด์— ๊ทธ๋ ค์ง€๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์ด์—ˆ์Šต๋‹ˆ๋‹ค.

  2. ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋“ค์–ด๊ฐ„ ๋ถ€๋ถ„
    ์ด ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ์‹œ์ž‘ ์€ ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์ด ๋ˆŒ๋Ÿฌ์ง„ ์‹œ์ ๋ถ€ํ„ฐ ์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ @IBAction ์„ ๋กœ๊ทธ์ธ๋ฒ„ํŠผ์— ์„ค์ •ํ•ด๋†“๊ณ , ๊ทธ IBAction ๋‚ด๋ถ€์—์„œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ ์šฉ์„ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  3. ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฝ”๋“œ

    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
  4. ์ดˆ๊ธฐ ์œ„์น˜ ์„ค์ •
    ์•„๋ž˜์—์„œ ์œ„ ๋กœ ์›€์ง์—ฌ์•ผ ํ•˜๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์ด์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ฒ˜์Œ๋ถ€ํ„ฐ autolayout์„ 200 ๋งŒํผ ์•„๋ž˜๋กœ ์œ„์น˜๋ฅผ ์žก์•˜์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋ฒ„ํŠผ์ด ๋ˆŒ๋Ÿฌ์กŒ์„ ๋•Œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด ๋‹ค์‹œ 200๋งŒํผ ์˜ฌ๋ผ์˜ค๋„๋ก ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

  5. ์กฐ๊ฑด๋ฌธ ์„ค์ •
    ํ•œ๊ฐ€์ง€ ์˜ˆ์™ธ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์–ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์„ ์ฒ˜์Œ์œผ๋กœ ๋ˆŒ๋Ÿฌ ๋“ค์–ด์˜ค๋ฉด์„œ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ž‘๋™๋˜๊ณ , ๊ทธ ๋‹ค์Œ๋ถ€ํ„ฐ๋Š” ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ๋„ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ž‘๋™ํ•˜๋ฉด ์•ˆ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (๊ทธ๋ ‡๊ฒŒ ๋˜๋ฉด ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ ๋•Œ๋งˆ๋‹ค ์•„์ด๋”” ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ž€์ด 200์”ฉ ์œ„๋กœ ์˜ฌ๋ผ๊ฐˆํ…Œ๋‹ˆ๊นŒ์š”..) ๊ทธ๋ž˜์„œ loginBtnFirstPressed: Bool์„ ํ•˜๋‚˜ ์„ ์–ธํ•ด์ฃผ์–ด์„œ ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์ด ์ฒ˜์Œ์œผ๋กœ ๋ˆŒ๋ฆด ๋•Œ true์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ๊ณ , ๊ทธ ๋‹ค์Œ๋ถ€ํ„ฐ๋Š” ์„œ๋ฒ„ํ†ต์‹ ์ด ๋˜๊ณ  ์• ๋‹ˆ๋ฉ”์ด์…˜์€ ์ž‘๋™์ด ์•ˆ๋˜๋„๋ก ์ฒ˜๋ฆฌํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

  6. ๋’ค๋กœ ๋Œ์•„๊ฐ€๋Š” ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ
    ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์•„์ด๋”” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์น˜๋‹ค๊ฐ€, ๋’ค๋กœ ๋Œ์•„๊ฐ€๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ์—๋„ ๋˜‘๊ฐ™์ด ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ž‘๋™์„ ๋„ฃ์–ด์ฃผ์–ด์„œ ๋‹ค์‹œ ๋‚ด๋ ค๊ฐ€๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ‘€ ์„ธ์€

1. UIRefreshControl

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()
    }

2. Custom TabBar

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)
}
				

๐Ÿ“š Meaning Extension

1. Toast Alert Extension

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

  • ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋น ๋ฅด๊ฒŒ ์‹œ์ž‘๋˜๊ณ  ์™„๋ฃŒ ๋  ์ฏค ๋Š๋ ค์ง.

2. timeAgoSince Extension

๋งˆ์ดํ”ผ๋“œ์™€ ๊ทธ๋ฃนํ”ผ๋“œ์˜ ๊ฒŒ์‹œ๋ฌผ ์ž‘์„ฑ ์‹œ๊ฐ„์ด ํ˜„์žฌ๋กœ๋ถ€ํ„ฐ ์–ผ๋งˆ ์ „์ธ์ง€ ํ‘œ์‹œํ•ด์ฃผ๋Š” 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 "์ง€๊ธˆ"
        }

๐Ÿ‘‰ About Us


"๋ฏธ๋‹์˜ iOS ๊ฐœ๋ฐœ์ž๋“ค์€ ์ฝ”๋“œ๋ฆฌ๋ทฐ์™€ ํšจ์œจ์ ์ธ ํ˜‘์—…์œผ๋กœ ํ•จ๊ป˜ ์„ฑํ•˜๋Š” ์•ฑ๊ฐœ๋ฐœ์„ ์ง€ํ–ฅํ•ฉ๋‹ˆ๋‹ค."


๋ฏผํฌ ๋ฏผ์Šน ์„ธ์€
contact : xwoud@naver.com
github: xwoud
contact : seonminseung@naver.com
github: MinseungSeon
contact : hotpigtomato@gmail.com
github: pk33n
ํƒ€์ž„์Šคํƒฌํ”„, ํ™ˆ ํ™”๋ฉด ๋‹ด๋‹น ์Šคํ”Œ๋ž˜์‹œ ๋ฐ ๋กœ๊ทธ์ธ, ๋ฏธ์…˜ ํ™”๋ฉด ๋‹ด๋‹น ๊ทธ๋ฃน ๋ฐ ์ปค์Šคํ…€ ํƒญ๋ฐ” ๋‹ด๋‹น

About

๐ŸŽ๋ฏธ๋‹์–ธ์ฆˆ๋“ค์€ iOS๊ฐ€ ์ข‹iOS๐Ÿ


Languages

Language:Swift 99.9%Language:Ruby 0.1%