/GifSearchApp

๐Ÿ‘พgif๋ฅผ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๋Š” iOS ์•ฑ๐Ÿ‘พ

Primary LanguageSwift

Gif ๊ฒ€์ƒ‰/์ฆ๊ฒจ์ฐพ๊ธฐ ์•ฑ

์›€์ง์ด๋Š” gif๋ฅผ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๊ณ , ์ƒ์„ธ ์ •๋ณด๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋งˆ์Œ์— ๋“œ๋Š” gif๋ฅผ ์ฆ๊ฒจ์ฐพ๊ธฐํ•  ์ˆ˜ ์žˆ๋Š” iOS ์•ฑ ๊ฐœ๋ฐœ ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค.

[ ํ”„๋กœ์ ํŠธ ๊ฐœ๋ฐœํ™˜๊ฒฝ ]

  • iOS Depolyment Target: Xcode 11.6
  • Supporting Device Target: iOS 13
  • CocoPods Version: 1.10.0.beta.2
  • Language: Swift

[ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ]

  • Alamofire
    HTTP ํ†ต์‹  ์ค‘ Alamofire๊ฐ€ ์ œ๊ณตํ•˜๋Š” Request&Response ์ฒด์ด๋‹ ํ•จ์ˆ˜์™€ URL/JSON ํ˜•ํƒœ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ ์ธ์ฝ”๋”ฉ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์ฑ„ํƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • Kingfisher
    ๊ฐ™์€ ์ด๋ฏธ์ง€ URL์„ ๋งค๋ฒˆ ์ƒˆ๋กญ๊ฒŒ ํ˜ธ์ถœํ•˜๋Š” ๋ฐ์—์„œ ์˜ค๋Š” ์ง€์—ฐ์ด ๊ฒ€์ƒ‰ ํŽ˜์ด์ง€์™€ ์ฆ๊ฒจ์ฐพ๊ธฐ ๋ฆฌ์ŠคํŠธ์— ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์†๋„๋ฅผ ๋Šฆ์ถ˜๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.
    ๋”ฐ๋ผ์„œ ์ด๋ฏธ์ง€ ๋กœ๋“œ ์†๋„ ๊ฐœ์„ ์„ ์œ„ํ•ด Kingfisher์˜ ์ด๋ฏธ์ง€ ์บ์‹œ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

[ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ]

๊ฒ€์ƒ‰ ํ™”๋ฉด ์ฆ๊ฒจ์ฐพ๊ธฐ ํ™”๋ฉด ๋ชจ๋‹ฌ ํ™”๋ฉด
search_page favorite_page modal_page_favorite_add

[ ๊ธฐ๋Šฅ ]

1. ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ

