diff --git a/WeatherForecast/WeatherForecast.xcodeproj/project.pbxproj b/WeatherForecast/WeatherForecast.xcodeproj/project.pbxproj index 9b2f170..d337aef 100644 --- a/WeatherForecast/WeatherForecast.xcodeproj/project.pbxproj +++ b/WeatherForecast/WeatherForecast.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 61C1CA882BCD585500932E4A /* WeatherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C1CA872BCD585500932E4A /* WeatherViewController.swift */; }; + 61C1CA8A2BCD586900932E4A /* WeatherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C1CA892BCD586900932E4A /* WeatherView.swift */; }; + 61E8B7922BD41BD600802B65 /* WeatherDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E8B7912BD41BD600802B65 /* WeatherDetailView.swift */; }; + 61E8B7942BDA864B00802B65 /* ImageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E8B7932BDA864B00802B65 /* ImageManager.swift */; }; 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 */; }; @@ -19,6 +23,10 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 61C1CA872BCD585500932E4A /* WeatherViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewController.swift; sourceTree = ""; }; + 61C1CA892BCD586900932E4A /* WeatherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherView.swift; sourceTree = ""; }; + 61E8B7912BD41BD600802B65 /* WeatherDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherDetailView.swift; sourceTree = ""; }; + 61E8B7932BDA864B00802B65 /* ImageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageManager.swift; sourceTree = ""; }; C741F66F2B58F00500A4DDC0 /* Weather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weather.swift; sourceTree = ""; }; 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 = ""; }; @@ -66,8 +74,12 @@ C7743D8E2B21C38100DF0D09 /* SceneDelegate.swift */, C7743DA02B21C3B400DF0D09 /* WeatherTableViewCell.swift */, C7743D902B21C38100DF0D09 /* ViewController.swift */, + 61C1CA892BCD586900932E4A /* WeatherView.swift */, + 61C1CA872BCD585500932E4A /* WeatherViewController.swift */, C741F66F2B58F00500A4DDC0 /* Weather.swift */, + 61E8B7912BD41BD600802B65 /* WeatherDetailView.swift */, C7743DA22B21CA8500DF0D09 /* WeatherDetailViewController.swift */, + 61E8B7932BDA864B00802B65 /* ImageManager.swift */, C7743D922B21C38100DF0D09 /* Main.storyboard */, C7743D952B21C38200DF0D09 /* Assets.xcassets */, C7743D972B21C38200DF0D09 /* LaunchScreen.storyboard */, @@ -148,11 +160,15 @@ buildActionMask = 2147483647; files = ( C7743DA12B21C3B400DF0D09 /* WeatherTableViewCell.swift in Sources */, + 61E8B7942BDA864B00802B65 /* ImageManager.swift in Sources */, C7743D912B21C38100DF0D09 /* ViewController.swift in Sources */, C7743D8D2B21C38100DF0D09 /* AppDelegate.swift in Sources */, C7743DA32B21CA8600DF0D09 /* WeatherDetailViewController.swift in Sources */, + 61C1CA882BCD585500932E4A /* WeatherViewController.swift in Sources */, C741F6702B58F00500A4DDC0 /* Weather.swift in Sources */, + 61E8B7922BD41BD600802B65 /* WeatherDetailView.swift in Sources */, C7743D8F2B21C38100DF0D09 /* SceneDelegate.swift in Sources */, + 61C1CA8A2BCD586900932E4A /* WeatherView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/WeatherForecast/WeatherForecast/Base.lproj/Main.storyboard b/WeatherForecast/WeatherForecast/Base.lproj/Main.storyboard index 4798dc7..aa977cf 100644 --- a/WeatherForecast/WeatherForecast/Base.lproj/Main.storyboard +++ b/WeatherForecast/WeatherForecast/Base.lproj/Main.storyboard @@ -1,24 +1,25 @@ - + - + + - + - - + + - + diff --git a/WeatherForecast/WeatherForecast/ImageManager.swift b/WeatherForecast/WeatherForecast/ImageManager.swift new file mode 100644 index 0000000..470923d --- /dev/null +++ b/WeatherForecast/WeatherForecast/ImageManager.swift @@ -0,0 +1,30 @@ +// +// ImageManager.swift +// WeatherForecast +// +// Created by EUNSUNG on 4/25/24. +// + +import UIKit + +struct ImageManager { + private let imageChache: NSCache = NSCache() + + func downloadImage(urlString: String) async -> UIImage? { + guard let url: URL = URL(string: urlString), + let (data, _) = try? await URLSession.shared.data(from: url), + let image: UIImage = UIImage(data: data) else { + return nil + } + return image + } + + func getImageChache(forKey: String) -> UIImage? { + guard let image = imageChache.object(forKey: forKey as NSString) else { return nil } + return image + } + + func setImageChache(image: UIImage, forKey: String) { + imageChache.setObject(image, forKey: forKey as NSString) + } +} diff --git a/WeatherForecast/WeatherForecast/SceneDelegate.swift b/WeatherForecast/WeatherForecast/SceneDelegate.swift index 264a1ab..23a09c7 100644 --- a/WeatherForecast/WeatherForecast/SceneDelegate.swift +++ b/WeatherForecast/WeatherForecast/SceneDelegate.swift @@ -15,7 +15,19 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } + + guard let scene: UIWindowScene = (scene as? UIWindowScene) else { return } + + let viewController: WeatherViewController = WeatherViewController(presentable: WeatherDetailViewController(), imageManager: ImageManager()) + let navigationController: UINavigationController = UINavigationController(rootViewController: viewController) + navigationController.navigationBar.prefersLargeTitles = true + navigationController.navigationBar.tintColor = .black + + let window: UIWindow = UIWindow(windowScene: scene) + window.rootViewController = navigationController + + self.window = window + window.makeKeyAndVisible() } func sceneDidDisconnect(_ scene: UIScene) { diff --git a/WeatherForecast/WeatherForecast/ViewController.swift b/WeatherForecast/WeatherForecast/ViewController.swift index 50b66fb..4ce79cf 100644 --- a/WeatherForecast/WeatherForecast/ViewController.swift +++ b/WeatherForecast/WeatherForecast/ViewController.swift @@ -1,166 +1,166 @@ +//// +//// WeatherForecast - ViewController.swift +//// Created by yagom. +//// Copyright © yagom. All rights reserved. +//// +// +//import UIKit +// +//class ViewController: 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 +// }() +// +// var tempUnit: TempUnit = .metric +// +// override func viewDidLoad() { +// super.viewDidLoad() +// initialSetUp() +// } +//} +// +//extension ViewController { +// @objc private func changeTempUnit() { +// switch tempUnit { +// case .imperial: +// tempUnit = .metric +// navigationItem.rightBarButtonItem?.title = "섭씨" +// case .metric: +// tempUnit = .imperial +// navigationItem.rightBarButtonItem?.title = "화씨" +// } +// refresh() +// } +// +// @objc private func refresh() { +// fetchWeatherJSON() +// tableView.reloadData() +// refreshControl.endRefreshing() +// } +// +// private func initialSetUp() { +// navigationItem.rightBarButtonItem = UIBarButtonItem(title: "화씨", 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 +// } +// +// private func layTable() { +// tableView = .init(frame: .zero, style: .plain) +// view.addSubview(tableView) +// tableView.translatesAutoresizingMaskIntoConstraints = false +// +// let safeArea: UILayoutGuide = view.safeAreaLayoutGuide +// +// NSLayoutConstraint.activate([ +// tableView.topAnchor.constraint(equalTo: safeArea.topAnchor), +// tableView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor), +// tableView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor), +// tableView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor) +// ]) +// } +//} +// +//extension ViewController { +// private func fetchWeatherJSON() { +// +// let jsonDecoder: JSONDecoder = .init() +// jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase +// +// guard let data = NSDataAsset(name: "weather")?.data else { +// return +// } +// +// let info: WeatherJSON +// do { +// info = try jsonDecoder.decode(WeatherJSON.self, from: data) +// } catch { +// print(error.localizedDescription) +// return +// } +// +// weatherJSON = info +// navigationItem.title = weatherJSON?.city.name +// } +//} +// +//extension ViewController: UITableViewDataSource { +// +// func numberOfSections(in tableView: UITableView) -> Int { +// 1 +// } +// +// func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { +// weatherJSON?.weatherForecast.count ?? 0 +// } +// +// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { +// 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 +// 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 +// } +// } +// +// return cell +// } +//} +// +//extension ViewController: 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 +// detailViewController.tempUnit = tempUnit +// navigationController?.show(detailViewController, sender: self) +// } +//} +// // -// WeatherForecast - ViewController.swift -// Created by yagom. -// Copyright © yagom. All rights reserved. -// - -import UIKit - -class ViewController: 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 - }() - - var tempUnit: TempUnit = .metric - - override func viewDidLoad() { - super.viewDidLoad() - initialSetUp() - } -} - -extension ViewController { - @objc private func changeTempUnit() { - switch tempUnit { - case .imperial: - tempUnit = .metric - navigationItem.rightBarButtonItem?.title = "섭씨" - case .metric: - tempUnit = .imperial - navigationItem.rightBarButtonItem?.title = "화씨" - } - refresh() - } - - @objc private func refresh() { - fetchWeatherJSON() - tableView.reloadData() - refreshControl.endRefreshing() - } - - private func initialSetUp() { - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "화씨", 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 - } - - private func layTable() { - tableView = .init(frame: .zero, style: .plain) - view.addSubview(tableView) - tableView.translatesAutoresizingMaskIntoConstraints = false - - let safeArea: UILayoutGuide = view.safeAreaLayoutGuide - - NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: safeArea.topAnchor), - tableView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor), - tableView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor), - tableView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor) - ]) - } -} - -extension ViewController { - private func fetchWeatherJSON() { - - let jsonDecoder: JSONDecoder = .init() - jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase - - guard let data = NSDataAsset(name: "weather")?.data else { - return - } - - let info: WeatherJSON - do { - info = try jsonDecoder.decode(WeatherJSON.self, from: data) - } catch { - print(error.localizedDescription) - return - } - - weatherJSON = info - navigationItem.title = weatherJSON?.city.name - } -} - -extension ViewController: UITableViewDataSource { - - func numberOfSections(in tableView: UITableView) -> Int { - 1 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - weatherJSON?.weatherForecast.count ?? 0 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - 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 - 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 - } - } - - return cell - } -} - -extension ViewController: 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 - detailViewController.tempUnit = tempUnit - navigationController?.show(detailViewController, sender: self) - } -} - - diff --git a/WeatherForecast/WeatherForecast/Weather.swift b/WeatherForecast/WeatherForecast/Weather.swift index ede7585..39a96b2 100644 --- a/WeatherForecast/WeatherForecast/Weather.swift +++ b/WeatherForecast/WeatherForecast/Weather.swift @@ -22,10 +22,33 @@ class WeatherForecastInfo: Decodable { // MARK: - MainClass class MainInfo: Decodable { + //let temp: Temperature let temp, feelsLike, tempMin, tempMax: Double let pressure, seaLevel, grndLevel, humidity, pop: Double } +protocol Temperature { + var unit: String { get } + var unitString: String { get } + func getTemperature(temp: Double) -> Double +} + +struct MetricTemperature: Temperature { + var unit: String = "℃" + var unitString: String = "화씨" + func getTemperature(temp: Double) -> Double { + return temp + } +} + +struct ImperialTemperature: Temperature { + var unit: String = "℉" + var unitString: String = "섭씨" + func getTemperature(temp: Double) -> Double { + return temp / 2 // 화씨 임시 로직 + } +} + // MARK: - Weather class Weather: Decodable { let id: Int @@ -50,13 +73,13 @@ class Coord: Decodable { } // MARK: - Temperature Unit -enum TempUnit: String { - case metric, imperial - var expression: String { - switch self { - case .metric: return "℃" - case .imperial: return "℉" - } - } -} +//enum TempUnit: String { +// case metric, imperial +// var expression: String { +// switch self { +// case .metric: return "℃" +// case .imperial: return "℉" +// } +// } +//} diff --git a/WeatherForecast/WeatherForecast/WeatherDetailView.swift b/WeatherForecast/WeatherForecast/WeatherDetailView.swift new file mode 100644 index 0000000..eefcdf1 --- /dev/null +++ b/WeatherForecast/WeatherForecast/WeatherDetailView.swift @@ -0,0 +1,105 @@ +// +// WeatherDetailView.swift +// WeatherForecast +// +// Created by EUNSUNG on 4/21/24. +// + +import UIKit + +protocol WeatherDetailViewDelegate: class { + func setValue(weatherGroupLabel: UILabel, weatherDescriptionLabel: UILabel, temperatureLabel: UILabel, feelsLikeLabel: UILabel, maximumTemperatureLable: UILabel, minimumTemperatureLable: UILabel, popLabel: UILabel, humidityLabel: UILabel, sunriseTimeLabel: UILabel, sunsetTimeLabel: UILabel, iconImageView: UIImageView) +} + +class WeatherDetailView: UIView { + 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 weak var delegate: WeatherDetailViewDelegate? + + // 다음에 swift 5.10으로 업데이트 후 nested protocol 적용해보겠습니다. +// protocol WeatherDetailViewDelegate2 { +// +// } + + init(delegate: WeatherDetailViewDelegate) { + self.delegate = delegate + super.init(frame: .zero) + + setUI() + setValue() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setUI() { + self.backgroundColor = .white + spacingView.backgroundColor = .clear + spacingView.setContentHuggingPriority(.defaultLow, for: .vertical) + weatherGroupLabel.font = .preferredFont(forTextStyle: .largeTitle) + weatherDescriptionLabel.font = .preferredFont(forTextStyle: .largeTitle) + + setStackView() + } + + func setValue() { + delegate?.setValue(weatherGroupLabel: self.weatherGroupLabel, weatherDescriptionLabel: self.weatherDescriptionLabel, temperatureLabel: self.temperatureLabel, feelsLikeLabel: self.feelsLikeLabel, maximumTemperatureLable: self.maximumTemperatureLable, minimumTemperatureLable: self.minimumTemperatureLable, popLabel: self.popLabel, humidityLabel: self.humidityLabel, sunriseTimeLabel: self.sunriseTimeLabel, sunsetTimeLabel: self.sunsetTimeLabel, iconImageView: self.iconImageView) + } + + func setStackView() { + let mainStackView: UIStackView = .init(arrangedSubviews: [ + iconImageView, + weatherGroupLabel, + weatherDescriptionLabel, + temperatureLabel, + feelsLikeLabel, + maximumTemperatureLable, + minimumTemperatureLable, + popLabel, + humidityLabel, + sunriseTimeLabel, + sunsetTimeLabel, + spacingView + ]) + + mainStackView.arrangedSubviews.forEach { subview in + guard let subview: UILabel = subview as? UILabel else { return } + subview.textColor = .black + subview.backgroundColor = .clear + subview.numberOfLines = 1 + subview.textAlignment = .center + subview.font = .preferredFont(forTextStyle: .body) + } + + mainStackView.axis = .vertical + mainStackView.alignment = .center + mainStackView.spacing = 8 + self.addSubview(mainStackView) + mainStackView.translatesAutoresizingMaskIntoConstraints = false + + let safeArea: UILayoutGuide = self.safeAreaLayoutGuide + NSLayoutConstraint.activate([ + mainStackView.topAnchor.constraint(equalTo: safeArea.topAnchor), + mainStackView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor), + mainStackView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, + constant: 16), + mainStackView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, + constant: -16), + iconImageView.widthAnchor.constraint(equalTo: iconImageView.heightAnchor), + iconImageView.widthAnchor.constraint(equalTo: safeArea.widthAnchor, + multiplier: 0.3) + ]) + } +} diff --git a/WeatherForecast/WeatherForecast/WeatherDetailViewController.swift b/WeatherForecast/WeatherForecast/WeatherDetailViewController.swift index 69d3dfb..f6bdee7 100644 --- a/WeatherForecast/WeatherForecast/WeatherDetailViewController.swift +++ b/WeatherForecast/WeatherForecast/WeatherDetailViewController.swift @@ -6,11 +6,11 @@ import UIKit -class WeatherDetailViewController: UIViewController { - +class WeatherDetailViewController: UIViewController, WeatherDetailViewDelegate, WeatherDetailPresentable { + var weatherForecastInfo: WeatherForecastInfo? var cityInfo: City? - var tempUnit: TempUnit = .metric + var temperature: Temperature? let dateFormatter: DateFormatter = { let formatter: DateFormatter = DateFormatter() @@ -21,107 +21,77 @@ class WeatherDetailViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - initialSetUp() + setNavigationTitle() } - private func initialSetUp() { - view.backgroundColor = .white - + override func loadView() { + view = WeatherDetailView(delegate: self) + } + + private func setNavigationTitle() { guard let listInfo = weatherForecastInfo else { return } let date: Date = Date(timeIntervalSince1970: listInfo.dt) navigationItem.title = dateFormatter.string(from: date) + } + + private func cityDateFormatter() -> DateFormatter { + let formatter: DateFormatter = DateFormatter() + formatter.dateFormat = .none + formatter.timeStyle = .short + formatter.locale = .init(identifier: "ko_KR") - 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: [ - iconImageView, - weatherGroupLabel, - weatherDescriptionLabel, - temperatureLabel, - feelsLikeLabel, - maximumTemperatureLable, - minimumTemperatureLable, - popLabel, - humidityLabel, - sunriseTimeLabel, - sunsetTimeLabel, - spacingView - ]) - - mainStackView.arrangedSubviews.forEach { subview in - guard let subview: UILabel = subview as? UILabel else { return } - subview.textColor = .black - subview.backgroundColor = .clear - subview.numberOfLines = 1 - subview.textAlignment = .center - subview.font = .preferredFont(forTextStyle: .body) - } - - weatherGroupLabel.font = .preferredFont(forTextStyle: .largeTitle) - weatherDescriptionLabel.font = .preferredFont(forTextStyle: .largeTitle) - - mainStackView.axis = .vertical - mainStackView.alignment = .center - mainStackView.spacing = 8 - view.addSubview(mainStackView) - mainStackView.translatesAutoresizingMaskIntoConstraints = false - - let safeArea: UILayoutGuide = view.safeAreaLayoutGuide - NSLayoutConstraint.activate([ - mainStackView.topAnchor.constraint(equalTo: safeArea.topAnchor), - mainStackView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor), - mainStackView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, - constant: 16), - mainStackView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, - constant: -16), - iconImageView.widthAnchor.constraint(equalTo: iconImageView.heightAnchor), - 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)%" - - if let cityInfo { - let formatter: DateFormatter = DateFormatter() - formatter.dateFormat = .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)))" - } - + return formatter + } + + private func setIconImage(icon:String, iconImageView: UIImageView) { Task { - let iconName: String = listInfo.weather.icon - let urlString: String = "https://openweathermap.org/img/wn/\(iconName)@2x.png" + let urlString: String = "https://openweathermap.org/img/wn/\(icon)@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 } } + + // protocol function + func setValue(weatherGroupLabel: UILabel, weatherDescriptionLabel: UILabel, temperatureLabel: UILabel, feelsLikeLabel: UILabel, maximumTemperatureLable: UILabel, minimumTemperatureLable: UILabel, popLabel: UILabel, humidityLabel: UILabel, sunriseTimeLabel: UILabel, sunsetTimeLabel: UILabel, iconImageView: UIImageView) { + guard let listInfo = weatherForecastInfo else { return } + + weatherGroupLabel.text = listInfo.weather.main + weatherDescriptionLabel.text = listInfo.weather.description + guard let temperature = temperature else {return} + temperatureLabel.text = "현재 기온 : \(temperature.getTemperature(temp: listInfo.main.temp))\(temperature.unit)" + feelsLikeLabel.text = "체감 기온 : \(listInfo.main.feelsLike)\(temperature.unit)" + maximumTemperatureLable.text = "최고 기온 : \(listInfo.main.tempMax)\(temperature.unit)" + minimumTemperatureLable.text = "최저 기온 : \(listInfo.main.tempMin)\(temperature.unit)" + popLabel.text = "강수 확률 : \(listInfo.main.pop * 100)%" + humidityLabel.text = "습도 : \(listInfo.main.humidity)%" + + if let cityInfo { + sunriseTimeLabel.text = "일출 : \(cityDateFormatter().string(from: Date(timeIntervalSince1970: cityInfo.sunrise)))" + sunsetTimeLabel.text = "일몰 : \(cityDateFormatter().string(from: Date(timeIntervalSince1970: cityInfo.sunset)))" + } + setIconImage(icon: listInfo.weather.icon, iconImageView: iconImageView) + } + + func setWeatherForecastInfo(weatherForecaseInfo: WeatherForecastInfo?) { + self.weatherForecastInfo = weatherForecaseInfo + } + + func setCityInfo(cityInfo: City?) { + self.cityInfo = cityInfo + } + + func setTemperature(temperature: Temperature) { + self.temperature = temperature + } + + func showDetailViewController(on navigationController: UINavigationController?) { + navigationController?.show(self, sender: self) + } } diff --git a/WeatherForecast/WeatherForecast/WeatherView.swift b/WeatherForecast/WeatherForecast/WeatherView.swift new file mode 100644 index 0000000..14985e2 --- /dev/null +++ b/WeatherForecast/WeatherForecast/WeatherView.swift @@ -0,0 +1,100 @@ +// +// WeatherView.swift +// WeatherForecast +// +// Created by EUNSUNG on 4/15/24. +// + +import UIKit + +protocol WeatherViewDelegate { + func setNavigationItem(buttonItem: UIBarButtonItem) + func changeTempUnit() + func refresh() + func cellCount() -> Int + func setCell(tableView: UITableView, indexPath: IndexPath) -> UITableViewCell + func didSelectRow(tableView: UITableView, indexPath: IndexPath) +} + +class WeatherView: UIView { + private var delegate:WeatherViewDelegate + + private var tableView: UITableView! + private let refreshControl: UIRefreshControl = UIRefreshControl() + private var icons: [UIImage]? + + init(delegate: WeatherViewDelegate) { + self.delegate = delegate + super.init(frame: .zero) + + initialSetUp() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func initialSetUp() { + self.backgroundColor = .white + delegate.setNavigationItem(buttonItem: UIBarButtonItem(title: "화씨", image: nil, target: self, action: #selector(onBarButton))) + + layTable() + + refreshControl.addTarget(self, + action: #selector(onRefreshControl), + for: .valueChanged) + + tableView.refreshControl = refreshControl + tableView.register(WeatherTableViewCell.self, forCellReuseIdentifier: "WeatherCell") + + tableView.dataSource = self + tableView.delegate = self + } + + @objc private func onBarButton() { + delegate.changeTempUnit() + delegate.refresh() + tableView.reloadData() + } + + @objc private func onRefreshControl() { + delegate.refresh() + refreshControl.endRefreshing() + tableView.reloadData() + } + + private func layTable() { + tableView = .init(frame: .zero, style: .plain) + self.addSubview(tableView) + tableView.translatesAutoresizingMaskIntoConstraints = false + + let safeArea: UILayoutGuide = self.safeAreaLayoutGuide + + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: safeArea.topAnchor), + tableView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor), + tableView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor), + tableView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor) + ]) + } +} + +extension WeatherView: UITableViewDataSource { + func numberOfSections(in tableView: UITableView) -> Int { + 1 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return delegate.cellCount() + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + return delegate.setCell(tableView: tableView, indexPath: indexPath) + } +} + +extension WeatherView: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + delegate.didSelectRow(tableView: tableView, indexPath: indexPath) + } +} diff --git a/WeatherForecast/WeatherForecast/WeatherViewController.swift b/WeatherForecast/WeatherForecast/WeatherViewController.swift new file mode 100644 index 0000000..5c2ad1d --- /dev/null +++ b/WeatherForecast/WeatherForecast/WeatherViewController.swift @@ -0,0 +1,148 @@ +// +// WeatherViewController.swift +// WeatherForecast +// +// Created by EUNSUNG on 4/15/24. +// + +import UIKit + +protocol WeatherDetailPresentable { + func setWeatherForecastInfo(weatherForecaseInfo: WeatherForecastInfo?) + func setCityInfo(cityInfo: City?) + func setTemperature(temperature: Temperature) + func showDetailViewController(on navigationController: UINavigationController?) +} + +class WeatherViewController: UIViewController, WeatherViewDelegate { + private var weatherJSON: WeatherJSON? + private var temperature: Temperature + private 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 presentable: WeatherDetailPresentable + private let imageManager: ImageManager + + init(presentable: WeatherDetailPresentable, imageManager: ImageManager, temperature: Temperature = MetricTemperature()) { + self.presentable = presentable + self.imageManager = imageManager + self.temperature = temperature + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + refresh() + } + + override func loadView() { + view = WeatherView(delegate: self) + } + + func setTitle(titleText: String) { + navigationItem.title = titleText + } + + private func convertTimeToDateString(timeInterval: TimeInterval) -> String { + let date: Date = Date(timeIntervalSince1970: timeInterval) + return dateFormatter.string(from: date) + } + + private func setImageFromURL(icon: String, imageView: UIImageView) { + let urlString: String = "https://openweathermap.org/img/wn/\(icon)@2x.png" + + if let image = imageManager.getImageChache(forKey: urlString) { + imageView.image = image + } + Task { + guard let image = await imageManager.downloadImage(urlString: urlString) else { + return + } + imageManager.setImageChache(image: image, forKey: urlString) + imageView.image = image + } + } + + // protocol function + func setNavigationItem(buttonItem: UIBarButtonItem) { + navigationItem.rightBarButtonItem = buttonItem + } + + func changeTempUnit() { + temperature = temperature as? MetricTemperature == nil ? MetricTemperature() : ImperialTemperature() + navigationItem.rightBarButtonItem?.title = temperature.unitString +// tempUnit = tempUnit == .imperial ? .metric : .imperial +// navigationItem.rightBarButtonItem?.title = tempUnit == .imperial ? "화씨" : "섭씨" + } + + func refresh() { + guard let json = decodeWeatherJSON(dataName: "weather") else { + return + } + weatherJSON = json + setTitle(titleText: (weatherJSON?.city.name)!) + } + + func cellCount() -> Int { + return weatherJSON?.weatherForecast.count ?? 0 + } + + func setCell(tableView: UITableView, indexPath: IndexPath) -> UITableViewCell { + 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 = "\(temperature.getTemperature(temp: weatherForecastInfo.main.temp))\(temperature.unit)" +// cell.temperatureLabel.text = "\(weatherForecastInfo.main.temp)\(tempUnit.expression)" + cell.dateLabel.text = convertTimeToDateString(timeInterval: weatherForecastInfo.dt) + setImageFromURL(icon: weatherForecastInfo.weather.icon, imageView: cell.weatherIcon) + + return cell + } + + func didSelectRow(tableView: UITableView, indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + presentable.setWeatherForecastInfo(weatherForecaseInfo: weatherJSON?.weatherForecast[indexPath.row]) + presentable.setCityInfo(cityInfo: weatherJSON?.city) + presentable.setTemperature(temperature: temperature) + presentable.showDetailViewController(on: navigationController) + } +} + +extension WeatherViewController { + private func fetchWeatherJSON(dataName: String) -> Data? { + guard let data = NSDataAsset(name: dataName)?.data else { + return nil + } + return data + } + + private func decodeWeatherJSON(dataName: String) -> WeatherJSON? { + let jsonDecoder: JSONDecoder = .init() + jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase + + guard let data = fetchWeatherJSON(dataName: dataName) else { + return nil + } + do { + return try jsonDecoder.decode(WeatherJSON.self, from: data) + } catch { + print(error.localizedDescription) + return nil + } + } +}