---
# System prepended metadata

title: ios-weather-manager

---

# ios-weather-manager

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

---
# 🪧 Agenda

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

---

---
# 🚀 Step 1 - 모델/네트워킹 타입 구현
- Open Weather Map에서 제공하는 날씨 API의 데이터 형식을 고려하여 모델 타입으로 구현
- API를 통해 전달받은 JSON 데이터를 활용할 수 있는 모델 타입으로 구현
## 🎯 API 사용 관련 스터디
### API 사용을 위해 API KEY 발급 및 쿼리 사용에 대한 숙지
<img src="https://i.imgur.com/tc1rM4Z.png" width="60%">

### Postman을 활용하여 실제 데이터를 확인
<img src="https://i.imgur.com/5vFqAwZ.png">

### JSON형식의 데이터를 Model로 변환
```swift=
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을 만들도록 하였다.
```swift=
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`프로토콜 타입으로 묶어서 사용하였다.

```Swift=
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 공식문서에 의거하였다.
```Swift=
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:)` 메서드를 사용하여 구현하였다.

```swift=
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 충돌 문제
<img src="https://user-images.githubusercontent.com/79438622/231670384-adabd34d-25ce-4321-983d-64f8db6a4e97.png">

 `CollectionLayoutListConfiguration`의 적용된 제약 조건이 아닌 Custom으로 만든 View의 제약 조건으로 변경되기 위해 priority를 주어 적용시켰다.
 
```swift=
extension NSLayoutConstraint {
    func withPriority(_ priority: UILayoutPriority) -> Self {
        self.priority = priority
        return self
    }
}
```