๋ฒˆํ˜ธ ์ค‘์š”๋„ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์—ฌ๋ถ€
1 ๊ธฐ๋ณธ Giphy API๋ฅผ ์ด์šฉํ•œ ์ •์ ์ธ GIF ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ O
1-1 ๊ธฐ๋ณธ ์ƒ๋‹จ [๊ฒ€์ƒ‰ํ•˜๊ธฐ] ํ…์ŠคํŠธํ•„๋“œ๊ฐ€ ํฌ์ปค์Šค ๋˜๋ฉด ํ‚ค๋ณด๋“œ๊ฐ€ ๋ณด์—ฌ์ง€๋„๋ก ๊ตฌํ˜„ O
1-2 ๊ธฐ๋ณธ ์Šคํฌ๋กค๋ทฐ๋ฅผ ์Šคํฌ๋กคํ–ˆ์„ ๋•Œ, ํ‚ค๋ณด๋“œ์˜ Enter๋ฅผ ์ณค์„ ๋•Œ, ํ‚ค๋ณด๋“œ๊ฐ€ ๋‚ด๋ ค๊ฐ€๋„๋ก ๊ตฌํ˜„ O
1-3 ๊ธฐ๋ณธ ์˜๋‹จ์–ด๋ฅผ ์ž…๋ ฅํ–ˆ์„ ๋•Œ๋งˆ๋‹ค ๊ฒ€์ƒ‰ API๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ๊ตฌํ˜„ O
2 ๊ธฐ๋ณธ ๊ฒ€์ƒ‰ ํ•„๋“œ์— ๋นˆ ํ…์ŠคํŠธ๊ฐ€ ์ž…๋ ฅ๋˜์–ด ์žˆ๋‹ค๋ฉด ๋นˆ ํ™”๋ฉด์„ ๋ณด์—ฌ์ฃผ๋„๋ก ๊ตฌํ˜„ O
3 ๊ธฐ๋ณธ ํŽ˜์ด์ง•์„ ์ด์šฉํ•ด API์˜ ์ตœ๋Œ€๋กœ ์กฐํšŒ ๊ฐ€๋Šฅํ•œ ์ด๋ฏธ์ง€ ๊ฐฏ์ˆ˜ (limit=24)๋งŒํผ ๋ถˆ๋Ÿฌ์˜ค๋„๋ก ๊ตฌํ˜„ O
4 ๊ธฐ๋ณธ ๋ฆฌ์ŠคํŠธ ์ตœํ•˜๋‹จ์— <๋”๋ณด๊ธฐ> ๋ฒ„ํŠผ์„ ๋‘์–ด, ์ดํ›„์˜ ๋” ๋งŽ์€ ์ด๋ฏธ์ง€๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ O
5 ๊ธฐ๋ณธ ๊ฐ ์ด๋ฏธ์ง€๋ฅผ ๋ˆ„๋ฅผ ๋•Œ Modal์ด ๋œจ๋„๋ก ๊ตฌํ˜„ O
6 ํ•„์ˆ˜ ์–ด๋Š ๋””๋ฐ”์ด์Šค์—์„œ๋“  ํ•œ row์— ์ตœ๋Œ€ 3๊ฐœ ๋‹จ์œ„์˜ ์ด๋ฏธ์ง€๊ฐ€ ๊ทธ๋ฆฌ๋“œ ํ˜•์‹์œผ๋กœ ๋…ธ์ถœ๋˜๋„๋ก ๊ตฌํ˜„ O
6-1 ํ•„์ˆ˜ ๊ทธ๋ฆฌ๋“œ ๋‚ด์˜ Cell์€ ๊ฐ€๋กœ ์„ธ๋กœ์˜ ๊ธธ์ด๋Š” 1:1๋กœ ๋™์ผํ•˜๊ฒŒ ๊ตฌํ˜„ O
7 ํ•„์ˆ˜ ์ด๋ฏธ์ง€ ๋น„์œจ์€ ์œ ์ง€๋œ ์ฑ„ ์‚ฌ์ด์ฆˆ๊ฐ€ Cell์˜ ์‚ฌ์ด์ฆˆ๋ฅผ ๋„˜์ง€ ์•Š๋„๋ก ๋งž์ถ”๊ธฐ O
1 ์ถ”๊ฐ€ ์ •์ ์ธ GIF ์ด๋ฏธ์ง€๋ฅผ ๋™์ ์œผ๋กœ ์›€์ง์ด๊ฒŒ ๊ตฌํ˜„ X
2 ์ถ”๊ฐ€ ๋ฌดํ•œ์Šคํฌ๋กค์„ ์ ์šฉํ•˜์—ฌ ๋Š์ž„ ์—†์ด ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋“œ๋  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ O
3 ์ถ”๊ฐ€ ๊ฒ€์ƒ‰์–ด๊ฐ€ ์ž…๋ ฅ๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด trend API๋ฅผ ์ด์šฉํ•˜์—ฌ ์ตœ์‹  ํŠธ๋ Œ๋“œ GIF๋ฅผ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฆฌ์ŠคํŒ…ํ•˜๋„๋ก ๊ตฌํ˜„ O
4 ์ถ”๊ฐ€ ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋”ฉ๋  ๋•Œ placeholder ์ด๋ฏธ์ง€ ๋„ฃ๊ธฐ O
5 ์ถ”๊ฐ€ ๋„คํŠธ์›Œํฌ ์—๋Ÿฌ ๋“ฑ์— ์˜ํ•ด API ํ†ต์‹ ์ด ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ Alert ๋“ฑ์œผ๋กœ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•ˆ๋‚ดํ•˜๊ธฐ O

2. ์ฆ๊ฒจ์ฐพ๊ธฐ ๊ธฐ๋Šฅ

