Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

날씨앱 [STEP 1] kun #15

Open
wants to merge 4 commits into
base: rft_1_kun11
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 187 additions & 21 deletions WeatherForecast/WeatherForecast.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

51 changes: 0 additions & 51 deletions WeatherForecast/WeatherForecast/Base.lproj/Main.storyboard

This file was deleted.

2 changes: 0 additions & 2 deletions WeatherForecast/WeatherForecast/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
Expand Down
30 changes: 30 additions & 0 deletions WeatherForecast/WeatherForecast/Model/TempUnit.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// TempUnit.swift
// WeatherForecast
//
// Created by MIN SEONG KIM on 2024/02/02.
//

import Foundation

// MARK: - Temperature Unit
enum TempUnit: String {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rawValue를 사용하는 곳은 없어보이는데, String으로 선언하신 이유�는 무엇일까요?

case metric
case imperial

var expression: String {
switch self {
case .metric: return "℃"
case .imperial: return "℉"
}
}

var strOpposite: String {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 변수명만 보고 이 변수가 어떤 값을 반환하는지 추측하기 어려워보여요

switch self {
case .metric:
return "화씨"
case .imperial:
return "섭씨"
}
}
}
40 changes: 40 additions & 0 deletions WeatherForecast/WeatherForecast/Model/WeatherDetailInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// WeatherInfo.swift
// WeatherForecast
//
// Created by MIN SEONG KIM on 2024/02/05.
//

import Foundation

struct WeatherDetailInfo {
private let weatherForecastInfo: WeatherForecastInfo
private let cityInfo: City
private let tempUnit: TempUnit

var date: String { weatherForecastInfo.date }

var sunrise: String { cityInfo.sunriseString }
var sunset: String { cityInfo.sunsetString }

var weatherMain: String { weatherForecastInfo.weatherMain }
var description: String { weatherForecastInfo.description }

var temp: String { "\(weatherForecastInfo.temp)\(tempUnit.expression)"}
var tempMax: String { "\(weatherForecastInfo.tempMax)\(tempUnit.expression)" }
var tempMin: String { "\(weatherForecastInfo.tempMin)\(tempUnit.expression)" }

var feelsLike: String { "\(weatherForecastInfo.feelsLike)\(tempUnit.expression)" }
var pop: String { weatherForecastInfo.pop }

var humidity: String { weatherForecastInfo.humidity }

var iconName: String { weatherForecastInfo.iconName }
Comment on lines +15 to +32
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 computed property들을 만드신 이유가 궁금해요.

Clean-Architecture에서는 서버에서 받은 모델을 뷰에서 사용할 모델로 새로 만드는 역할이 있는데요, DTO, Entity키워드로 검색해보시면 좋을 거 같고,

해당 레이어가 빠진다고 하더라도, weatherForecastInfo를 internal로 열어서 직접 접근하는게, computed property를 사용하는것보다 오버헤드가 적을거 같다는 생각이 드네요


//MARK: - Init
init(weatherForecastInfo: WeatherForecastInfo, cityInfo: City, tempUnit: TempUnit) {
self.weatherForecastInfo = weatherForecastInfo
self.cityInfo = cityInfo
self.tempUnit = tempUnit
}
}
95 changes: 95 additions & 0 deletions WeatherForecast/WeatherForecast/Network/DTO/Weather.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//
// WeatherForecast - Weather.swift
// Created by yagom.
// Copyright © yagom. All rights reserved.
//

import Foundation

// MARK: - Weather JSON Format
struct WeatherJSON: Decodable {
let weatherForecast: [WeatherForecastInfo]
let city: City
}

// MARK: - List
struct WeatherForecastInfo: Decodable {
let dt: TimeInterval
let main: MainInfo
let weather: Weather
let dtTxt: String

var date: String {
let formatter: DateFormatter = DateFormatter()
formatter.locale = .init(identifier: "ko_KR")
formatter.dateFormat = "yyyy-MM-dd(EEEEE) a HH:mm"

return formatter.string(from: Date(timeIntervalSince1970: dt))
}

var weatherMain: String { weather.main }
var description: String { weather.description }

var temp: Double { main.temp }
var tempMax: Double { main.tempMax }
var tempMin: Double { main.tempMin }
var feelsLike: Double { main.feelsLike }

var pop: String { "\(main.pop * 100)%" }
var humidity: String { "\(main.humidity)%" }

var iconName: String { weather.icon }
}

// MARK: - MainClass
struct MainInfo: Decodable {
let temp: Double
let feelsLike: Double
let tempMin: Double
let tempMax: Double
let pressure: Double
let seaLevel: Double
let grndLevel: Double
let humidity: Double
let pop: Double
}

// MARK: - Weather
struct Weather: Decodable {
let id: Int
let main: String
let description: String
let icon: String
}

