Skip to content

sloDevz/iOS-Weather-Forecast

ย 
ย 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

124 Commits
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐ŸŒค๏ธ ๋‚ ์”จ ์•ฑ

๐Ÿ€ Team MeLo ๐ŸŒ

๐Ÿƒ๐Ÿป๐Ÿƒ๐Ÿปโ€โ™‚๏ธ๐Ÿ’จ ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„: 23.03.13 ~ 23.04.14

๐Ÿ€ย Logan ๐Ÿ€ ๐ŸŒ Mason ๐ŸŒ

๋ฆฌ๋ทฐ์–ด: ๋ผ์ž๋ƒ( @wonhee009 )

๐Ÿ“ฑ ๊ตฌํ˜„ํ™”๋ฉด

Simulator Screen Recording - iPhone 14 Pro - 2023-04-13 at 13.34.09.gif

โœจย ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ

  • ๋„คํŠธ์›Œํ‚น
    • ๋ฒ”์šฉ์„ฑ๊ณผ ์žฌ์‚ฌ์šฉ์„ฑ, ์œ ์—ฐ์„ฑ์„ ๊ณ ๋ คํ•œ ๋„คํŠธ์›Œํ‚น ํƒ€์ž… ๊ตฌํ˜„
      • Repository, NetworkService์˜ ๋ ˆ์ด์–ด ๋ถ„๋ฆฌ
      • Endpoint ๊ฐ์ฒด ๋ถ„๋ฆฌ๋ฅผ ํ†ตํ•œ ํ™•์žฅ์„ฑ ์žˆ๋Š” ํ”„๋กœ๊ทธ๋žจ ์„ค๊ณ„
    • URLSession
      • Completion handler ๋ฐฉ์‹์—์„œ Swift Concurrency ๋ฅผ ํ™œ์šฉํ•œ async await ๋ฐฉ์‹์œผ๋กœ ๋ฆฌํŒฉํ„ฐ๋ง
  • ์ปฌ๋ ‰์…˜๋ทฐ ๊ตฌํ˜„
    • FlowLayout์˜ ๊ธฐ๋ณธ ๊ตฌํ˜„ โ†’ CompositionalLayout list ๊ตฌํ˜„์œผ๋กœ ๋ฆฌํŒฉํ„ฐ๋ง
  • CoreLocation
    • ์‚ฌ์šฉ์ž ์œ„์น˜ ๊ถŒํ•œ ์ฒดํฌ ๋ฐ ๋ถ€์—ฌ
    • ํ˜„์žฌ ์ขŒํ‘œ, ์ฃผ์†Œ ๊ฐ€์ ธ์˜ค๊ธฐ
  • ์ด๋ฏธ์ง€ ์บ์‹ฑ
    • NSCache ๋ฅผ ํ™œ์šฉ
  • Core Graphics ๋ฅผ ํ™œ์šฉํ•œ ๊ทธ๋ž˜ํ”„ ๊ตฌํ˜„
    • ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ , ๋‚ ์”จ ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋ž˜ํ”„๋กœ ์ง์ ‘ ๊ทธ๋ฆฌ๊ฒŒ ๊ตฌํ˜„

โ‰๏ธย ๊ณ ๋ฏผ๊ณผ ํ•ด๊ฒฐ

STEP1

1๏ธโƒฃย API ํ‚ค ์ˆจ๊ธฐ๊ธฐ

  • APIKey.xcconfig ๋ผ๋Š” ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ , ํ•ด๋‹น ํŒŒ์ผ์„ .gitignore์— ์ถ”๊ฐ€ํ•œ ๋’ค Info.plist ์—์„œ ํ•ด๋‹น ํ‚ค๊ฐ’์„ ๋ฐ›์•„์™€์„œ Bundle.main.object(forInfoDIctionaryKey:) ๋ฐฉ์‹์œผ๋กœ ํ•ด๋‹น ๊ฐ’์„ ๋ฐ›์•„์˜ด
  • git์ด ์ถ”์ ํ•˜๋Š” ํ”„๋กœ์ ํŠธ ํด๋” ๋ฐ”๋กœ ๋ฐ”๊นฅ ํด๋”์— ํ•ด๋‹น configuration ํŒŒ์ผ์„ ๋‘๊ณ , relative path (../../../APIKey.xcconfig) ๋กœ ์„ค์ •ํ•˜์—ฌ ์ฒ˜๋ฆฌ

