diff --git a/WeatherForecast/WeatherForecast.xcodeproj/project.pbxproj b/WeatherForecast/WeatherForecast.xcodeproj/project.pbxproj index 9b2f170..64fe31f 100644 --- a/WeatherForecast/WeatherForecast.xcodeproj/project.pbxproj +++ b/WeatherForecast/WeatherForecast.xcodeproj/project.pbxproj @@ -7,11 +7,16 @@ objects = { /* Begin PBXBuildFile section */ + 60EC0E812BA032760042C815 /* DateFormatter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60EC0E802BA032760042C815 /* DateFormatter+.swift */; }; + 60EC0E852BA0431A0042C815 /* WeatherJSONService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60EC0E842BA0431A0042C815 /* WeatherJSONService.swift */; }; + 60EC0E892BA0673F0042C815 /* TempUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60EC0E882BA0673F0042C815 /* TempUnit.swift */; }; + 60EC0E8B2BA087720042C815 /* WeatherJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60EC0E8A2BA087720042C815 /* WeatherJSON.swift */; }; + 60EC0E8F2BA1C2520042C815 /* WeatherImageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60EC0E8E2BA1C2520042C815 /* WeatherImageService.swift */; }; + 60EC0E912BA1CD590042C815 /* WeatherDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60EC0E902BA1CD590042C815 /* WeatherDetailView.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 */; }; - C7743D912B21C38100DF0D09 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7743D902B21C38100DF0D09 /* ViewController.swift */; }; - C7743D942B21C38100DF0D09 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C7743D922B21C38100DF0D09 /* Main.storyboard */; }; + C7743D912B21C38100DF0D09 /* WeatherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7743D902B21C38100DF0D09 /* WeatherViewController.swift */; }; C7743D962B21C38200DF0D09 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C7743D952B21C38200DF0D09 /* Assets.xcassets */; }; C7743D992B21C38200DF0D09 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C7743D972B21C38200DF0D09 /* LaunchScreen.storyboard */; }; C7743DA12B21C3B400DF0D09 /* WeatherTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7743DA02B21C3B400DF0D09 /* WeatherTableViewCell.swift */; }; @@ -19,12 +24,17 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 60EC0E802BA032760042C815 /* DateFormatter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+.swift"; sourceTree = ""; }; + 60EC0E842BA0431A0042C815 /* WeatherJSONService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherJSONService.swift; sourceTree = ""; }; + 60EC0E882BA0673F0042C815 /* TempUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempUnit.swift; sourceTree = ""; }; + 60EC0E8A2BA087720042C815 /* WeatherJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherJSON.swift; sourceTree = ""; }; + 60EC0E8E2BA1C2520042C815 /* WeatherImageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherImageService.swift; sourceTree = ""; }; + 60EC0E902BA1CD590042C815 /* WeatherDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherDetailView.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 = ""; }; C7743D8E2B21C38100DF0D09 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - C7743D902B21C38100DF0D09 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - C7743D932B21C38100DF0D09 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + C7743D902B21C38100DF0D09 /* WeatherViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewController.swift; 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 = ""; }; C7743D9A2B21C38200DF0D09 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -43,6 +53,88 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 60EC0E772BA02A2B0042C815 /* Scene */ = { + isa = PBXGroup; + children = ( + 60EC0E792BA02A540042C815 /* WeatherDetail */, + 60EC0E782BA02A3C0042C815 /* Weather */, + ); + path = Scene; + sourceTree = ""; + }; + 60EC0E782BA02A3C0042C815 /* Weather */ = { + isa = PBXGroup; + children = ( + C7743D902B21C38100DF0D09 /* WeatherViewController.swift */, + C7743DA02B21C3B400DF0D09 /* WeatherTableViewCell.swift */, + ); + path = Weather; + sourceTree = ""; + }; + 60EC0E792BA02A540042C815 /* WeatherDetail */ = { + isa = PBXGroup; + children = ( + C7743DA22B21CA8500DF0D09 /* WeatherDetailViewController.swift */, + 60EC0E902BA1CD590042C815 /* WeatherDetailView.swift */, + ); + path = WeatherDetail; + sourceTree = ""; + }; + 60EC0E7A2BA02A820042C815 /* Resource */ = { + isa = PBXGroup; + children = ( + C7743D8C2B21C38100DF0D09 /* AppDelegate.swift */, + C7743D8E2B21C38100DF0D09 /* SceneDelegate.swift */, + ); + path = Resource; + sourceTree = ""; + }; + 60EC0E7B2BA02A8C0042C815 /* Source */ = { + isa = PBXGroup; + children = ( + 60EC0E832BA041390042C815 /* Service */, + 60EC0E7F2BA032660042C815 /* Utils */, + 60EC0E7C2BA02AC80042C815 /* Model */, + 60EC0E772BA02A2B0042C815 /* Scene */, + ); + path = Source; + sourceTree = ""; + }; + 60EC0E7C2BA02AC80042C815 /* Model */ = { + isa = PBXGroup; + children = ( + C741F66F2B58F00500A4DDC0 /* Weather.swift */, + 60EC0E882BA0673F0042C815 /* TempUnit.swift */, + 60EC0E8A2BA087720042C815 /* WeatherJSON.swift */, + ); + path = Model; + sourceTree = ""; + }; + 60EC0E7F2BA032660042C815 /* Utils */ = { + isa = PBXGroup; + children = ( + 60EC0E822BA0327B0042C815 /* Extensions */, + ); + path = Utils; + sourceTree = ""; + }; + 60EC0E822BA0327B0042C815 /* Extensions */ = { + isa = PBXGroup; + children = ( + 60EC0E802BA032760042C815 /* DateFormatter+.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 60EC0E832BA041390042C815 /* Service */ = { + isa = PBXGroup; + children = ( + 60EC0E842BA0431A0042C815 /* WeatherJSONService.swift */, + 60EC0E8E2BA1C2520042C815 /* WeatherImageService.swift */, + ); + path = Service; + sourceTree = ""; + }; C7743D802B21C38100DF0D09 = { isa = PBXGroup; children = ( @@ -62,13 +154,8 @@ C7743D8B2B21C38100DF0D09 /* WeatherForecast */ = { isa = PBXGroup; children = ( - C7743D8C2B21C38100DF0D09 /* AppDelegate.swift */, - C7743D8E2B21C38100DF0D09 /* SceneDelegate.swift */, - C7743DA02B21C3B400DF0D09 /* WeatherTableViewCell.swift */, - C7743D902B21C38100DF0D09 /* ViewController.swift */, - C741F66F2B58F00500A4DDC0 /* Weather.swift */, - C7743DA22B21CA8500DF0D09 /* WeatherDetailViewController.swift */, - C7743D922B21C38100DF0D09 /* Main.storyboard */, + 60EC0E7A2BA02A820042C815 /* Resource */, + 60EC0E7B2BA02A8C0042C815 /* Source */, C7743D952B21C38200DF0D09 /* Assets.xcassets */, C7743D972B21C38200DF0D09 /* LaunchScreen.storyboard */, C7743D9A2B21C38200DF0D09 /* Info.plist */, @@ -136,7 +223,6 @@ files = ( C7743D992B21C38200DF0D09 /* LaunchScreen.storyboard in Resources */, C7743D962B21C38200DF0D09 /* Assets.xcassets in Resources */, - C7743D942B21C38100DF0D09 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -147,11 +233,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 60EC0E852BA0431A0042C815 /* WeatherJSONService.swift in Sources */, C7743DA12B21C3B400DF0D09 /* WeatherTableViewCell.swift in Sources */, - C7743D912B21C38100DF0D09 /* ViewController.swift in Sources */, + 60EC0E892BA0673F0042C815 /* TempUnit.swift in Sources */, + C7743D912B21C38100DF0D09 /* WeatherViewController.swift in Sources */, C7743D8D2B21C38100DF0D09 /* AppDelegate.swift in Sources */, + 60EC0E812BA032760042C815 /* DateFormatter+.swift in Sources */, C7743DA32B21CA8600DF0D09 /* WeatherDetailViewController.swift in Sources */, + 60EC0E8F2BA1C2520042C815 /* WeatherImageService.swift in Sources */, C741F6702B58F00500A4DDC0 /* Weather.swift in Sources */, + 60EC0E912BA1CD590042C815 /* WeatherDetailView.swift in Sources */, + 60EC0E8B2BA087720042C815 /* WeatherJSON.swift in Sources */, C7743D8F2B21C38100DF0D09 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -159,14 +251,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ - C7743D922B21C38100DF0D09 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - C7743D932B21C38100DF0D09 /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; C7743D972B21C38200DF0D09 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -308,7 +392,6 @@ INFOPLIST_FILE = WeatherForecast/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( @@ -335,7 +418,6 @@ INFOPLIST_FILE = WeatherForecast/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/WeatherForecast/WeatherForecast/Base.lproj/Main.storyboard b/WeatherForecast/WeatherForecast/Base.lproj/Main.storyboard deleted file mode 100644 index 4798dc7..0000000 --- a/WeatherForecast/WeatherForecast/Base.lproj/Main.storyboard +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WeatherForecast/WeatherForecast/Info.plist b/WeatherForecast/WeatherForecast/Info.plist index dd3c9af..0eb786d 100644 --- a/WeatherForecast/WeatherForecast/Info.plist +++ b/WeatherForecast/WeatherForecast/Info.plist @@ -15,8 +15,6 @@ Default Configuration UISceneDelegateClassName $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main diff --git a/WeatherForecast/WeatherForecast/AppDelegate.swift b/WeatherForecast/WeatherForecast/Resource/AppDelegate.swift similarity index 100% rename from WeatherForecast/WeatherForecast/AppDelegate.swift rename to WeatherForecast/WeatherForecast/Resource/AppDelegate.swift diff --git a/WeatherForecast/WeatherForecast/Resource/SceneDelegate.swift b/WeatherForecast/WeatherForecast/Resource/SceneDelegate.swift new file mode 100644 index 0000000..4523274 --- /dev/null +++ b/WeatherForecast/WeatherForecast/Resource/SceneDelegate.swift @@ -0,0 +1,30 @@ +// +// WeatherForecast - SceneDelegate.swift +// Created by yagom. +// Copyright © yagom. All rights reserved. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + + guard let scene = (scene as? UIWindowScene) else { return } + window = UIWindow(windowScene: scene) + + let weatherService: WeatherJSONService = WeatherJSONService() + let imageService: WeatherImageService = WeatherImageService() + let tempUnit = TempUnit.fahrenheit + + let viewController = WeatherViewController(tempUnit: tempUnit, + weatherService: weatherService, + imageService: imageService) + let navigationController = UINavigationController(rootViewController: viewController) + window?.rootViewController = navigationController + window?.makeKeyAndVisible() + } +} + diff --git a/WeatherForecast/WeatherForecast/SceneDelegate.swift b/WeatherForecast/WeatherForecast/SceneDelegate.swift deleted file mode 100644 index 264a1ab..0000000 --- a/WeatherForecast/WeatherForecast/SceneDelegate.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// WeatherForecast - SceneDelegate.swift -// Created by yagom. -// Copyright © yagom. All rights reserved. -// - -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - - var window: UIWindow? - - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // 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 } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } - - -} - diff --git a/WeatherForecast/WeatherForecast/Source/Model/TempUnit.swift b/WeatherForecast/WeatherForecast/Source/Model/TempUnit.swift new file mode 100644 index 0000000..bb129bd --- /dev/null +++ b/WeatherForecast/WeatherForecast/Source/Model/TempUnit.swift @@ -0,0 +1,39 @@ +// +// TempUnit.swift +// WeatherForecast +// +// Created by 홍승완 on 2024/03/12. +// + + +// MARK: - Temperature Unit +enum TempUnit: String { + case celsius, fahrenheit + + var symbol: String { + switch self { + case .celsius: + return "℃" + case .fahrenheit: + return "℉" + } + } + + var description: String { + switch self { + case .celsius: + return "섭씨" + case .fahrenheit: + return "화씨" + } + } + + mutating func toggle() { + switch self { + case .celsius: + self = .fahrenheit + case .fahrenheit: + self = .celsius + } + } +} diff --git a/WeatherForecast/WeatherForecast/Weather.swift b/WeatherForecast/WeatherForecast/Source/Model/Weather.swift similarity index 58% rename from WeatherForecast/WeatherForecast/Weather.swift rename to WeatherForecast/WeatherForecast/Source/Model/Weather.swift index ede7585..7daaee2 100644 --- a/WeatherForecast/WeatherForecast/Weather.swift +++ b/WeatherForecast/WeatherForecast/Source/Model/Weather.swift @@ -6,14 +6,8 @@ import Foundation -// MARK: - Weather JSON Format -class WeatherJSON: Decodable { - let weatherForecast: [WeatherForecastInfo] - let city: City -} - // MARK: - List -class WeatherForecastInfo: Decodable { +struct WeatherForecastInfo: Decodable { let dt: TimeInterval let main: MainInfo let weather: Weather @@ -21,13 +15,13 @@ class WeatherForecastInfo: Decodable { } // MARK: - MainClass -class MainInfo: Decodable { +struct MainInfo: Decodable { let temp, feelsLike, tempMin, tempMax: Double let pressure, seaLevel, grndLevel, humidity, pop: Double } // MARK: - Weather -class Weather: Decodable { +struct Weather: Decodable { let id: Int let main: String let description: String @@ -35,7 +29,7 @@ class Weather: Decodable { } // MARK: - City -class City: Decodable { +struct City: Decodable { let id: Int let name: String let coord: Coord @@ -45,18 +39,6 @@ class City: Decodable { } // MARK: - Coord -class Coord: Decodable { +struct Coord: Decodable { let lat, lon: Double } - -// MARK: - Temperature Unit -enum TempUnit: String { - case metric, imperial - var expression: String { - switch self { - case .metric: return "℃" - case .imperial: return "℉" - } - } -} - diff --git a/WeatherForecast/WeatherForecast/Source/Model/WeatherJSON.swift b/WeatherForecast/WeatherForecast/Source/Model/WeatherJSON.swift new file mode 100644 index 0000000..872e446 --- /dev/null +++ b/WeatherForecast/WeatherForecast/Source/Model/WeatherJSON.swift @@ -0,0 +1,25 @@ +// +// WeatherJSON.swift +// WeatherForecast +// +// Created by 홍승완 on 2024/03/12. +// + +// MARK: - Weather JSON Format +protocol WeatherDataProtocol { + var weatherForecast: [WeatherForecastInfo] { get } + var city: City { get } +} + +struct WeatherJSON: Decodable, WeatherDataProtocol { + private let _weatherForecast: [WeatherForecastInfo] + private let _city: City + + var weatherForecast: [WeatherForecastInfo] { + return _weatherForecast + } + + var city: City { + return _city + } +} diff --git a/WeatherForecast/WeatherForecast/Source/Scene/Weather/WeatherTableViewCell.swift b/WeatherForecast/WeatherForecast/Source/Scene/Weather/WeatherTableViewCell.swift new file mode 100644 index 0000000..7ecdc90 --- /dev/null +++ b/WeatherForecast/WeatherForecast/Source/Scene/Weather/WeatherTableViewCell.swift @@ -0,0 +1,113 @@ +// +// WeatherForecast - WeatherTableViewCell.swift +// Created by yagom. +// Copyright © yagom. All rights reserved. +// + +import UIKit + +final class WeatherTableViewCell: UITableViewCell { + static let identifier = String(describing: WeatherTableViewCell.self) + + let weatherIcon: UIImageView = UIImageView() + let dateLabel: UILabel = UILabel() + let temperatureLabel: UILabel = UILabel() + let weatherLabel: UILabel = UILabel() + let descriptionLabel: UILabel = UILabel() + let dashLabel: UILabel = UILabel() + + override init(style: UITableViewCell.CellStyle, + reuseIdentifier: String?) { + super.init(style: style, + reuseIdentifier: reuseIdentifier) + layViews() + reset() + } + + required init?(coder: NSCoder) { + fatalError() + } + + override func prepareForReuse() { + super.prepareForReuse() + reset() + } + + private func layViews() { + let labels: [UILabel] = [dateLabel, + temperatureLabel, + weatherLabel, + dashLabel, + descriptionLabel] + + labels.forEach { label in + label.textColor = .black + label.font = .preferredFont(forTextStyle: .body) + label.numberOfLines = 1 + } + + let weatherStackView: UIStackView = UIStackView(arrangedSubviews: [weatherLabel, + dashLabel, + descriptionLabel]) + + descriptionLabel.setContentHuggingPriority(.defaultLow, + for: .horizontal) + + weatherStackView.axis = .horizontal + weatherStackView.spacing = 8 + weatherStackView.alignment = .center + weatherStackView.distribution = .fill + + let verticalStackView: UIStackView = UIStackView(arrangedSubviews: [dateLabel, + temperatureLabel, + weatherStackView]) + + verticalStackView.axis = .vertical + verticalStackView.spacing = 8 + verticalStackView.distribution = .fill + verticalStackView.alignment = .leading + + let contentsStackView: UIStackView = UIStackView(arrangedSubviews: [weatherIcon, + verticalStackView]) + + contentsStackView.axis = .horizontal + contentsStackView.spacing = 16 + contentsStackView.alignment = .center + contentsStackView.distribution = .fill + contentsStackView.translatesAutoresizingMaskIntoConstraints = false + + contentView.addSubview(contentsStackView) + + NSLayoutConstraint.activate([contentsStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), + contentsStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + contentsStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), + contentsStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16), + weatherIcon.widthAnchor.constraint(equalTo: weatherIcon.heightAnchor), + weatherIcon.widthAnchor.constraint(equalToConstant: 100)]) + } + + private func reset() { + weatherIcon.image = UIImage(systemName: "arrow.down.circle.dotted") + dateLabel.text = "0000-00-00 00:00:00" + temperatureLabel.text = "00℃" + weatherLabel.text = "~~~" + descriptionLabel.text = "~~~~~" + } + + func configure(weatherForecastInfo: WeatherForecastInfo, tempUnit: TempUnit, imageService: WeatherImageService) { + let date: Date = Date(timeIntervalSince1970: weatherForecastInfo.dt) + let iconName: String = weatherForecastInfo.weather.icon + + weatherLabel.text = weatherForecastInfo.weather.main + descriptionLabel.text = weatherForecastInfo.weather.description + temperatureLabel.text = "\(weatherForecastInfo.main.temp)\(tempUnit.symbol)" + dateLabel.text = DateFormatter.convertToKorean(by: date) + + imageService.fetchImage(iconName: iconName) { image in + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + weatherIcon.image = image + } + } + } +} diff --git a/WeatherForecast/WeatherForecast/Source/Scene/Weather/WeatherViewController.swift b/WeatherForecast/WeatherForecast/Source/Scene/Weather/WeatherViewController.swift new file mode 100644 index 0000000..2157241 --- /dev/null +++ b/WeatherForecast/WeatherForecast/Source/Scene/Weather/WeatherViewController.swift @@ -0,0 +1,142 @@ +// +// WeatherForecast - ViewController.swift +// Created by yagom. +// Copyright © yagom. All rights reserved. +// + +import UIKit + +final class WeatherViewController: UIViewController { + private let tableView: UITableView = .init(frame: .zero, style: .plain) + private let refreshControl: UIRefreshControl = UIRefreshControl() + + private var weatherJSON: WeatherDataProtocol? + private var tempUnit: TempUnit + + private let weatherService: WeatherJSONService + private let imageService: WeatherImageService + + init(tempUnit: TempUnit, + weatherService: WeatherJSONService, + imageService: WeatherImageService) { + self.tempUnit = tempUnit + self.weatherService = weatherService + self.imageService = imageService + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + initialSetUp() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + weatherService.fetchWeather { [weak self] weatherJSON in + guard let self = self else { return } + + self.weatherJSON = weatherJSON + navigationItem.title = weatherJSON.city.name + } + } +} + +extension WeatherViewController { + @objc private func changeTempUnit() { + tempUnit.toggle() + refresh() + } + + @objc private func refresh() { + tableView.reloadData() + refreshControl.endRefreshing() + navigationItem.title = weatherJSON?.city.name + } + + private func initialSetUp() { + view.backgroundColor = .systemBackground + + navigationItem.rightBarButtonItem = UIBarButtonItem(title: tempUnit.description, + image: nil, + target: self, + action: #selector(changeTempUnit)) + navigationItem.title = weatherJSON?.city.name + + layTable() + + refreshControl.addTarget(self, + action: #selector(refresh), + for: .valueChanged) + + tableView.refreshControl = refreshControl + tableView.register(WeatherTableViewCell.self, + forCellReuseIdentifier: WeatherTableViewCell.identifier) + tableView.dataSource = self + tableView.delegate = self + } + + private func layTable() { + 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 WeatherViewController: UITableViewDataSource { + + func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return weatherJSON?.weatherForecast.count ?? 0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: WeatherTableViewCell.identifier, for: indexPath) + + guard let cell: WeatherTableViewCell = cell as? WeatherTableViewCell, + let weatherForecastInfo = weatherJSON?.weatherForecast[indexPath.row] else { + return cell + } + + cell.configure(weatherForecastInfo: weatherForecastInfo, + tempUnit: tempUnit, + imageService: imageService) + + return cell + } +} + +extension WeatherViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let weatherForecastInfo = weatherJSON?.weatherForecast[indexPath.row] + let city = weatherJSON?.city + let weatherDetailInfo = WeatherDetailInfo(weatherForecastInfo: weatherForecastInfo, + cityInfo: city, + tempUnit: tempUnit) + let detailViewController: WeatherDetailViewController = WeatherDetailViewController(weatherDetailInfo: weatherDetailInfo, + imageService: imageService) + + navigationController?.show(detailViewController, sender: self) + } +} + + diff --git a/WeatherForecast/WeatherForecast/Source/Scene/WeatherDetail/WeatherDetailView.swift b/WeatherForecast/WeatherForecast/Source/Scene/WeatherDetail/WeatherDetailView.swift new file mode 100644 index 0000000..9f28043 --- /dev/null +++ b/WeatherForecast/WeatherForecast/Source/Scene/WeatherDetail/WeatherDetailView.swift @@ -0,0 +1,81 @@ +// +// WeatherDetailView.swift +// WeatherForecast +// +// Created by 홍승완 on 2024/03/13. +// + +import UIKit + +final class WeatherDetailView: UIView { + 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() + let mainStackView: UIStackView = UIStackView() + + override init(frame: CGRect) { + super.init(frame: .zero) + + setupViews() + setupLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setupViews() { + [iconImageView, + weatherGroupLabel, + weatherDescriptionLabel, + temperatureLabel, + feelsLikeLabel, + maximumTemperatureLable, + minimumTemperatureLable, + popLabel, + humidityLabel, + sunriseTimeLabel, + sunsetTimeLabel, + spacingView + ].forEach { view in + mainStackView.addArrangedSubview(view) + } + + 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 + } + + func setupLayout() { + 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/Source/Scene/WeatherDetail/WeatherDetailViewController.swift b/WeatherForecast/WeatherForecast/Source/Scene/WeatherDetail/WeatherDetailViewController.swift new file mode 100644 index 0000000..d8f13e1 --- /dev/null +++ b/WeatherForecast/WeatherForecast/Source/Scene/WeatherDetail/WeatherDetailViewController.swift @@ -0,0 +1,82 @@ +// +// WeatherForecast - WeatherDetailViewController.swift +// Created by yagom. +// Copyright © yagom. All rights reserved. +// + +import UIKit + +struct WeatherDetailInfo { + let weatherForecastInfo: WeatherForecastInfo? + let cityInfo: City? + let tempUnit: TempUnit +} + +final class WeatherDetailViewController: UIViewController { + private let weatherDetailInfo: WeatherDetailInfo + private let contentView = WeatherDetailView() + + private let imageService: WeatherImageService + + init(weatherDetailInfo: WeatherDetailInfo, + imageService: WeatherImageService) { + + self.weatherDetailInfo = weatherDetailInfo + self.imageService = imageService + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { + super.viewDidLoad() + + initialSetUp() + } + + override func loadView() { + view = contentView + } + + private func initialSetUp() { + view.backgroundColor = .white + contentView.spacingView.backgroundColor = .clear + contentView.spacingView.setContentHuggingPriority(.defaultLow, for: .vertical) + contentView.weatherGroupLabel.font = .preferredFont(forTextStyle: .largeTitle) + contentView.weatherDescriptionLabel.font = .preferredFont(forTextStyle: .largeTitle) + + guard let listInfo = weatherDetailInfo.weatherForecastInfo, + let cityInfo = weatherDetailInfo.cityInfo else { + return + } + + let date: Date = Date(timeIntervalSince1970: listInfo.dt) + navigationItem.title = DateFormatter.convertToKorean(by: date) + + contentView.weatherGroupLabel.text = listInfo.weather.main + contentView.weatherDescriptionLabel.text = listInfo.weather.description + contentView.temperatureLabel.text = "현재 기온 : \(listInfo.main.temp)\(weatherDetailInfo.tempUnit.symbol)" + contentView.feelsLikeLabel.text = "체감 기온 : \(listInfo.main.feelsLike)\(weatherDetailInfo.tempUnit.symbol)" + contentView.maximumTemperatureLable.text = "최고 기온 : \(listInfo.main.tempMax)\(weatherDetailInfo.tempUnit.symbol)" + contentView.minimumTemperatureLable.text = "최저 기온 : \(listInfo.main.tempMin)\(weatherDetailInfo.tempUnit.symbol)" + contentView.popLabel.text = "강수 확률 : \(listInfo.main.pop * 100)%" + contentView.humidityLabel.text = "습도 : \(listInfo.main.humidity)%" + + let sunriseDate = Date(timeIntervalSince1970: cityInfo.sunrise) + let sunsetDate = Date(timeIntervalSince1970: cityInfo.sunset) + + contentView.sunriseTimeLabel.text = "일출 : \(DateFormatter.convertToCityTime(by: sunriseDate))" + contentView.sunsetTimeLabel.text = "일몰 : \(DateFormatter.convertToCityTime(by: sunsetDate))" + + let iconName: String = listInfo.weather.icon + + imageService.fetchImage(iconName: iconName) { image in + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + contentView.iconImageView.image = image + } + } + } +} diff --git a/WeatherForecast/WeatherForecast/Source/Service/WeatherImageService.swift b/WeatherForecast/WeatherForecast/Source/Service/WeatherImageService.swift new file mode 100644 index 0000000..a3565f1 --- /dev/null +++ b/WeatherForecast/WeatherForecast/Source/Service/WeatherImageService.swift @@ -0,0 +1,32 @@ +// +// WeatherImageService.swift +// WeatherForecast +// +// Created by 홍승완 on 2024/03/13. +// + + +import UIKit + +final class WeatherImageService { + private let imageCache: NSCache = NSCache() + + func fetchImage(iconName: String, completion: @escaping (UIImage) -> ()) { + let urlString: String = "https://openweathermap.org/img/wn/\(iconName)@2x.png" + + if let image = imageCache.object(forKey: urlString as NSString) { + completion(image) + } + + 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 + } + + imageCache.setObject(image, forKey: urlString as NSString) + completion(image) + } + } +} diff --git a/WeatherForecast/WeatherForecast/Source/Service/WeatherJSONService.swift b/WeatherForecast/WeatherForecast/Source/Service/WeatherJSONService.swift new file mode 100644 index 0000000..b17dfc9 --- /dev/null +++ b/WeatherForecast/WeatherForecast/Source/Service/WeatherJSONService.swift @@ -0,0 +1,31 @@ +// +// WeatherService.swift +// WeatherForecast +// +// Created by 홍승완 on 2024/03/12. +// + +import Foundation +import UIKit + +final class WeatherJSONService { + func fetchWeather(completion: @escaping (WeatherJSON) -> ()) { + 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 + } + + completion(info) + } +} diff --git a/WeatherForecast/WeatherForecast/Source/Utils/Extensions/DateFormatter+.swift b/WeatherForecast/WeatherForecast/Source/Utils/Extensions/DateFormatter+.swift new file mode 100644 index 0000000..11b0be7 --- /dev/null +++ b/WeatherForecast/WeatherForecast/Source/Utils/Extensions/DateFormatter+.swift @@ -0,0 +1,31 @@ +// +// DateFormatter+.swift +// WeatherForecast +// +// Created by 홍승완 on 2024/03/12. +// + +import Foundation + +extension DateFormatter { + static func convertToKorean(by date: Date) -> String { + let formatter: DateFormatter = DateFormatter() + formatter.locale = .init(identifier: "ko_KR") + formatter.dateFormat = "yyyy-MM-dd(EEEEE) a HH:mm" + + let time = formatter.string(from: date) + + return time + } + + static func convertToCityTime(by date: Date) -> String { + let formatter: DateFormatter = DateFormatter() + formatter.dateFormat = .none + formatter.timeStyle = .short + formatter.locale = .init(identifier: "ko_KR") + + let time = formatter.string(from: date) + + return time + } +} diff --git a/WeatherForecast/WeatherForecast/ViewController.swift b/WeatherForecast/WeatherForecast/ViewController.swift deleted file mode 100644 index 50b66fb..0000000 --- a/WeatherForecast/WeatherForecast/ViewController.swift +++ /dev/null @@ -1,166 +0,0 @@ -// -// 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/WeatherDetailViewController.swift b/WeatherForecast/WeatherForecast/WeatherDetailViewController.swift deleted file mode 100644 index 69d3dfb..0000000 --- a/WeatherForecast/WeatherForecast/WeatherDetailViewController.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// WeatherForecast - WeatherDetailViewController.swift -// Created by yagom. -// Copyright © yagom. All rights reserved. -// - -import UIKit - -class WeatherDetailViewController: UIViewController { - - 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 - }() - - override func viewDidLoad() { - super.viewDidLoad() - initialSetUp() - } - - private func initialSetUp() { - view.backgroundColor = .white - - guard let listInfo = weatherForecastInfo else { return } - - let date: Date = Date(timeIntervalSince1970: listInfo.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: [ - 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)))" - } - - 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 - } - } -} diff --git a/WeatherForecast/WeatherForecast/WeatherTableViewCell.swift b/WeatherForecast/WeatherForecast/WeatherTableViewCell.swift deleted file mode 100644 index 42cb519..0000000 --- a/WeatherForecast/WeatherForecast/WeatherTableViewCell.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// WeatherForecast - WeatherTableViewCell.swift -// Created by yagom. -// Copyright © yagom. All rights reserved. -// - -import UIKit - -class WeatherTableViewCell: UITableViewCell { - var weatherIcon: UIImageView! - var dateLabel: UILabel! - var temperatureLabel: UILabel! - var weatherLabel: UILabel! - var descriptionLabel: UILabel! - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - layViews() - reset() - } - - required init?(coder: NSCoder) { - fatalError() - } - - override func prepareForReuse() { - super.prepareForReuse() - reset() - } - - private func layViews() { - weatherIcon = UIImageView() - dateLabel = UILabel() - temperatureLabel = UILabel() - weatherLabel = UILabel() - let dashLabel: UILabel = UILabel() - descriptionLabel = UILabel() - - let labels: [UILabel] = [dateLabel, temperatureLabel, weatherLabel, dashLabel, descriptionLabel] - - labels.forEach { label in - label.textColor = .black - label.font = .preferredFont(forTextStyle: .body) - label.numberOfLines = 1 - } - - let weatherStackView: UIStackView = UIStackView(arrangedSubviews: [ - weatherLabel, - dashLabel, - descriptionLabel - ]) - - descriptionLabel.setContentHuggingPriority(.defaultLow, - for: .horizontal) - - weatherStackView.axis = .horizontal - weatherStackView.spacing = 8 - weatherStackView.alignment = .center - weatherStackView.distribution = .fill - - - let verticalStackView: UIStackView = UIStackView(arrangedSubviews: [ - dateLabel, - temperatureLabel, - weatherStackView - ]) - - verticalStackView.axis = .vertical - verticalStackView.spacing = 8 - verticalStackView.distribution = .fill - verticalStackView.alignment = .leading - - let contentsStackView: UIStackView = UIStackView(arrangedSubviews: [ - weatherIcon, - verticalStackView - ]) - - contentsStackView.axis = .horizontal - contentsStackView.spacing = 16 - contentsStackView.alignment = .center - contentsStackView.distribution = .fill - contentsStackView.translatesAutoresizingMaskIntoConstraints = false - - contentView.addSubview(contentsStackView) - - NSLayoutConstraint.activate([ - contentsStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), - contentsStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), - contentsStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), - contentsStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16), - weatherIcon.widthAnchor.constraint(equalTo: weatherIcon.heightAnchor), - weatherIcon.widthAnchor.constraint(equalToConstant: 100) - ]) - } - - private func reset() { - weatherIcon.image = UIImage(systemName: "arrow.down.circle.dotted") - dateLabel.text = "0000-00-00 00:00:00" - temperatureLabel.text = "00℃" - weatherLabel.text = "~~~" - descriptionLabel.text = "~~~~~" - } -}