์ฆ๊ฒจ์ฐพ๊ธฐ ํ•ด์ œ ์ฆ๊ฒจ์ฐพ๊ธฐ ์ถ”๊ฐ€ ์ฆ๊ฒจ์ฐพ๊ธฐ ๊ฐฏ์ˆ˜ ์ œํ•œ ์ฆ๊ฒจ์ฐพ๊ธฐ ํ™”๋ฉด ๊ฒฐ๊ณผ ์ฆ๊ฒจ์ฐพ๊ธฐ ๊ฐœ์ˆ˜ 0๊ฐœ์ผ ๋•Œ
์ฆ๊ฒจ์ฐพ๊ธฐ ํ•ด์ œ ์ฆ๊ฒจ์ฐพ๊ธฐ ์ถ”๊ฐ€ ์ฆ๊ฒจ์ฐพ๊ธฐ ์ œํ•œ ์ฆ๊ฒจ์ฐพ๊ธฐ ํ™”๋ฉด ๊ฒฐ๊ณผ ์ฆ๊ฒจ์ฐพ๊ธฐ ๋ฐ์ดํ„ฐ ์—†๋Š” ํ™”๋ฉด
๋ฒˆํ˜ธ ์ค‘์š”๋„ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์—ฌ๋ถ€
1 ๊ธฐ๋ณธ ๋‚ด๊ฐ€ ์ฆ๊ฒจ์ฐพ๊ธฐํ•œ ์ด๋ฏธ์ง€ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ๊ตฌํ˜„ O
2 ๊ธฐ๋ณธ ๊ฐ ์ด๋ฏธ์ง€๋ฅผ ๋ˆ„๋ฅผ ๊ฒฝ์šฐ Modal ๋„์šฐ๊ธฐ O
3 ๊ธฐ๋ณธ ์•ฑ์„ ์ข…๋ฃŒํ•˜๊ธฐ ์ „๊นŒ์ง€ ๋‚ด๊ฐ€ ์ฆ๊ฒจ์ฐพ๊ธฐํ•œ ์ด๋ฏธ์ง€๊ฐ€ ํœ˜๋ฐœ๋˜์ง€ ์•Š๊ฒŒ ๊ตฌํ˜„ O
4 ๊ธฐ๋ณธ ์ตœ๋Œ€ 20๊ฐœ์˜ ์ด๋ฏธ์ง€๋ฅผ ์ฆ๊ฒจ์ฐพ๊ธฐ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ O
4-1 ๊ธฐ๋ณธ ์ตœ๋Œ€ 20๊ฐœ ์ด์ƒ ๋“ฑ๋ก ์‹œ ๊ฒฝ๊ณ ๋ฌธ๊ตฌ์™€ ํ•จ๊ป˜ ์ฆ๊ฒจ์ฐพ๊ธฐ์— ๋“ฑ๋ก๋˜์ง€ ์•Š๋„๋ก ๊ตฌํ˜„ O
5 ๊ธฐ๋ณธ ์–ด๋Š ๋””๋ฐ”์ด์Šค์—์„œ๋“  ํ•œ row์— ์ตœ๋Œ€ 3๊ฐœ ๋‹จ์œ„์˜ ์ด๋ฏธ์ง€๊ฐ€ ๊ทธ๋ฆฌ๋“œ ํ˜•์‹์œผ๋กœ ๋…ธ์ถœ๋˜๋„๋ก ๊ตฌํ˜„ O
5-1 ๊ธฐ๋ณธ ๊ทธ๋ฆฌ๋“œ ๋‚ด์˜ Cell์€ ๊ฐ€๋กœ ์„ธ๋กœ์˜ ๊ธธ์ด๊ฐ€ 1:1๋กœ ๋™์ผํ•˜๋„๋ก ๊ตฌํ˜„ O
6 ๊ธฐ๋ณธ ์ด๋ฏธ์ง€ ๋น„์œจ์€ ์œ ์ง€๋œ ์ฑ„ ์‚ฌ์ด์ฆˆ๊ฐ€ Cell์˜ ์‚ฌ์ด์ฆˆ๋ฅผ ๋„˜์ง€ ์•Š๋„๋ก ๋งž์ถ”๊ธฐ O
7 ์ถ”๊ฐ€ ์ •์ ์ธ GIF ์ด๋ฏธ์ง€๋ฅผ ๋™์ ์œผ๋กœ ์›€์ง์ด๊ฒŒ ๊ตฌํ˜„ X
8 ์ถ”๊ฐ€ ์ฆ๊ฒจ์ฐพ๊ธฐ ํ•œ ๋‚ด์šฉ์ด ์—†์„ ๋•Œ ๋นˆํ™”๋ฉด์„ ํšจ๊ณผ์ ์œผ๋กœ ์œ ์ €์—๊ฒŒ ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜ O
9 ์ถ”๊ฐ€ ์•ฑ์„ ์ข…๋ฃŒํ•˜๋”๋ผ๋„ Local Storage๋ฅผ ์ด์šฉํ•˜์—ฌ ํœ˜๋ฐœ๋˜์ง€ ์•Š๋„๋ก ๊ตฌํ˜„ O
10 ์ถ”๊ฐ€ ๊ฐ€์žฅ ์ตœ๊ทผ์— ์ฆ๊ฒจ์ฐพ๊ธฐํ•œ ์ด๋ฏธ์ง€๊ฐ€ ์ตœ์ƒ๋‹จ์— ๋ณด์ผ ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ O
11 ์ถ”๊ฐ€ ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋”ฉ๋  ๋•Œ placeholder ์ด๋ฏธ์ง€ ๋„ฃ๊ธฐ O
12 ์ถ”๊ฐ€ ๋„คํŠธ์›Œํฌ ์—๋Ÿฌ ๋“ฑ์— ์˜ํ•ด API ํ†ต์‹ ์ด ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ Alert ๋“ฑ์œผ๋กœ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•ˆ๋‚ดํ•˜๊ธฐ O