2๏ธโƒฃย Repository์˜ ์—ญํ• , Service์˜ ์—ญํ• 

STEP2

1๏ธโƒฃย LocationManager๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๊ฐ์ฒด ๋ถ„๋ฆฌ

  • ์‚ฌ์šฉ์ž์˜ ์œ„์น˜๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๊ธฐ๋Šฅ์— ๋Œ€ํ•ด์„œ๋Š” ๋‚ ์”จ ์•ฑ ํŠน์„ฑ์ƒ ์‚ฌ์šฉ์ž์˜ ์œ„์น˜๋ฅผ ๊ณ„์†ํ•ด์„œ ์—…๋ฐ์ดํŠธ ํ•  ํ•„์š”๋Š” ์—†๋‹ค ์ƒ๊ฐํ•˜์—ฌ ํ•„์š”ํ•  ๋•Œ๋งŒ ์œ„์น˜๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๊ฒƒ์œผ๋กœ ๊ตฌํ˜„.

  • ๊ตฌํ˜„ ์ดˆ๊ธฐ์—” fetchLocation ํ›„์— ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฐฉ์‹์œผ๋กœ ๋„˜์–ด์˜ค๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ VC๋กœ ๋„˜๊ธฐ๋Š” ๊ณผ์ •์—์„œ completionHandler ๋กœ closure ๋ฅผ ํ• ๋‹นํ•˜์—ฌ ์ฒ˜๋ฆฌํ•˜๋Š” ์‹์œผ๋กœ ๊ตฌํ˜„.

    // ๊ตฌํ˜„ ์ดˆ๊ธฐ -> CompletionHandler ๋กœ ์ฒ˜๋ฆฌ
    final class LocationDataManager: NSObject, CLLocationManagerDelegate {
    		let locationManager = CLLocationManager()
        private var locationUpdateCompletion: ((CLLocation) -> Void)?
    
    		// completion์„ ํ• ๋‹นํ•˜๊ณ  ์œ„์น˜๋ฅผ ์š”์ฒญ
    		func requestLocation(completion: @escaping (CLLocation?) -> Void) {
            locationUpdateCompletion = completion
            locationManager.requestLocation()
        }
    
    		// ์™„๋ฃŒ๋œ ํ›„, completion์— ๋„˜๊ธฐ๊ณ  nil์„ ํ• ๋‹นํ•ด์„œ ํ•ด์ œ
    		func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            guard let location = locations.last else { return }
            locationUpdateCompletion?(location)
            locationUpdateCompletion = nil
        }
    
    // ViewController
    class ViewController: UIViewController {
    		// ํ˜ธ์ถœ ์‹œ...
    		locationDataManager.requestLocation { [weak self] location in
    				self?.location = location
        }
    

    ์ด๋ ‡๊ฒŒ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด didUpdateLocations ๋กœ ๊ฒฐ๊ณผ๊ฐ’์ด ๋„˜์–ด์˜จ ๋’ค, completion์— nil์„ ํ• ๋‹น.

  • ํ•˜์ง€๋งŒ ์‚ฌ์šฉ์ž ์œ„์น˜ ๊ถŒํ•œ ์„ค์ •๋„ ๋™์ผํ•œ ๋กœ์ง์œผ๋กœ ์ž‘์„ฑํ•  ๋•Œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋Š”๋ฐ, ์‚ฌ์šฉ์ž ๊ถŒํ•œ์— ๋Œ€ํ•œ delegate ๋ฉ”์„œ๋“œ๋Š” ํ”„๋กœ๊ทธ๋žจ์ด ์‹œ์ž‘๋˜๊ณ  ์–ผ๋งˆ ์ง€๋‚˜์ง€ ์•Š์•„ ์•Œ์•„์„œ ๋ถˆ๋ฆฌ๋Š” ๋ฉ”์„œ๋“œ์˜ ํŠน์ง•์„ ์ง€๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— completion ์ด ๋ฐ”๋กœ nil์ด ๋˜๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒ.

    flag ๋ฅผ ๋‘๊ฑฐ๋‚˜, ์‹œ๊ฐ„์„ ์ฒดํฌํ•˜๋Š” ๋“ฑ ์—ฌ๋Ÿฌ ์šฐํšŒ๋ฐฉ๋ฒ•์ด ์žˆ๊ธด ํ•˜๊ฒ ์ง€๋งŒ ๋ณธ์งˆ์ ์ธ ํ•ด๊ฒฐ์ด ์•„๋‹ˆ๋ผ ์ƒ๊ฐ.

  • ๊ณ ๋ฏผ๋์— ํ•ด๋‹น LocationDataManager์˜ delegate๋ฅผ ์„ค์ •ํ•˜์ž๋Š” ํŒ๋‹จ์„ ํ•˜๊ฒŒ ๋จ.

  • CLLocationManager ๊ฐ์ฒด์˜ delegate๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” LocationDataManager์˜ delegate๋ฅผ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด ๋น„ํšจ์œจ์ ์ธ๊ฒŒ ์•„๋‹Œ๊ฐ€ ๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ์ง€๋งŒ, ๋‚ด๋ถ€ ์ฒ˜๋ฆฌ๋ฅผ ์ถ”์ƒํ™”ํ•˜์—ฌ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋งŒ ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ๋กœ ๋„˜๊ธธ ์ˆ˜ ์žˆ๋‹ค๋Š” ๋ฐ์„œ ์žฅ์ ์ด ์žˆ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ๊ธฐ์— ์ง„ํ–‰.

2๏ธโƒฃย OSLog ํ™œ์šฉ

  • ํŠน๋ณ„ํ•œ error handling ํ•˜์ง€ ์•Š๋Š” ์ƒํ™ฉ์—์„œ print ๋ฌธ์œผ๋กœ ์ฒ˜๋ฆฌํ•ด๋†“์•˜๋˜ ๊ตฌ๋ฌธ๋“ค์„ OSLog ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๊ธฐ๋ก์ด ๋‚จ์„ ์ˆ˜ ์žˆ๊ฒŒ ๋ณ€๊ฒฝ

  • OSLog ์˜ ๋ฉ”์‹œ์ง€๋กœ๋Š” StaticString ๋งŒ์„ ํ—ˆ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— swift์˜ string interpolation ์œผ๋กœ๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์—†๋‹ค๊ณ  ํ•˜์—ฌ, Objective-C์˜ string interpolation ๋ฐฉ์‹์ธ โ€œ%@โ€์„ ์‚ฌ์šฉํ•˜์—ฌ ์ถœ๋ ฅํ•˜๋„๋ก ๊ตฌํ˜„

    func log(_ log: OSLog, error: Error) {
        os_log(.error, log: log, "%@", error.localizedDescription)
    }

STEP3

๐Ÿ’ฐย ์ด๋ฏธ์ง€ ์บ์‹ฑ ๊ตฌํ˜„

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

๐Ÿ—“๏ธย DateFormatter ๊ตฌํ˜„

  • ์„œ๋ฒ„์—์„œ ๋ฐ›์•„์˜จ โ€œ2023-04-06 14:22:00โ€ ํ˜•ํƒœ์˜ ๋‚ ์งœ ๋ฐ์ดํ„ฐ๋ฅผ ์š”๊ตฌ์‚ฌํ•ญ์ธ โ€œ04/06(๋ชฉ) 14์‹œโ€ ํ˜•ํƒœ๋กœ ๋ณ€๊ฒฝํ•˜๊ธฐ ์œ„ํ•ด DateFormatter ๋กœ์ง์„ ๊ตฌํ˜„.
  • ์ฒ˜์Œ์—” ์•„๋ž˜์ฒ˜๋Ÿผ DateFormatter ์˜ extension์„ ๊ตฌํ˜„ํ•ด์„œ ์ฒ˜๋ฆฌํ•ด์คฌ์ง€๋งŒ, ์ด ๋กœ์ง์—์„œ ๋ช‡๊ฐ€์ง€ ๋‹จ์ ์„ ๋ฐœ๊ฒฌ.
  1. extension์ด๋ผ๋Š” ์ 
  • ํŠน์ • ๊ฒฝ์šฐ (์…€์— ํ‘œ์‹œ)์—๋งŒ ์“ฐ์ด๋Š” ๋กœ์ง์ด๊ธฐ์—, ๋ชจ๋“  dateFormatter๊ฐ€ ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋ฅผ ์•Œ ํ•„์š”๋Š” ์—†์„ ๋“ฏ ํ•ด๋ณด์˜€์Œ
  1. ๋ชจ๋“  cell์ด ๋งค๋ฒˆ dateFormatter ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•œ๋‹ค๋Š” ์ 
  • dateFormatter๋ฅผ VC์˜ ํ”„๋กœํผํ‹ฐ๋กœ ๋นผ๋Š” ๊ฒƒ์œผ๋กœ ํ•ด๊ฒฐ์€ ๊ฐ€๋Šฅ
  1. ๋งค๋ฒˆ DateFormatter์˜ dateFormat์„ 2๋ฒˆ์”ฉ ๋ณ€๊ฒฝ ํ•ด์ฃผ๋Š” ๋กœ์ง์ด ์กด์žฌํ•จ
  • ๊ทธ๋ ‡๊ฒŒ ํฐ ๋น„์šฉ์ด ์•„๋‹ ์ˆ˜ ์žˆ์ง€๋งŒ, CollectionView Cell ์— ์‚ฌ์šฉ๋˜๋Š” ๋กœ์ง์ด๊ธฐ์— ์•„์ฃผ ๋น ๋ฅธ ์‹œ๊ฐ„ ๋‚ด์— ์—ฌ๋Ÿฌ๋ฒˆ ํ˜ธ์ถœ๋˜๋Š” ์„ฑ๊ฒฉ์„ ์ง€๋‹Œ๋‹ค๊ณ  ํŒ๋‹จํ•จ

    extension DateFormatter {
        func dateString(with dateString: String) -> String {
            self.dateFormat = "yyyy-MM-dd HH:mm:ss"
            guard let date = self.date(from: dateString) else { return "Formatting dateString fail"}
    
            self.dateFormat = "MM/dd(EEE) HH์‹œ"
            let formattedDate = self.string(from: date)
    
            return formattedDate
        }
    }
    func collectionView(_ collectionView: UICollectionView,
                        cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    		...
        let date = DateFormatter().dateString(with: weather.dateString)
        cell.configure(date: date, temperature: temperature, iconCode: iconCode)
    		...
    }
  • ์œ„์™€ ๊ฐ™์€ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์ด์ฒ˜๋Ÿผ static ํ•œ DateFormatter ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑ ํ•ด ๋†“๊ณ , ํ•ด๋‹น ์ธ์Šคํ„ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” static func ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ Util์„ ๋งŒ๋“ฆ.

    ์„œ๋ฒ„์—์„œ ๋‚ด๋ ค์˜ค๋Š” ๋ฐ์ดํ„ฐ ์ค‘ Int ๊ฐ’์ธ timestamp ๊ฐ€ ์กด์žฌํ•˜๊ธฐ์—, ๋ฐ”๋กœ Date ๋กœ ๋ณ€ํ™˜์ด ๊ฐ€๋Šฅํ–ˆ๋‹ค.

    • ์•ฑ์ด ์ข…๋ฃŒ๋˜๊ธฐ ์ „๊นŒ์ง€ ๊ณ„์†ํ•ด์„œ Cell์„ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋Š” ์„ฑ๊ฒฉ์ด๊ธฐ์— static์ด ์ ์ ˆํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Œ.
    enum DateFormatUtil {
        private static let dateFormatter: DateFormatter = {
            let formatter = DateFormatter()
            formatter.dateFormat = "MM/dd(EEE) HH์‹œ"
            return formatter
        }()
        
        static func format(with timestamp: Int) -> String {
            let date = Date(timeIntervalSince1970: TimeInterval(timestamp))
    
            let formattedDate = dateFormatter.string(from: date)
            return formattedDate
        }
    }
  • ์ด ๋•Œ static ํ•œ dateFormatter ์ธ์Šคํ„ด์Šค ๊ณ„์†ํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์ด๋ฏ€๋กœ Thread Safety๊ฐ€ ๊ฑฑ์ •๋˜๊ธด ํ–ˆ์ง€๋งŒ, ๊ณต์‹๋ฌธ์„œ์˜ DateFormatter๋Š” Thread Safe ํ•˜๋‹ค๋Š” ๋ฌธ๊ตฌ๋ฅผ ๋ณด๊ณ  ํ•ด๋‹น ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ด๋„ ๋ฌธ์ œ์—†๋Š” ๋” ํšจ์œจ์ ์ธ ๋กœ์ง์ด๋ผ ํŒ๋‹จ

    ๊ณต์‹๋ฌธ์„œ: On iOS 7 and laterย NSDateFormatterย is thread safe.

๐ŸŽจย HeaderView๋งŒ ๋”ฐ๋กœ ์—…๋ฐ์ดํŠธํ•˜๊ฒŒ๋” ๊ตฌํ˜„

  • ์ปฌ๋ ‰์…˜๋ทฐ์˜ ํ—ค๋”๋ทฐ์—์„œ ํ•„์š”๋กœ ํ•˜๋Š” API ๋ฐ์ดํ„ฐ๋Š” ํ˜„์žฌ ๋‚ ์”จ ๋ฐ์ดํ„ฐ์ด๊ณ , ์…€์—์„œ ํ•„์š”๋กœ ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ์ฃผ๊ฐ„ ๋‚ ์”จ ๋ฐ์ดํ„ฐ์—ฌ์„œ ๊ฐ๊ฐ์˜ API ์š”์ฒญ์ด ๋ณ„๋„๋กœ ์ด๋ฃจ์–ด์ง

  • ์ฒ˜์Œ์—๋Š” ํ—ค๋”๋ทฐ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•˜๊ธฐ ์œ„ํ•ด์„œ CollectionView์˜ reloadData ํ•ด์ฃผ์—ˆ์ง€๋งŒ, ํ—ค๋”๋ทฐ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•˜๊ธฐ ์œ„ํ•ด ์ „์ฒด ์…€๋“ค์„ ์—…๋ฐ์ดํŠธ ํ•˜๋Š” ๊ฒƒ์€ ๋น„ํšจ์œจ์ ์ด๋ž€ ์ƒ๊ฐ์ด ๋“ค์–ด ํ—ค๋”๋ทฐ๋งŒ ์—…๋ฐ์ดํŠธ ํ•˜๋„๋ก ๋ฆฌํŒฉํ„ฐ๋ง

    guard let headerView = self.collectionView.visibleSupplementaryViews(
        ofKind: UICollectionView.elementKindSectionHeader
    ).first as? WeatherHeaderView else { return }
    
    headerView.configure(
        with: currentWeather.weatherDetail,
        address: "\\(placemark.locality ?? "") \\(placemark.name ?? "")",
        icon: image
    )

๐ŸŒ ๋‘ ๊ฐœ์˜ ๋„คํŠธ์›Œํ‚น ํ†ต์‹ ์ด ์™„๋ฃŒ๋˜๋Š” ์‹œ์  ํŒŒ์•…

  • โ€œํ˜„์žฌ ๋‚ ์”จโ€ ์™€ โ€œ๋‚ ์”จ ์˜ˆ๋ณดโ€ ๊ฐ€ ๋ชจ๋‘ ์™„๋ฃŒ๋œ ํ›„ Refresh Control ์„ endRefreshing ์ฒ˜๋ฆฌํ•ด์ฃผ๊ธฐ ์œ„ํ•ด, DispatchGroup ์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„
  • ๊ฐ๊ฐ์˜ ๋‚ ์”จ ์š”์ฒญ์ด ์‹œ์ž‘๋  ๋•Œ enter(), ๋๋‚ฌ์„ ๋•Œ leave()๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ์™„๋ฃŒ๋œ ์‹œ์ ์— notify()์—์„œ endRefreshing ์ด ํ˜ธ์ถœ๋˜๋„๋ก ์ฒ˜๋ฆฌ

๐ŸŒ API Endpoint, Generic ๋ฅผ ํ†ตํ•œ ํ™•์žฅ์„ฑ ์žˆ๋Š” ๋„คํŠธ์›Œํ‚น ๊ตฌํ˜„

  • ํ”„๋กœ์ ํŠธ ์Šคํ…์ด ์ง„ํ–‰๋˜๋ฉฐ icon ์ด๋ฏธ์ง€ API ์š”์ฒญ์ด ์ถ”๊ฐ€๋˜๋Š” ๋“ฑ, ์š”๊ตฌ์‚ฌํ•ญ์ด ๋ณ€๊ฒฝ๋˜๋ฉฐ ๋งค๋ฒˆ
  • enum๊ณผ ์—ฐ๊ด€๊ฐ’์„ ํ™œ์šฉํ•˜์—ฌ,
    • ํ˜ธ์ถœํ•˜๋Š” repository์—์„œ
enum OpenWeatherAPIEndpoints {
    case weather(coordinate: Coordinate)
    case forecast(coordinate: Coordinate)
    case iconImage(id: String)
}

extension OpenWeatherAPIEndpoints {
    var endpoint: Endpoint {
        switch self {
        case .weather(let coordinate):
            return Endpoint(baseURL: "https://api.openweathermap.org",
                            path: "/data/2.5/weather",
                            queryItems: generateQueryItems(coordinate: coordinate))
		...
		var urlRequest: URLRequest? { 
				// Endpoint ๋ฅผ URLRequest ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋กœ์ง
		}
		...
struct Endpoint {
    let baseURL: String
    let path: String
    let queryItems: [URLQueryItem]?
    let httpMethod: HTTPMethodType
}
  • ๋˜ํ•œ ์ œ๋„ค๋ฆญ์„ ํ™œ์šฉ ํ•ด, fetchForecast, fetchWeather ์˜ ๋ฉ”์„œ๋“œ๋ฅผ ํ•˜๋‚˜๋กœ ์ฒ˜๋ฆฌ
func fetchData<T: Decodable>(type: T.Type,
                             endpoint: OpenWeatherAPIEndpoints) async throws -> T {
    guard let urlRequest = endpoint.urlRequest else { throw NetworkError.invalidURL }

    let data = try await service.performRequest(with: urlRequest)
    let decodedData = try self.deserializer.deserialize(T.self, data: data)
    return decodedData
}

STEP4

๐Ÿšค Swift Concurrency ์ ์šฉ (๋ฆฌํŒฉํ† ๋ง)

  • ๊ธฐ์กด์˜ Completion Handler ๋ฐฉ์‹์˜ URLSessionDataTask ๋„คํŠธ์›Œํ‚น ์š”์ฒญ์€ ์™„๋ฃŒ ์‹œ์ ์„ ์ฝ”๋“œ๋กœ ์ง๊ด€์ ์œผ๋กœ ํŒŒ์•…ํ•˜๊ธฐ ์–ด๋ ต๊ณ , ์ค‘์ฒฉ๋œ ํด๋กœ์ € ๊ตฌ๋ฌธ์ด ๋ฐœ์ƒํ•œ๋‹ค๋Š” ๋‹จ์ ์ด ์กด์žฌ
  • ํŠนํžˆ, ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ โ€๋‚ ์”จ API ์š”์ฒญ โ†’ ์™„๋ฃŒ ์‹œ์  โ†’ ๋‚ ์”จ ์•„์ด์ฝ˜ API ์š”์ฒญ โ†’ ์™„๋ฃŒโ€ ์™€ ๊ฐ™์ด ๋‘ ๊ฐœ์˜ ๋„คํŠธ์›Œํ‚น ์š”์ฒญ์„ ์ข…์†์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•จ.

๋ฆฌํŒฉํ„ฐ๋ง ์ „)

final class NetworkService {
		func performRequest(with url: URL?,
                        httpMethodType: HTTPMethodType,
                        completion: @escaping (Result<Data, NetworkError>) -> Void) {
        guard let url else {
            completion(.failure(.invalidURL))
            log(.network, error: NetworkError.invalidURL)
            return
        }
        
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = httpMethodType.rawValue
        
        let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
            guard error == nil else {
                completion(.failure(.networking))
                log(.network, error: NetworkError.networking)
                return
            }

            guard let httpResponse = response as? HTTPURLResponse,
                  (200...299).contains(httpResponse.statusCode) else {
                completion(.failure(.response))
                log(.network, error: NetworkError.response)
                return
            }

            guard let data else {
                completion(.failure(.invalidData))
                log(.network, error: NetworkError.invalidData)
                return
            }

            completion(.success(data))
        }
        task.resume()
    }
}
// (Service ->) Repository๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๋Š” ViewController ๋‚ด๋ถ€
private func fetchWeather(coordinate: Coordinate) {
		// 1๏ธโƒฃ API ์—์„œ ๋‚ ์”จ ๋ฐ์ดํ„ฐ๋ฅผ ๋จผ์ € ๋ฐ›์•„์˜ด
    repository.fetchWeather(coordinate: coordinate) { currentWeather in
					// 2๏ธโƒฃ ๋ฐ›์•„์˜จ ํ•ด๋‹น ๋‚ ์”จ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ, API์—์„œ ๋‚ ์”จ ์•„์ด์ฝ˜ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ด
					// ๐Ÿ˜ต ์ค‘์ฒฉ ํด๋กœ์ € ๋ฐœ์ƒ..!
			    repository.fetchWeatherIconImage(withID: currentWeather.weathers.first?.icon ?? "") { iconImage in
								// ๋ฐ›์•„์˜จ iconImage๋กœ View ์—…๋ฐ์ดํŠธ
					}
		}	
}
  • ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด Swift Concurrency ์˜ async-await ๋ฐฉ์‹์œผ๋กœ ๋ฆฌํŒฉํ„ฐ๋ง ์ง„ํ–‰
    • ๋ฆฌํŒฉํ„ฐ๋ง ํ›„ ๊ฐ ์š”์ฒญ๊ฐ„์˜ ์ˆœ์„œ์™€ ์ฝ”๋“œ์˜ ํ๋ฆ„์ด ์‰ฝ๊ฒŒ ํŒŒ์•…๋จ

๋ฆฌํŒฉํ„ฐ๋ง ํ›„)

final class OpenWeatherRepository {
...
		func fetchData<T: Decodable>(type: T.Type,
                                 endpoint: OpenWeatherAPIEndpoints) async throws -> T {
        guard let urlRequest = endpoint.urlRequest else { throw NetworkError.invalidURL }

        let data = try await service.performRequest(with: urlRequest)
        let decodedData = try self.deserializer.deserialize(T.self, data: data)
        return decodedData
    }
...
}

final class NetworkService {
    func performRequest(with urlRequest: URLRequest?) async throws -> Data {
        guard let urlRequest else {
            log(.network, error: NetworkError.invalidURL)
            throw NetworkError.invalidURL
        }

        let (data, response) = try await URLSession.shared.data(for: urlRequest)

        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            log(.network, error: NetworkError.response)
            throw NetworkError.response
        }
        return data
    }
}
// (Service ->) Repository๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๋Š” ViewController ๋‚ด๋ถ€
private func fetchWeather(coordinate: Coordinate) {
	  Task {
				// 1๏ธโƒฃ API ์—์„œ ๋‚ ์”จ ๋ฐ์ดํ„ฐ๋ฅผ ๋จผ์ € ๋ฐ›์•„์˜ด
	      let currentWeather = try await repository.fetchData(type: CurrentWeather.self,
	                                                          endpoint: .weather(coordinate: coordinate))
				// 2๏ธโƒฃ ๋ฐ›์•„์˜จ ํ•ด๋‹น ๋‚ ์”จ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ, API์—์„œ ๋‚ ์”จ ์•„์ด์ฝ˜ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ด
				let iconImage = try await repository.fetchWeatherIconImage(withID: currentWeather.weathers.first?.icon ?? "")	

				// ๋ฐ›์•„์˜จ iconImage ํ™œ์šฉ! ์ฝ”๋“œ์˜ ํ๋ฆ„์ด ๊น”๋”ํ•ด์ง.
	  }
}

๋‚ด์šฉ ์ถ”๊ฐ€ ์—…๋ฐ์ดํŠธ ์˜ˆ์ •

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages