✍🏻 프로젝트 기간: 23.03.13
~ 23.04.14
Step 1 - 모델/네트워킹 타입 구현
Step 2 - 위치정보 확인 및 날씨 API 호출
Step 3 - UI 구현
Step 4 - 수동 위치 설정 기능 추가
Step 5 - 화면회전 및 꺾은선 그래프
실행결과
- Open Weather Map에서 제공하는 날씨 API의 데이터 형식을 고려하여 모델 타입으로 구현
- API를 통해 전달받은 JSON 데이터를 활용할 수 있는 모델 타입으로 구현
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
}
}
.gitignore
파일에 *.xcconfig
을 추적하지 못하도록 설정
- 현재 위치의 위도와 경도 확인
- 위도와 경도를 활용해 현재 위치의 주소 확인
- 현재 위치의 날씨와 현재 위치의 5일 예보를 날씨 API를 통해 데이터를 요청하고 받아오는 기능을 구현
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
}
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
}
}
위 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()
- CollectionView의 구성요소를
CollectionLayoutListConfiguration
에서 ReusableView와 ListCell로 구현 - CompositionalLayout을 활용하여 Collection View를 코드로 구현
- View와 Model간의
Data Binding
은 Delegate를 사용하여 구현
CollectionView가 처음 나왔을 때 같이 공개되었던 가장 기본이 되는 DataSource로서 CollectionView에 대한 Cell을 제공하는데 사용하는 개체로 ViewController에 채택하여 사용하였다.
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)
}
기존에는 CompositionalLayout을 사용하면 Section -> Group Size -> Group -> Item Size -> Item 순으로 Section의 속성을 정의하여
Cell의 UI Components들을 사용하였는데 날씨앱에서는 TableViewCell처럼 사용하기 때문에 ListCell을 사용해보려고 하였다.
처음에는 Section을 만들어 UICollectionViewCell을 사용하는 방법으로 진행하였는데 기존의 CustomCell을 ListCell로 Configuration을 바꿔서 사용하였다.
CollectionLayoutListConfiguration
의 적용된 제약 조건이 아닌 Custom으로 만든 View의 제약 조건으로 변경되기 위해 priority를 주어 적용시켰다.
extension NSLayoutConstraint {
func withPriority(_ priority: UILayoutPriority) -> Self {
self.priority = priority
return self
}
}