3. ๋ชจ๋‹ฌ ๊ธฐ๋Šฅ

ํƒญ1, ํƒญ2์—์„œ ์ด๋ฏธ์ง€ ์„ ํƒ ์‹œ ๋ณด์—ฌ์ง€๋Š” Modal์ฐฝ

์ „์ฒด ๊ณต์œ  ํŒŒ์ผ ๊ณต์œ 
์ „์ฒด ๊ณต์œ  ํŒŒ์ผ ๊ณต์œ 
๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜
๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜
๋ฒˆํ˜ธ ์ค‘์š”๋„ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์—ฌ๋ถ€
1 ๊ธฐ๋ณธ Modal๋กœ ๋ณด์—ฌ์ง€๊ฒŒ ๊ตฌํ˜„ O
2 ๊ธฐ๋ณธ ์ •์ ์ธ ์ด๋ฏธ์ง€์™€ ๊ด€๋ จ ์ •๋ณด(์ด๋ฆ„, rate ๋“ฑ) ํ…์ŠคํŠธ๋ฅผ ๋ณด์—ฌ์ฃผ๋„๋ก ๊ตฌํ˜„ O
3 ๊ธฐ๋ณธ ํƒ€์ธ์—๊ฒŒ ์ด๋ฏธ์ง€ ์ฃผ์†Œ์™€ ์ด๋ฆ„์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฒ„ํŠผ์„ ํฌํ•จ O
4 ๊ธฐ๋ณธ ๋ณ„ ํ‘œ์‹œ ๋ฒ„ํŠผ์„ ํ†ตํ•ด ๋งˆ์Œ์— ๋“œ๋Š” ์ด๋ฏธ์ง€๋ฅผ ์ฆ๊ฒจ์ฐพ๊ธฐ, ํ•ด์ œํ•  ์ˆ˜ ์žˆ๊ฒŒ ๊ตฌํ˜„ O
5 ํ•„์ˆ˜ ์ด๋ฏธ์ง€์˜ ํฌ๊ธฐ๋Š” 160X160 ์ด๋‚ด์—์„œ ๋น„์œจ์ด ์œ ์ง€๋œ ์ฑ„๊ณ  ๋ณด์—ฌ์ง€๋„๋ก ๊ตฌํ˜„ O
6 ํ•„์ˆ˜ ์ด๋ฏธ ์ฆ๊ฒจ์ฐพ๊ธฐ์— ๋“ฑ๋กํ•œ ์ด๋ฏธ์ง€๊ฐ€ 20๊ฐœ ์ด์ƒ์ด๋ผ๋ฉด ์‹ ๊ทœ๋กœ ์ฆ๊ฒจ์ฐพ๊ธฐํ•  ์ˆ˜ ์—†๋„๋ก ๊ตฌํ˜„ O
7 ์ถ”๊ฐ€ ์ •์ ์ธ GIF ์ด๋ฏธ์ง€๋ฅผ ๋™์ ์œผ๋กœ ์›€์ง์ด๊ฒŒ ๊ตฌํ˜„ X
8 ์ถ”๊ฐ€ ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋”ฉ๋  ๋•Œ placeholder ์ด๋ฏธ์ง€ ๋„ฃ๊ธฐ O
9 ์ถ”๊ฐ€ ๋„คํŠธ์›Œํฌ ์—๋Ÿฌ ๋“ฑ์— ์˜ํ•ด API ํ†ต์‹ ์ด ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ Alert ๋“ฑ์œผ๋กœ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•ˆ๋‚ดํ•˜๊ธฐ O

