ohdair / ios-weather-forecast

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ios-weather-manager

✍🏻 프로젝트 기간: 23.03.13 ~ 23.04.14


🪧 Agenda

Step 1 - 모델/네트워킹 타입 구현
Step 2 - 위치정보 확인 및 날씨 API 호출
Step 3 - UI 구현
Step 4 - 수동 위치 설정 기능 추가
Step 5 - 화면회전 및 꺾은선 그래프
실행결과



🚀 Step 1 - 모델/네트워킹 타입 구현

  • Open Weather Map에서 제공하는 날씨 API의 데이터 형식을 고려하여 모델 타입으로 구현
  • API를 통해 전달받은 JSON 데이터를 활용할 수 있는 모델 타입으로 구현

🎯 API 사용 관련 스터디

API 사용을 위해 API KEY 발급 및 쿼리 사용에 대한 숙지

Postman을 활용하여 실제 데이터를 확인

JSON형식의 데이터를 Model로 변환

struct CurrentWeather: WeatherModel {
    let coordinator: Coordinate?
    let weathers: [Weather]
    let main: Main
    let visibility: Double?
    let wind: Wind?
    let clouds: Clouds?
    let rain: Rain?
    let snow: Snow?
    let timeOfDataCalculation: Double?
    let weatherSystem: WeatherSystem?
    let timezone: Int?
    let id: Int?
    let name: String?
    
    enum CodingKeys: String, CodingKey {
        case coordinator = "coord"
        case weathers = "weather"
        case main
        case visibility
        case wind
        case clouds
        case rain
        case snow
        case timeOfDataCalculation = "dt"
        case weatherSystem = "sys"
        case timezone
        case id
        case name
    }
}

APIKEY를 숨기기 위해 xcconfig 형식의 파일에 APIKEY를 저장

.gitignore 파일에 *.xcconfig 을 추적하지 못하도록 설정



🚀 Step 2 - 위치정보 확인 및 날씨 API 호출

  • 현재 위치의 위도와 경도 확인
  • 위도와 경도를 활용해 현재 위치의 주소 확인
  • 현재 위치의 날씨와 현재 위치의 5일 예보를 날씨 API를 통해 데이터를 요청하고 받아오는 기능을 구현

URLSession을 사용하여 JSON 데이터 받아오기

1️⃣ URLComponents

Open API로 HTTPMethod를 GET요청을 해야할 URL이 위치가 변경될 때마다 새로운 URL이 필요하게 되어 URLComponent를 사용하여 Path와 Query를 이용하여 URL을 만들도록 하였다.

static func configureURL(of weatherCastType: URLPath,
                         with coordintate: CLLocationCoordinate2D) throws -> URL {
    let baseURL: String = "https://api.openweathermap.org/data/2.5/"
    let latitude = URLQueryItem(name: OpenWeatherParameter.latitude, value: coordintate.latitude.description)
    let longitude = URLQueryItem(name: OpenWeatherParameter.longitude, value: coordintate.longitude.description)
    let unitsOfMeasurement = URLQueryItem(name: OpenWeatherParameter.measurement, value: Measurement.celsius)

    guard let weatherAPIKEY = Bundle.main.object(forInfoDictionaryKey: "WeatherAPIKEY") as? String else {
        fatalError("Weather API KEY is E.M.P.T.Y !!")
    }

    guard var components = URLComponents(string: "\(baseURL)\(weatherCastType.path)") else {
        throw URLComponentsError.invalidComponent
    }

    let appid = URLQueryItem(name: OpenWeatherParameter.apiKey, value: weatherAPIKEY)
    components.queryItems = [latitude, longitude, appid, unitsOfMeasurement]

    guard let url = components.url else {
        throw URLComponentsError.invalidComponent
    }

    return url
}

2️⃣ JSON Decoder

URLRequest를 통해 받아오는 데이터는 타입이 Data(Byte)로 나오기 떄문에 프로젝트에서 직접 사용해야 하는 타입으로 Decoding이 필요한 부분을 메서드로 분리하였다.
추가적으로 프로젝트에서 Forecast(5일 예보)와 CurrentWeather(현재 날씨) 타입 모두를 받을 수 있도록 WeatherModel프로토콜 타입으로 묶어서 사용하였다.

private func loadJSON(weatherType: WeatherModel.Type, weatherData: Data) -> WeatherModel? {
    let decoder = JSONDecoder()

    do {
        let wishData = try decoder.decode(weatherType, from: weatherData)
        return wishData
    } catch {
        print("Unable to decode \(weatherData): (error)")
        return nil
    }
}

3️⃣ URL Request

configureURL(of: with:) 메서드를 통해 나온 URL을 URLRequest메서드에서 사용할 수 있다.
HTTP Response Error에 대한 핸들링은 200에서 299번대의 Error에 대해서만 진행하였고 Apple 공식문서에 의거하였다.

let url = try URLPath.configureURL(of: path, with: location)
var urlRequest = URLRequest(url: url)

urlRequest.httpMethod = "GET"
session.dataTask(with: urlRequest) { data, response, error in
    guard error == nil else {
        completion(nil, NetworkError.notConnected)
        return
    }

    guard let httpResponse = response as? HTTPURLResponse,
          (200...299).contains(httpResponse.statusCode) else {
        let responseError = HTTPResponseError.error(statusCode: (response as? HTTPURLResponse)?.statusCode ?? 404,
                                                    description: response.debugDescription)
        completion(nil, responseError)
        return
    }

    guard let data = data else {
        completion(nil, NetworkEntityLoadingError.networkFailure)
        return
    }

    guard let wishData = self.loadJSON(weatherType: path.weatherMetaType, weatherData: data) else {
        completion(nil, NetworkEntityLoadingError.invalidData)
        return
    }
    completion(wishData, nil)
}.resume()


🚀 Step 3 - UI 구현

  • CollectionView의 구성요소를 CollectionLayoutListConfiguration에서 ReusableView와 ListCell로 구현
  • CompositionalLayout을 활용하여 Collection View를 코드로 구현
  • View와 Model간의 Data Binding은 Delegate를 사용하여 구현

🎯 Collection View 관련 스터디

CollectionView DataSource

CollectionView가 처음 나왔을 때 같이 공개되었던 가장 기본이 되는 DataSource로서 CollectionView에 대한 Cell을 제공하는데 사용하는 개체로 ViewController에 채택하여 사용하였다.

CollectionLayoutListConfiguration을 활용한 Layout 설정

iOS 14 이후에 생긴 CollectionLayoutListConfiguration를 활용하여 collectionView에 대한 구성요소를 grouped으로 설정 및 headerView를 등록(Regist)하였고
CompositionalLayout에서 UICollectionViewListCell으로 사용할 수 있는 list(using:) 메서드를 사용하여 구현하였다.

private func configureCollectionView() -> UICollectionViewLayout {
    var configuration = UICollectionLayoutListConfiguration(appearance: .grouped)
    configuration.headerMode = .supplementary
    configuration.backgroundColor = UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 0.2)
    return UICollectionViewCompositionalLayout.list(using: configuration)
}

🧨 트러블 슈팅

ListCell 등록하기

기존에는 CompositionalLayout을 사용하면 Section -> Group Size -> Group -> Item Size -> Item 순으로 Section의 속성을 정의하여
Cell의 UI Components들을 사용하였는데 날씨앱에서는 TableViewCell처럼 사용하기 때문에 ListCell을 사용해보려고 하였다.
처음에는 Section을 만들어 UICollectionViewCell을 사용하는 방법으로 진행하였는데 기존의 CustomCell을 ListCell로 Configuration을 바꿔서 사용하였다.

AutoLayout 충돌 문제

CollectionLayoutListConfiguration의 적용된 제약 조건이 아닌 Custom으로 만든 View의 제약 조건으로 변경되기 위해 priority를 주어 적용시켰다.

extension NSLayoutConstraint {
    func withPriority(_ priority: UILayoutPriority) -> Self {
        self.priority = priority
        return self
    }
}

About


Languages

Language:Swift 100.0%