// MARK: - City
struct City: Decodable {
let id: Int
let name: String
let coord: Coord
let country: String
let population, timezone: Int
let sunrise, sunset: TimeInterval

var sunriseString: String {
let formatter: DateFormatter = DateFormatter()
formatter.dateFormat = .none
formatter.timeStyle = .short
formatter.locale = .init(identifier: "ko_KR")
return formatter.string(from: Date(timeIntervalSince1970: sunrise))
}

var sunsetString: String {
let formatter: DateFormatter = DateFormatter()
formatter.dateFormat = .none
formatter.timeStyle = .short
formatter.locale = .init(identifier: "ko_KR")
return formatter.string(from: Date(timeIntervalSince1970: sunset))
}
}

// MARK: - Coord
struct Coord: Decodable {
let lat: Double
let lon: Double
}
13 changes: 13 additions & 0 deletions WeatherForecast/WeatherForecast/Network/Error/JsonError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// NetworkError.swift
// WeatherForecast
//
// Created by MIN SEONG KIM on 2024/02/02.
//

import Foundation

enum JsonError: Error {
case emptyData // 데이터 미존재
case failDecode // 디코드 실패
}
Comment on lines +10 to +13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CustomStringConvertible 프로토콜로 주석에 있는 설명을 description으로 옮겨보는건 어떨까요?

14 changes: 14 additions & 0 deletions WeatherForecast/WeatherForecast/Network/Error/NetworkError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// ImageServiceError.swift
// WeatherForecast
//
// Created by MIN SEONG KIM on 2024/02/02.
//

import Foundation

enum NetworkError: Error {
case invalidUrl // url 에러
case networkFail // 응답상태코드 에러
case invalidData // 유효하지 않은 데이터
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// NetworkService.swift
// WeatherForecast
//
// Created by MIN SEONG KIM on 2024/02/02.
//

import Foundation

protocol JsonService {
func fetchWeather() async -> Result<WeatherJSON, JsonError>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// ImageService.swift
// WeatherForecast
//
// Created by MIN SEONG KIM on 2024/02/02.
//

import UIKit

protocol NetworkService {
func fetchImage(iconName: String,
urlSession: URLSession) async throws -> UIImage
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// TempUnitManagerService.swift
// WeatherForecast
//
// Created by MIN SEONG KIM on 2024/02/06.
//

import Foundation

protocol TempUnitManagerService {
var tempUnit: TempUnit { get }

func update()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// TempUnitManager.swift
// WeatherForecast
//
// Created by MIN SEONG KIM on 2024/02/06.
//

import Foundation

final class TempUnitManager: TempUnitManagerService {
var tempUnit: TempUnit = .metric

func update() {
switch tempUnit {
case .metric:
tempUnit = .imperial
case .imperial:
tempUnit = .metric
}
}


}
Comment on lines +10 to +23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음.. 이 부분은 tempUnit을 바꾸기 위해 프로토콜과 class까지 생성되어야 하는 의문이 들어요!
미래를 위해 여러가지 복잡한 온도 관리 로직이 생긴다면 필요할 것 같지만,
단순히 tempUnit만을 위한거라면 요런 걸 만들어보는건 어떨까요?

    mutating func toggle() {
        switch self {
        case .metric:
            self = .imperial
        case .imperial:
            self = .metric
        }
    }

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// WeatherIconImageService.swift
// WeatherForecast
//
// Created by MIN SEONG KIM on 2024/02/02.
//

import UIKit

final class WeatherIconImageService: NetworkService {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

캐시를 관리하는 객체와 이미지를 다운받는 객체를 분리하는건 어떨까요?

private let imageChache: NSCache<NSString, UIImage> = NSCache()

func fetchImage(iconName: String,
urlSession: URLSession) async throws -> UIImage {
let urlString: String = "https://openweathermap.org/img/wn/\(iconName)@2x.png"

guard let url: URL = URL(string: urlString) else {
throw NetworkError.invalidUrl
}

let (data, response) = try await urlSession.data(from: url)
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
throw NetworkError.networkFail
}

guard let image: UIImage = UIImage(data: data) else {
throw NetworkError.invalidData
}

imageChache.setObject(image, forKey: urlString as NSString)

return image
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// NetworkService.swift
// WeatherForecast
//
// Created by MIN SEONG KIM on 2024/02/02.
//

import UIKit

final class WeatherJsonService: JsonService {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. JsonService의 경우 WeatherJSON, JsonError에만 특정된 fetch를 할 수 있을거 같아요. 다만 JsonService를 선언하신 목적은 json에서 무언가를 파싱하는 기능을 선언하신 거 같고, 확장성을 가지려면 제네릭한 Return Type을 가지는게 좋아보여요
  2. Asset에서 json을 가져오는 행동은 동기로 일어나는데, async인 이유는 무엇일까요?
  3. throws함수로 만들 수도 있을 거 같은데, 굳이 Result로 변환하시는 이유는 무엇일까요?
  4. JSONDecoder의 경우 외부에서 주입받는것도 확장성을 위해 고려할 수 있을 것 같아요

func fetchWeather() async -> Result<WeatherJSON, JsonError> {
let jsonDecoder: JSONDecoder = .init()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase

guard let data = NSDataAsset(name: "weather")?.data else {
return .failure(.emptyData)
}

let info: WeatherJSON
do {
info = try jsonDecoder.decode(WeatherJSON.self, from: data)
return .success(info)
} catch {
return .failure(.failDecode)
}
}
}
Loading