[ ํ”„๋กœ์ ํŠธ ์„ค๊ณ„ ]

ํ”„๋กœ์ ํŠธ ํด๋” ๊ตฌ์กฐ

  • Resources ํด๋”: ์ด๋ฏธ์ง€ ๋“ฑ๊ณผ ๊ฐ™์€ ํ”„๋กœ์ ํŠธ ๋ฆฌ์†Œ์Šค๋ฅผ ๊ด€๋ฆฌ
  • Sources ํด๋”: ํ”„๋กœ์ ํŠธ ์†Œ์Šค(Storyboard, ViewController, View, Model, Service)๋ฅผ ๊ด€๋ฆฌ
  • Utils ํด๋”: ํ”„๋กœ์ ํŠธ ์ „์ฒด์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ๊ธฐํƒ€ ํŒŒ์ผ์„ ๊ด€๋ฆฌ
    • Networkํด๋”: ์ „์ฒด ๋„คํŠธ์›Œํ‚น์„ ์ง€์›ํ•˜๋Š” ํ”„๋กœํ† ์ฝœ ํŒŒ์ผ ๊ด€๋ฆฌ
    • Extensions ํด๋”: UIView, UIViewController ๋“ฑ์˜ ๋‹ค์–‘ํ•œ Extension ๊ด€๋ฆฌ
  • SupportingFiles ํด๋”: ๊ทธ ๋ฐ–์˜ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง€์›ํ•˜๋Š” ํŒŒ์ผ(AppDelegate, SceneDelegate, Info.plist)์„ ๊ด€๋ฆฌ

๋„คํŠธ์›Œํฌ ๋ ˆ์ด์–ด ๋ฐฉ์‹ ๋„์ž…

ํด๋ผ์ด์–ธํŠธ์—์„œ Giphy API ๋‚ด์šฉ์„ ์ฝ์–ด๋“ค์ด๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋งŽ์€ ํ•จ์ˆ˜๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ํ•œ ์š”์ฒญ์ด ํ•„์š”ํ•˜๊ณ , ์ด ๊ณผ์ •์—์„œ ์œ ์‚ฌํ•œ ์ฝ”๋“œ๋ฅผ ๋ฐ˜๋ณต์ ์œผ๋กœ ์ž‘์„ฑํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๋ฐ˜๋ณต์  ํ•จ์ˆ˜๋ฅผ ์ œ๋„ค๋ฆญ์„ ๋ฐ”ํƒ•์œผ๋กœ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋„คํŠธ์›Œํฌ ๋ ˆ์ด์–ด ๋ฐฉ์‹์„ ๋„์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ „๋ฐ˜์ ์ธ ๊ตฌ์กฐ๋Š” Controller, Middle Layer, Service Manager, Model ๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.

1. Controller
Controller ๋Š” ์„œ๋น„์Šค๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ViewController๋ฅผ ๋œปํ•ฉ๋‹ˆ๋‹ค. ViewController์—์„œ ์„œ๋น„์Šค๋ฅผ ํ˜ธ์ถœํ•˜๊ฒŒ ๋˜๋ฉด Middle Layer๋ฅผ ํ˜ธ์ถœํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
2. Middle Layer
Middle Layer๋ž€ Controller์™€ Service Manager ์‚ฌ์ด์˜ ๋ธŒ๋ฆฟ์ง€ ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค. Middle Layer์—์„œ๋Š” ์‘๋‹ต ์ฝ”๋“œ๋ฅผ ํ†ตํ•œ ๋„คํŠธ์›Œํฌ ์ƒํƒœ๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
3. Service Manager
๋„คํŠธ์›Œํฌ ์ƒํƒœ๊ฐ€ ์„ฑ๊ณต์ ์ด๋ผ๋ฉด, ๋„คํŠธ์›Œํฌ ๋ ˆ์ด์–ด์˜ ํ•ต์‹ฌ์ธ Service Manager์—์„œ Codable์„ ํ†ตํ•ด ์ •์˜ํ•œ Model์„ ํ†ตํ•ด ํŒŒ์‹ฑํ•œ ํ›„ JSON ๋ฐ์ดํ„ฐ์™€ ์ƒํƒœ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

