From 20210af1af3c0f97f158f02be39a1e742b85566e Mon Sep 17 00:00:00 2001 From: Song Jin Young Date: Thu, 25 Apr 2024 17:14:54 +0900 Subject: [PATCH 1/2] STEP 1 --- .../WeatherForecast.xcodeproj/project.pbxproj | 8 +- .../Base.lproj/Main.storyboard | 8 +- WeatherForecast/WeatherForecast/Weather.swift | 23 +++- .../WeatherDetailViewController.swift | 116 ++++++++-------- ....swift => WeatherListViewController.swift} | 130 ++++++++++-------- .../WeatherTableViewCell.swift | 84 ++++++----- 6 files changed, 213 insertions(+), 156 deletions(-) rename WeatherForecast/WeatherForecast/{ViewController.swift => WeatherListViewController.swift} (67%) diff --git a/WeatherForecast/WeatherForecast.xcodeproj/project.pbxproj b/WeatherForecast/WeatherForecast.xcodeproj/project.pbxproj index 9b2f170..0ebbcce 100644 --- a/WeatherForecast/WeatherForecast.xcodeproj/project.pbxproj +++ b/WeatherForecast/WeatherForecast.xcodeproj/project.pbxproj @@ -10,7 +10,7 @@ C741F6702B58F00500A4DDC0 /* Weather.swift in Sources */ = {isa = PBXBuildFile; fileRef = C741F66F2B58F00500A4DDC0 /* Weather.swift */; }; C7743D8D2B21C38100DF0D09 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7743D8C2B21C38100DF0D09 /* AppDelegate.swift */; }; C7743D8F2B21C38100DF0D09 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7743D8E2B21C38100DF0D09 /* SceneDelegate.swift */; }; - C7743D912B21C38100DF0D09 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7743D902B21C38100DF0D09 /* ViewController.swift */; }; + C7743D912B21C38100DF0D09 /* WeatherListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7743D902B21C38100DF0D09 /* WeatherListViewController.swift */; }; C7743D942B21C38100DF0D09 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C7743D922B21C38100DF0D09 /* Main.storyboard */; }; C7743D962B21C38200DF0D09 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C7743D952B21C38200DF0D09 /* Assets.xcassets */; }; C7743D992B21C38200DF0D09 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C7743D972B21C38200DF0D09 /* LaunchScreen.storyboard */; }; @@ -23,7 +23,7 @@ C7743D892B21C38100DF0D09 /* WeatherForecast.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WeatherForecast.app; sourceTree = BUILT_PRODUCTS_DIR; }; C7743D8C2B21C38100DF0D09 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C7743D8E2B21C38100DF0D09 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - C7743D902B21C38100DF0D09 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + C7743D902B21C38100DF0D09 /* WeatherListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherListViewController.swift; sourceTree = ""; }; C7743D932B21C38100DF0D09 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; C7743D952B21C38200DF0D09 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C7743D982B21C38200DF0D09 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -65,7 +65,7 @@ C7743D8C2B21C38100DF0D09 /* AppDelegate.swift */, C7743D8E2B21C38100DF0D09 /* SceneDelegate.swift */, C7743DA02B21C3B400DF0D09 /* WeatherTableViewCell.swift */, - C7743D902B21C38100DF0D09 /* ViewController.swift */, + C7743D902B21C38100DF0D09 /* WeatherListViewController.swift */, C741F66F2B58F00500A4DDC0 /* Weather.swift */, C7743DA22B21CA8500DF0D09 /* WeatherDetailViewController.swift */, C7743D922B21C38100DF0D09 /* Main.storyboard */, @@ -148,7 +148,7 @@ buildActionMask = 2147483647; files = ( C7743DA12B21C3B400DF0D09 /* WeatherTableViewCell.swift in Sources */, - C7743D912B21C38100DF0D09 /* ViewController.swift in Sources */, + C7743D912B21C38100DF0D09 /* WeatherListViewController.swift in Sources */, C7743D8D2B21C38100DF0D09 /* AppDelegate.swift in Sources */, C7743DA32B21CA8600DF0D09 /* WeatherDetailViewController.swift in Sources */, C741F6702B58F00500A4DDC0 /* Weather.swift in Sources */, diff --git a/WeatherForecast/WeatherForecast/Base.lproj/Main.storyboard b/WeatherForecast/WeatherForecast/Base.lproj/Main.storyboard index 4798dc7..c5c4e03 100644 --- a/WeatherForecast/WeatherForecast/Base.lproj/Main.storyboard +++ b/WeatherForecast/WeatherForecast/Base.lproj/Main.storyboard @@ -1,17 +1,17 @@ - + - + - + - + diff --git a/WeatherForecast/WeatherForecast/Weather.swift b/WeatherForecast/WeatherForecast/Weather.swift index ede7585..c7835d3 100644 --- a/WeatherForecast/WeatherForecast/Weather.swift +++ b/WeatherForecast/WeatherForecast/Weather.swift @@ -53,10 +53,29 @@ class Coord: Decodable { enum TempUnit: String { case metric, imperial var expression: String { + guard let expression = expressionMapping[self.rawValue] else { return ""} + return expression.rawValue + } + + var desc: String { switch self { - case .metric: return "℃" - case .imperial: return "℉" + case .metric: + return "섭씨" + case .imperial: + return "화씨" } } + + private var expressionMapping: [String: TempExpression] { + return [ + TempUnit.metric.rawValue: .metric, + TempUnit.imperial.rawValue: .imperial + ] + } +} + +enum TempExpression: String { + case metric = "℃" + case imperial = "℉" } diff --git a/WeatherForecast/WeatherForecast/WeatherDetailViewController.swift b/WeatherForecast/WeatherForecast/WeatherDetailViewController.swift index 69d3dfb..1e7db06 100644 --- a/WeatherForecast/WeatherForecast/WeatherDetailViewController.swift +++ b/WeatherForecast/WeatherForecast/WeatherDetailViewController.swift @@ -8,16 +8,24 @@ import UIKit class WeatherDetailViewController: UIViewController { + private let iconImageView: UIImageView = UIImageView() + private let weatherGroupLabel: UILabel = UILabel() + private let weatherDescriptionLabel: UILabel = UILabel() + private let temperatureLabel: UILabel = UILabel() + private let feelsLikeLabel: UILabel = UILabel() + private let maximumTemperatureLable: UILabel = UILabel() + private let minimumTemperatureLable: UILabel = UILabel() + private let popLabel: UILabel = UILabel() + private let humidityLabel: UILabel = UILabel() + private let sunriseTimeLabel: UILabel = UILabel() + private let sunsetTimeLabel: UILabel = UILabel() + private let spacingView: UIView = UIView() + private var mainStackView: UIStackView? + var weatherForecastInfo: WeatherForecastInfo? var cityInfo: City? var tempUnit: TempUnit = .metric - - let dateFormatter: DateFormatter = { - let formatter: DateFormatter = DateFormatter() - formatter.locale = .init(identifier: "ko_KR") - formatter.dateFormat = "yyyy-MM-dd(EEEEE) a HH:mm" - return formatter - }() + private let dateFormatter: DateFormatter = DateFormatter(format: "yyyy-MM-dd(EEEEE) a HH:mm") override func viewDidLoad() { super.viewDidLoad() @@ -26,28 +34,22 @@ class WeatherDetailViewController: UIViewController { private func initialSetUp() { view.backgroundColor = .white - + // 옵셔널 언래핑을 해서 파라미터로 전달하는게 나을지, 아니면 각 메서드 안에서 하는게 나을지 guard let listInfo = weatherForecastInfo else { return } - let date: Date = Date(timeIntervalSince1970: listInfo.dt) + setTitle(listInfo.dt) + setMainStackView() + setSubviewData(listInfo) + setCityInfo() + } + + private func setTitle(_ dt: TimeInterval) { + let date: Date = Date(timeIntervalSince1970: dt) navigationItem.title = dateFormatter.string(from: date) - - let iconImageView: UIImageView = UIImageView() - let weatherGroupLabel: UILabel = UILabel() - let weatherDescriptionLabel: UILabel = UILabel() - let temperatureLabel: UILabel = UILabel() - let feelsLikeLabel: UILabel = UILabel() - let maximumTemperatureLable: UILabel = UILabel() - let minimumTemperatureLable: UILabel = UILabel() - let popLabel: UILabel = UILabel() - let humidityLabel: UILabel = UILabel() - let sunriseTimeLabel: UILabel = UILabel() - let sunsetTimeLabel: UILabel = UILabel() - let spacingView: UIView = UIView() - spacingView.backgroundColor = .clear - spacingView.setContentHuggingPriority(.defaultLow, for: .vertical) - - let mainStackView: UIStackView = .init(arrangedSubviews: [ + } + + private func setMainStackView() { + mainStackView = .init(arrangedSubviews: [ iconImageView, weatherGroupLabel, weatherDescriptionLabel, @@ -61,7 +63,26 @@ class WeatherDetailViewController: UIViewController { sunsetTimeLabel, spacingView ]) - + + setUISubviews() + layMainStackView() + } + + private func setSubviewData(_ listInfo: WeatherForecastInfo) { + weatherGroupLabel.text = listInfo.weather.main + weatherDescriptionLabel.text = listInfo.weather.description + temperatureLabel.text = "현재 기온 : \(listInfo.main.temp)\(tempUnit.expression)" + feelsLikeLabel.text = "체감 기온 : \(listInfo.main.feelsLike)\(tempUnit.expression)" + maximumTemperatureLable.text = "최고 기온 : \(listInfo.main.tempMax)\(tempUnit.expression)" + minimumTemperatureLable.text = "최저 기온 : \(listInfo.main.tempMin)\(tempUnit.expression)" + popLabel.text = "강수 확률 : \(listInfo.main.pop * 100)%" + humidityLabel.text = "습도 : \(listInfo.main.humidity)%" + iconImageView.setImage(from: listInfo.weather.icon) + } + + private func setUISubviews() { + guard let mainStackView = mainStackView else { return } + mainStackView.arrangedSubviews.forEach { subview in guard let subview: UILabel = subview as? UILabel else { return } subview.textColor = .black @@ -73,7 +94,12 @@ class WeatherDetailViewController: UIViewController { weatherGroupLabel.font = .preferredFont(forTextStyle: .largeTitle) weatherDescriptionLabel.font = .preferredFont(forTextStyle: .largeTitle) - + spacingView.backgroundColor = .clear + spacingView.setContentHuggingPriority(.defaultLow, for: .vertical) + } + + private func layMainStackView() { + guard let mainStackView = mainStackView else { return } mainStackView.axis = .vertical mainStackView.alignment = .center mainStackView.spacing = 8 @@ -92,36 +118,14 @@ class WeatherDetailViewController: UIViewController { iconImageView.widthAnchor.constraint(equalTo: safeArea.widthAnchor, multiplier: 0.3) ]) - - weatherGroupLabel.text = listInfo.weather.main - weatherDescriptionLabel.text = listInfo.weather.description - temperatureLabel.text = "현재 기온 : \(listInfo.main.temp)\(tempUnit.expression)" - feelsLikeLabel.text = "체감 기온 : \(listInfo.main.feelsLike)\(tempUnit.expression)" - maximumTemperatureLable.text = "최고 기온 : \(listInfo.main.tempMax)\(tempUnit.expression)" - minimumTemperatureLable.text = "최저 기온 : \(listInfo.main.tempMin)\(tempUnit.expression)" - popLabel.text = "강수 확률 : \(listInfo.main.pop * 100)%" - humidityLabel.text = "습도 : \(listInfo.main.humidity)%" - + } + + private func setCityInfo() { if let cityInfo { - let formatter: DateFormatter = DateFormatter() - formatter.dateFormat = .none + let formatter: DateFormatter = DateFormatter(format: .none) formatter.timeStyle = .short - formatter.locale = .init(identifier: "ko_KR") - sunriseTimeLabel.text = "일출 : \(formatter.string(from: Date(timeIntervalSince1970: cityInfo.sunrise)))" - sunsetTimeLabel.text = "일몰 : \(formatter.string(from: Date(timeIntervalSince1970: cityInfo.sunset)))" - } - - Task { - let iconName: String = listInfo.weather.icon - let urlString: String = "https://openweathermap.org/img/wn/\(iconName)@2x.png" - - guard let url: URL = URL(string: urlString), - let (data, _) = try? await URLSession.shared.data(from: url), - let image: UIImage = UIImage(data: data) else { - return - } - - iconImageView.image = image + sunriseTimeLabel.text = "일출 : \(formatter.string(from: cityInfo.sunrise))" + sunsetTimeLabel.text = "일몰 : \(formatter.string(from: cityInfo.sunset))" } } } diff --git a/WeatherForecast/WeatherForecast/ViewController.swift b/WeatherForecast/WeatherForecast/WeatherListViewController.swift similarity index 67% rename from WeatherForecast/WeatherForecast/ViewController.swift rename to WeatherForecast/WeatherForecast/WeatherListViewController.swift index 50b66fb..56225bc 100644 --- a/WeatherForecast/WeatherForecast/ViewController.swift +++ b/WeatherForecast/WeatherForecast/WeatherListViewController.swift @@ -6,18 +6,18 @@ import UIKit -class ViewController: UIViewController { +final class ImageCache { + static let shared: NSCache = NSCache() + private init() {} +} + +class WeatherListViewController: UIViewController { + var tableView: UITableView! let refreshControl: UIRefreshControl = UIRefreshControl() var weatherJSON: WeatherJSON? var icons: [UIImage]? - let imageChache: NSCache = NSCache() - let dateFormatter: DateFormatter = { - let formatter: DateFormatter = DateFormatter() - formatter.locale = .init(identifier: "ko_KR") - formatter.dateFormat = "yyyy-MM-dd(EEEEE) a HH:mm" - return formatter - }() + let dateFormatter: DateFormatter = DateFormatter(format: "yyyy-MM-dd(EEEEE) a HH:mm") var tempUnit: TempUnit = .metric @@ -27,16 +27,15 @@ class ViewController: UIViewController { } } -extension ViewController { +extension WeatherListViewController { @objc private func changeTempUnit() { switch tempUnit { case .imperial: tempUnit = .metric - navigationItem.rightBarButtonItem?.title = "섭씨" case .metric: tempUnit = .imperial - navigationItem.rightBarButtonItem?.title = "화씨" } + navigationItem.rightBarButtonItem?.title = tempUnit.desc refresh() } @@ -47,18 +46,11 @@ extension ViewController { } private func initialSetUp() { - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "화씨", image: nil, target: self, action: #selector(changeTempUnit)) + navigationItem.rightBarButtonItem = UIBarButtonItem(title: tempUnit.desc, image: nil, target: self, action: #selector(changeTempUnit)) layTable() - - refreshControl.addTarget(self, - action: #selector(refresh), - for: .valueChanged) - - tableView.refreshControl = refreshControl - tableView.register(WeatherTableViewCell.self, forCellReuseIdentifier: "WeatherCell") - tableView.dataSource = self - tableView.delegate = self + setTable() + refresh() } private func layTable() { @@ -75,17 +67,26 @@ extension ViewController { tableView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor) ]) } + + private func setTable() { + refreshControl.addTarget(self, + action: #selector(refresh), + for: .valueChanged) + + tableView.refreshControl = refreshControl + tableView.register(WeatherTableViewCell.self, forCellReuseIdentifier: "WeatherCell") + tableView.dataSource = self + tableView.delegate = self + } } -extension ViewController { +extension WeatherListViewController { private func fetchWeatherJSON() { let jsonDecoder: JSONDecoder = .init() jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase - guard let data = NSDataAsset(name: "weather")?.data else { - return - } + guard let data = NSDataAsset(name: "weather")?.data else { return } let info: WeatherJSON do { @@ -98,9 +99,17 @@ extension ViewController { weatherJSON = info navigationItem.title = weatherJSON?.city.name } + + private func setData(cell: WeatherTableViewCell, data: WeatherForecastInfo) { + cell.weatherLabel.text = data.weather.main + cell.descriptionLabel.text = data.weather.description + cell.temperatureLabel.text = "\(data.main.temp)\(tempUnit.expression)" + cell.dateLabel.text = dateFormatter.string(from: data.dt) + cell.weatherIcon.setImage(from: data.weather.icon) + } } -extension ViewController: UITableViewDataSource { +extension WeatherListViewController: UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { 1 @@ -114,47 +123,19 @@ extension ViewController: UITableViewDataSource { let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "WeatherCell", for: indexPath) guard let cell: WeatherTableViewCell = cell as? WeatherTableViewCell, - let weatherForecastInfo = weatherJSON?.weatherForecast[indexPath.row] else { - return cell - } - - cell.weatherLabel.text = weatherForecastInfo.weather.main - cell.descriptionLabel.text = weatherForecastInfo.weather.description - cell.temperatureLabel.text = "\(weatherForecastInfo.main.temp)\(tempUnit.expression)" - - let date: Date = Date(timeIntervalSince1970: weatherForecastInfo.dt) - cell.dateLabel.text = dateFormatter.string(from: date) - - let iconName: String = weatherForecastInfo.weather.icon - let urlString: String = "https://openweathermap.org/img/wn/\(iconName)@2x.png" - - if let image = imageChache.object(forKey: urlString as NSString) { - cell.weatherIcon.image = image + let weatherForecastInfo: WeatherForecastInfo = weatherJSON?.weatherForecast[indexPath.row] else { return cell } - - Task { - guard let url: URL = URL(string: urlString), - let (data, _) = try? await URLSession.shared.data(from: url), - let image: UIImage = UIImage(data: data) else { - return - } - - imageChache.setObject(image, forKey: urlString as NSString) - - if indexPath == tableView.indexPath(for: cell) { - cell.weatherIcon.image = image - } - } + + setData(cell: cell, data: weatherForecastInfo) return cell } } -extension ViewController: UITableViewDelegate { +extension WeatherListViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - let detailViewController: WeatherDetailViewController = WeatherDetailViewController() detailViewController.weatherForecastInfo = weatherJSON?.weatherForecast[indexPath.row] detailViewController.cityInfo = weatherJSON?.city @@ -163,4 +144,37 @@ extension ViewController: UITableViewDelegate { } } +extension DateFormatter { + convenience init(format: String?) { + self.init() + self.locale = .init(identifier: "ko_KR") + self.dateFormat = format + } + + func string(from timeInterval: TimeInterval) -> String { + let date: Date = Date(timeIntervalSince1970: timeInterval) + return self.string(from: date) + } +} +extension UIImageView { + func setImage(from iconName: String, cache: NSCache? = ImageCache.shared) { + let urlString: String = "https://openweathermap.org/img/wn/\(iconName)@2x.png" + + if let cache = cache, let image = cache.object(forKey: urlString as NSString) { + self.image = image + return + } + + Task { + guard let url: URL = URL(string: urlString), + let (data, _) = try? await URLSession.shared.data(from: url), + let image: UIImage = UIImage(data: data) else { + return + } + + cache?.setObject(image, forKey: urlString as NSString) + self.image = image + } + } +} diff --git a/WeatherForecast/WeatherForecast/WeatherTableViewCell.swift b/WeatherForecast/WeatherForecast/WeatherTableViewCell.swift index 42cb519..3839cc6 100644 --- a/WeatherForecast/WeatherForecast/WeatherTableViewCell.swift +++ b/WeatherForecast/WeatherForecast/WeatherTableViewCell.swift @@ -7,11 +7,15 @@ import UIKit class WeatherTableViewCell: UITableViewCell { - var weatherIcon: UIImageView! - var dateLabel: UILabel! - var temperatureLabel: UILabel! - var weatherLabel: UILabel! - var descriptionLabel: UILabel! + var weatherIcon: UIImageView = UIImageView() + var dateLabel: UILabel = UILabel() + var temperatureLabel: UILabel = UILabel() + var weatherLabel: UILabel = UILabel() + var descriptionLabel: UILabel = UILabel() + var dashLabel: UILabel = UILabel() + var weatherStackView: UIStackView? + var verticalStackView: UIStackView? + var contentsStackView: UIStackView? override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -29,13 +33,14 @@ class WeatherTableViewCell: UITableViewCell { } private func layViews() { - weatherIcon = UIImageView() - dateLabel = UILabel() - temperatureLabel = UILabel() - weatherLabel = UILabel() - let dashLabel: UILabel = UILabel() - descriptionLabel = UILabel() - + layLabels() + setWeatherStackView() + setVerticalStackView() + setContentsStackView() + layContentsStackView() + } + + private func layLabels() { let labels: [UILabel] = [dateLabel, temperatureLabel, weatherLabel, dashLabel, descriptionLabel] labels.forEach { label in @@ -43,8 +48,10 @@ class WeatherTableViewCell: UITableViewCell { label.font = .preferredFont(forTextStyle: .body) label.numberOfLines = 1 } - - let weatherStackView: UIStackView = UIStackView(arrangedSubviews: [ + } + + private func setWeatherStackView() { + weatherStackView = UIStackView(arrangedSubviews: [ weatherLabel, dashLabel, descriptionLabel @@ -53,36 +60,49 @@ class WeatherTableViewCell: UITableViewCell { descriptionLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) - weatherStackView.axis = .horizontal - weatherStackView.spacing = 8 - weatherStackView.alignment = .center - weatherStackView.distribution = .fill - + weatherStackView?.axis = .horizontal + weatherStackView?.spacing = 8 + weatherStackView?.alignment = .center + weatherStackView?.distribution = .fill + } + + private func setVerticalStackView() { + guard let weatherStackView = weatherStackView else { return } - let verticalStackView: UIStackView = UIStackView(arrangedSubviews: [ + verticalStackView = UIStackView(arrangedSubviews: [ dateLabel, temperatureLabel, weatherStackView ]) - verticalStackView.axis = .vertical - verticalStackView.spacing = 8 - verticalStackView.distribution = .fill - verticalStackView.alignment = .leading + verticalStackView?.axis = .vertical + verticalStackView?.spacing = 8 + verticalStackView?.distribution = .fill + verticalStackView?.alignment = .leading + } + + private func setContentsStackView() { + guard let verticalStackView = verticalStackView else { return } - let contentsStackView: UIStackView = UIStackView(arrangedSubviews: [ + contentsStackView = UIStackView(arrangedSubviews: [ weatherIcon, verticalStackView ]) - contentsStackView.axis = .horizontal - contentsStackView.spacing = 16 - contentsStackView.alignment = .center - contentsStackView.distribution = .fill - contentsStackView.translatesAutoresizingMaskIntoConstraints = false + if let contentsStackView = contentsStackView { + contentsStackView.axis = .horizontal + contentsStackView.spacing = 16 + contentsStackView.alignment = .center + contentsStackView.distribution = .fill + contentsStackView.translatesAutoresizingMaskIntoConstraints = false + + contentView.addSubview(contentsStackView) + } + } + + private func layContentsStackView() { + guard let contentsStackView = contentsStackView else { return } - contentView.addSubview(contentsStackView) - NSLayoutConstraint.activate([ contentsStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), contentsStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), From d0b68e246050b3be8abffa66b43f2448ed8bf0b5 Mon Sep 17 00:00:00 2001 From: Song Jin Young Date: Thu, 25 Apr 2024 19:57:18 +0900 Subject: [PATCH 2/2] [STEP 2 & BONUS] --- WeatherForecast/WeatherForecast/Weather.swift | 33 +++++----- .../WeatherDetailViewController.swift | 6 +- .../WeatherListViewController.swift | 64 +++++++++++++++++-- 3 files changed, 75 insertions(+), 28 deletions(-) diff --git a/WeatherForecast/WeatherForecast/Weather.swift b/WeatherForecast/WeatherForecast/Weather.swift index c7835d3..fa4fa6a 100644 --- a/WeatherForecast/WeatherForecast/Weather.swift +++ b/WeatherForecast/WeatherForecast/Weather.swift @@ -50,32 +50,29 @@ class Coord: Decodable { } // MARK: - Temperature Unit -enum TempUnit: String { +enum TempUnit: String, CaseIterable { case metric, imperial var expression: String { - guard let expression = expressionMapping[self.rawValue] else { return ""} - return expression.rawValue + guard let expression = expressionMapping[self] else { return ""} + return expression } var desc: String { - switch self { - case .metric: - return "섭씨" - case .imperial: - return "화씨" - } + guard let desc = descMapping[self] else { return ""} + return desc } - private var expressionMapping: [String: TempExpression] { + private var expressionMapping: [TempUnit: String] { return [ - TempUnit.metric.rawValue: .metric, - TempUnit.imperial.rawValue: .imperial + TempUnit.metric: "℃", + TempUnit.imperial: "℉" + ] + } + + private var descMapping: [TempUnit: String] { + return [ + TempUnit.metric: "섭씨", + TempUnit.imperial: "화씨" ] } } - -enum TempExpression: String { - case metric = "℃" - case imperial = "℉" -} - diff --git a/WeatherForecast/WeatherForecast/WeatherDetailViewController.swift b/WeatherForecast/WeatherForecast/WeatherDetailViewController.swift index 1e7db06..ee2d103 100644 --- a/WeatherForecast/WeatherForecast/WeatherDetailViewController.swift +++ b/WeatherForecast/WeatherForecast/WeatherDetailViewController.swift @@ -71,10 +71,10 @@ class WeatherDetailViewController: UIViewController { private func setSubviewData(_ listInfo: WeatherForecastInfo) { weatherGroupLabel.text = listInfo.weather.main weatherDescriptionLabel.text = listInfo.weather.description - temperatureLabel.text = "현재 기온 : \(listInfo.main.temp)\(tempUnit.expression)" + temperatureLabel.text = "현재 기온 : \(listInfo.main.temp.changeTemperature(by:tempUnit))\(tempUnit.expression)" feelsLikeLabel.text = "체감 기온 : \(listInfo.main.feelsLike)\(tempUnit.expression)" - maximumTemperatureLable.text = "최고 기온 : \(listInfo.main.tempMax)\(tempUnit.expression)" - minimumTemperatureLable.text = "최저 기온 : \(listInfo.main.tempMin)\(tempUnit.expression)" + maximumTemperatureLable.text = "최고 기온 : \(listInfo.main.tempMax.changeTemperature(by:tempUnit))\(tempUnit.expression)" + minimumTemperatureLable.text = "최저 기온 : \(listInfo.main.tempMin.changeTemperature(by:tempUnit))\(tempUnit.expression)" popLabel.text = "강수 확률 : \(listInfo.main.pop * 100)%" humidityLabel.text = "습도 : \(listInfo.main.humidity)%" iconImageView.setImage(from: listInfo.weather.icon) diff --git a/WeatherForecast/WeatherForecast/WeatherListViewController.swift b/WeatherForecast/WeatherForecast/WeatherListViewController.swift index 56225bc..233094a 100644 --- a/WeatherForecast/WeatherForecast/WeatherListViewController.swift +++ b/WeatherForecast/WeatherForecast/WeatherListViewController.swift @@ -5,6 +5,7 @@ // import UIKit +import Foundation final class ImageCache { static let shared: NSCache = NSCache() @@ -29,12 +30,8 @@ class WeatherListViewController: UIViewController { extension WeatherListViewController { @objc private func changeTempUnit() { - switch tempUnit { - case .imperial: - tempUnit = .metric - case .metric: - tempUnit = .imperial - } + guard let strategy = StrategyContainer.strategies.filter({ $0.checkStrategy(tempUnit: tempUnit) }).first else { return } + tempUnit = strategy.changeTempUnit(tempUnit: tempUnit) navigationItem.rightBarButtonItem?.title = tempUnit.desc refresh() } @@ -103,7 +100,7 @@ extension WeatherListViewController { private func setData(cell: WeatherTableViewCell, data: WeatherForecastInfo) { cell.weatherLabel.text = data.weather.main cell.descriptionLabel.text = data.weather.description - cell.temperatureLabel.text = "\(data.main.temp)\(tempUnit.expression)" + cell.temperatureLabel.text = "\(data.main.temp.changeTemperature(by:tempUnit))\(tempUnit.expression)" cell.dateLabel.text = dateFormatter.string(from: data.dt) cell.weatherIcon.setImage(from: data.weather.icon) } @@ -178,3 +175,56 @@ extension UIImageView { } } } + +extension Double { + func changeTemperature(by tempUnit: TempUnit) -> String { + let strategies: [TempUnitStrategy] = [MetricTempUnitStrategy(), ImperialTempUnitStrategy()] + guard let strategy = strategies.filter({ $0.checkStrategy(tempUnit: tempUnit) }).first else { return "" } + return strategy.changeTemperatureToString(temperature: self) + } + + func string(format: String? = nil) -> String { + if let format = format { + return String(format: format, self) + } + return "\(self)" + } +} + +protocol TempUnitStrategy { + func changeTempUnit(tempUnit: TempUnit) -> TempUnit + func changeTemperatureToString(temperature: Double) -> String + func checkStrategy(tempUnit: TempUnit) -> Bool +} + +struct MetricTempUnitStrategy: TempUnitStrategy { + func changeTempUnit(tempUnit: TempUnit) -> TempUnit { + return .imperial + } + + func checkStrategy(tempUnit: TempUnit) -> Bool { + return tempUnit == .metric + } + + func changeTemperatureToString(temperature: Double) -> String { + return temperature.string() + } +} + +struct ImperialTempUnitStrategy: TempUnitStrategy { + func changeTempUnit(tempUnit: TempUnit) -> TempUnit { + return .metric + } + + func checkStrategy(tempUnit: TempUnit) -> Bool { + return tempUnit == .imperial + } + + func changeTemperatureToString(temperature: Double) -> String { + return ((temperature * 1.8) + 32).string(format: "%.2f") + } +} + +struct StrategyContainer { + static let strategies: [TempUnitStrategy] = [MetricTempUnitStrategy(), ImperialTempUnitStrategy()] +}