์ดํ›„ Controller์ธ ViewController์—์„œ ์ƒํƒœ ์ฝ”๋“œ์™€ ์ „๋‹ฌ๋ฐ›์€ ๋ฐ์ดํ„ฐ์— ๋งž๊ฒŒ View๋ฅผ ๋ฐ”๊ฟ”์ฃผ๋Š” ํ˜•ํƒœ๋กœ ๋„คํŠธ์›Œํ‚น์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

[ ํ”„๋กœ์ ํŠธ ์ด์Šˆ ]

์ด๋ฏธ์ง€ ๋ฆฌ์ŠคํŠธ ์Šคํฌ๋กค ๋Š๊น€ ํ˜„์ƒ

1. ๋ฌธ์ œ์ •์˜: Gifhy API์—์„œ ๋ถˆ๋Ÿฌ์˜จ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ๋“ค์„ '๊ฒ€์ƒ‰ํ™”๋ฉด' collectionView์˜ cell์— ํ‘œ์‹œํ•˜๋‹ˆ ์Šคํฌ๋กค ์„ฑ๋Šฅ์— ๋งค์šฐ ํฌ๊ฒŒ ์ €ํ•˜๋์Šต๋‹ˆ๋‹ค.
2. ์›์ธ: ๋ฌด๊ฑฐ์šด ์šฉ๋Ÿ‰์˜ Gif ์ด๋ฏธ์ง€๋ฅผ ํ•ด๋‹น cell์ด ํ™”๋ฉด์— ํ‘œ์‹œ๋  ๋•Œ๋งˆ๋‹ค ์žฌ์š”์ฒญํ•˜์—ฌ ํ™”๋ฉด์— ํ‘œ์‹œํ•ด์•ผ ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
3. ํ•ด๊ฒฐ์ฑ…:
๋”ฐ๋ผ์„œ ์ฒซ ๋ฒˆ์งธ๋กœ, cell ๋‚ด์˜ imageView์— ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋กœ๋“œํ•˜๋Š” ๊ณผ์ •์„ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ˆ˜ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
...
         DispatchQueue.global().async {
            DispatchQueue.main.async {
               cell.imageView.kf.setImage(with: url)
            }
         }
...
}

๋‘ ๋ฒˆ์งธ๋กœ, UICollectionViewDataSourcePrefetching ํ”„๋กœํ† ์ฝœ์„ ํ™œ์šฉํ•˜์—ฌ collectionView cell์ด ํ‘œ์‹œ๋  ๊ฒƒ์„ ๋ฏธ๋ฆฌ ์˜ˆ์ƒํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

extension SearchVC: UICollectionViewDataSourcePrefetching {
    func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
        for indexPath in indexPaths {
            if let cellToUpdate = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as? GifCVCell {
                if indexPath.row < gifDataList.count {
                    let url = gifDataList[indexPath.row].url
                    DispatchQueue.global().async {
                        DispatchQueue.main.async {
                            cellToUpdate.imageView.kf.setImage(with: url)
                        }
                    }
                    if let gifId = Int(gifDataList[indexPath.row].id) {
                        cellToUpdate.tag = gifId
                    }
                }
            }
        }
    }
}

CollectionView ๋ฌดํ•œ ์Šคํฌ๋กค ๊ธฐ๋Šฅ

1. ๋ฌธ์ œ์ •์˜: collectionView์˜ ๋์— ๋‹ค๋‹ค๋ฅผ ๋•Œ๊นŒ์ง€ ์Šคํฌ๋กค ํ–ˆ์„ ๋•Œ, ์ƒˆ๋กœ๊ณ ์นจ์ด ๋™์‹œ์ ์œผ๋กœ ์—ฌ๋Ÿฌ๋ฒˆ ํ˜ธ์ถœ๋˜๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.
2. ์›์ธ
ํ•ด๋‹น ์‹œ์ ์— ์ถ”๊ฐ€์ ์ธ gif ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•˜๋Š” getGifList() ํ•จ์ˆ˜๊ฐ€ ๋น„๋™๊ธฐ๋กœ ์‹คํ–‰๋˜๋Š” ํ•จ์ˆ˜์˜€๊ธฐ ๋•Œ๋ฌธ์ด์—ˆ์Šต๋‹ˆ๋‹ค.
3. ํ•ด๊ฒฐ์ฑ…
flag๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ getGifList() ํ•จ์ˆ˜ ์‹คํ–‰์ด ๋๋‚œ ํ›„์— ์ƒˆ๋กœ์šด ์ƒˆ๋กœ๊ณ ์นจ์„ ์š”์ฒญํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

func scrollViewDidScroll(_ scrollView: UIScrollView) {
   if self.collectionView.window == nil {
      return
   }
   let offsetTolerance = CGFloat(30)
        
   let offsetY = collectionView.contentOffset.y
   let contentHeight = collectionView.contentSize.height
        
    if offsetY > contentHeight - (collectionView.bounds.size.height  + offsetTolerance), !scrollViewReachedBottom {
      self.scrollViewReachedBottom = true
      self.offset += 25
      getGifList(keyword: gsno(self.searchTextField.text), offset: offset)
    }
  }
}
func getGifList(keyword: String, offset: Int) {
   GetGifSearchService.sharedInstance.getGifList(params: params) { (result) in
      switch result {
         case .networkSuccess(let data): //200
            let gifData = data as? GifSearchModel
            if let resResult = gifData {
               if let resultData = resResult.data {
                  ...
                  self.collectionView.reloadData()
                  self.scrollViewReachedBottom = false
               }
            }
            break
            ...
}

UserDefault์˜ key๊ฐ’์— Custom value ์„ค์ •ํ•˜๊ธฐ

1. ๋ฌธ์ œ์ •์˜
UserDefault์˜ value์— Custom ๊ตฌ์กฐ์ฒด๊ฐ’์œผ๋กœ ์„ค์ •ํ•˜๋‹ˆ [User Defaults] Attempt to set a non-property-list ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.
2. ์›์ธ
UserDefault๊ฐ€ ์ง€์›ํ•˜๋Š” value ํƒ€์ž…์—์„œ ๋ฒ—์–ด๋‚ฌ๊ธฐ ๋•Œ๋ฌธ์ด์—ˆ์Šต๋‹ˆ๋‹ค.
3. ํ•ด๊ฒฐ์ฑ…
๋”ฐ๋ผ์„œ UserDefault๊ฐ€ ์ง€์›ํ•˜๋Š” ํƒ€์ž…๊ผด๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , ํ•ด๋‹น key๊ฐ’์— ์ ‘๊ทผ์„ ์šฉ์ดํ•˜๊ฒŒ ํ•˜๊ธฐ์œ„ํ•ด FavoriteGifCache ๊ตฌ์กฐ์ฒด๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • FavoriteGifCache ๊ตฌ์กฐ์ฒด ๋‚ด์— favorites ํ‚ค ๊ฐ’์— ํ•ด๋‹นํ•˜๋Š” ์บ์‹œ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์œ„ํ•œ get ํ•จ์ˆ˜ ๊ตฌํ˜„
  • FavoriteGifCache ๊ตฌ์กฐ์ฒด ๋‚ด์— favorites ํ‚ค ๊ฐ’์— ํ•ด๋‹นํ•˜๋Š” ์บ์‹œ์— ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€๋ฅผ ์œ„ํ•œ save ํ•จ์ˆ˜ ๊ตฌํ˜„
  • FavoriteGifCache ๊ตฌ์กฐ์ฒด ๋‚ด์— favorites ํ‚ค ๊ฐ’์— ํ•ด๋‹นํ•˜๋Š” ์บ์‹œ์— ๋ฐ์ดํ„ฐ ์‚ญ์ œ๋ฅผ ์œ„ํ•œ remove ํ•จ์ˆ˜ ๊ตฌํ˜„
struct FavoriteGifCache {
    static let key = "fatorites"
    static func save(value: Dictionary<String, FavoriteGifInfo>) {
         UserDefaults.standard.set(try? PropertyListEncoder().encode(value), forKey: key)
    }
    
    static func get() -> Dictionary<String, FavoriteGifInfo>! {
        var userData: Dictionary<String, FavoriteGifInfo>!
        if let data = UserDefaults.standard.value(forKey: key) as? Data {
            userData = try? PropertyListDecoder().decode(Dictionary<String, FavoriteGifInfo>.self, from: data)
            return userData ?? Dictionary<String, FavoriteGifInfo>()
        } else {
            return userData
        }
    }
    
    static func remove() {
        UserDefaults.standard.removeObject(forKey: key)
    }
}

Modally Presentํ•œ Modal์ฐฝ์—์„œ ์ด์ „ ๋ทฐ๋กœ ๋ฐ์ดํ„ฐ ์ „๋‹ฌํ•˜๊ธฐ

1. ๋ฌธ์ œ์ •์˜

  • ์ฆ๊ฒจ์ฐพ๊ธฐ ํ™”๋ฉด์—์„œ Modal์ฐฝ์„ ๋„์šฐ๊ณ  ์ฆ๊ฒจ์ฐพ๊ธฐ๋ฅผ ํ•ด์ œํ•œ ํ›„ ๋‹ค์‹œ ๊ธฐ์กด์˜ ๋ทฐ๋กœ ๋Œ์•„์˜ค๋ฉด ์ฆ๊ฒจ์ฐพ๊ธฐ ํ™”๋ฉด์ด ์ฆ‰๊ฐ์ ์œผ๋กœ ๊ฐฑ์‹ ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
  • Modal์ฐฝ์ด ๋‚ด๋ ค๊ฐ€๋Š” ์‹œ์ ์— ๊ธฐ์กด์˜ ์ฆ๊ฒจ์ฐพ๊ธฐ ํ™”๋ฉด์—์„œ viewWillAppear/viewDidAppear์ด ํ˜ธ์ถœ๋˜๊ณ , ํ•ด๋‹น ์‹œ์ ์— ํ™”๋ฉด ๊ฐฑ์‹ ์„ ๊ธฐ๋Œ€ํ–ˆ์œผ๋‚˜ ์˜ˆ์ƒ์‹œ๋‚˜๋ฆฌ์˜ค์ฒ˜๋Ÿผ ๊ตฌํ˜„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

2. ์›์ธ
UIModalPresentationStyle๋กœ ๋ทฐ๊ฐ€ ํ˜ธ์ถœ๋  ๋•Œ๋Š” ๊ธฐ์กด์˜ present์˜ ์ƒ๋ช…์ฃผ๊ธฐ์™€ ๋‹ค๋ฅธ ๊ฒƒ์ด ์›์ธ์ด์—ˆ์Šต๋‹ˆ๋‹ค. modally presentํ•  ๋•Œ ๊ธฐ์กด ๋ทฐ๊ฐ€ viewWill/Diddisappear ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— Modal์ฐฝ์ด ์‚ฌ๋ผ์ง€๋”๋ผ๋„ viewWillAppear ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ collectionView.reloadData() ๋˜ํ•œ ํ˜ธ์ถœ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ๋ชจ๋‹ฌ์ฐฝ์—์„œ ์ฆ๊ฒจ์ฐพ๊ธฐ ๋ฆฌ์ŠคํŠธ๊ฐ€ ๊ฐฑ์‹ ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.
3. ํ•ด๊ฒฐ์ฑ…
modal์ฐฝ์—์„œ ์ฆ๊ฒจ์ฐพ๊ธฐ ํ•ด์ œ ์‹œ, ์ด์ „ ๋ทฐ์˜ collectionView.reloadData()๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก NotificationCenter๋ฅผ NotificationCenterํ™œ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ๋ชจ๋‹ฌ ํ™”๋ฉด
NotificationCenter.default.post(name: Notification.Name(rawValue: "reloadCollectionView"), object: nil)
  • ์ฆ๊ฒจ์ฐพ๊ธฐ ํ™”๋ฉด
override func viewDidLoad() {
    super.viewDidLoad()
    NotificationCenter.default.addObserver(self, selector: #selector(reloadCollectionView(_ :)), name: Notification.Name(rawValue: "reloadCollectionView"), object: nil)
}

@objc func reloadCollectionView(_ notification: Notification) {
    reloadView()
}
    
func reloadView() {
    initializeFavoriteGifInfoList()
    collectionView.reloadData()
}

. ..