Commit 42c32561 by Dmitriy Stepanets

Working on hourly graph view

parent ba2e8e60
...@@ -21,6 +21,8 @@ ...@@ -21,6 +21,8 @@
CD39F2EE25DE858D009FE398 /* NotificationName+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD39F2ED25DE858D009FE398 /* NotificationName+Localization.swift */; }; CD39F2EE25DE858D009FE398 /* NotificationName+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD39F2ED25DE858D009FE398 /* NotificationName+Localization.swift */; };
CD39F2F225DE94C4009FE398 /* CitySunCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD39F2F125DE94C4009FE398 /* CitySunCell.swift */; }; CD39F2F225DE94C4009FE398 /* CitySunCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD39F2F125DE94C4009FE398 /* CitySunCell.swift */; };
CD39F2F525DE9571009FE398 /* ArrowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD39F2F425DE9571009FE398 /* ArrowButton.swift */; }; CD39F2F525DE9571009FE398 /* ArrowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD39F2F425DE9571009FE398 /* ArrowButton.swift */; };
CD647D0225ED07D60034578B /* TodayViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD647D0125ED07D60034578B /* TodayViewModel.swift */; };
CD647D0625ED08050034578B /* ViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD647D0525ED08050034578B /* ViewModelProtocol.swift */; };
CD6B3036257262C2004B34B3 /* UIColor+Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6B3035257262C2004B34B3 /* UIColor+Highlight.swift */; }; CD6B3036257262C2004B34B3 /* UIColor+Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6B3035257262C2004B34B3 /* UIColor+Highlight.swift */; };
CD6B303B2572680C004B34B3 /* SelfSizingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6B303A2572680C004B34B3 /* SelfSizingButton.swift */; }; CD6B303B2572680C004B34B3 /* SelfSizingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6B303A2572680C004B34B3 /* SelfSizingButton.swift */; };
CD6B303E25726960004B34B3 /* ThemeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6B303D25726960004B34B3 /* ThemeProtocol.swift */; }; CD6B303E25726960004B34B3 /* ThemeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6B303D25726960004B34B3 /* ThemeProtocol.swift */; };
...@@ -39,6 +41,9 @@ ...@@ -39,6 +41,9 @@
CD86246C25E6826A0097F3FB /* InnerShadowLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD86246B25E6826A0097F3FB /* InnerShadowLayer.swift */; }; CD86246C25E6826A0097F3FB /* InnerShadowLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD86246B25E6826A0097F3FB /* InnerShadowLayer.swift */; };
CD9B6B1125DBC723001D9B80 /* CubicCurveAlgorithm.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9B6B1025DBC723001D9B80 /* CubicCurveAlgorithm.swift */; }; CD9B6B1125DBC723001D9B80 /* CubicCurveAlgorithm.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9B6B1025DBC723001D9B80 /* CubicCurveAlgorithm.swift */; };
CD9B6B1425DBCDE2001D9B80 /* GraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9B6B1325DBCDE2001D9B80 /* GraphView.swift */; }; CD9B6B1425DBCDE2001D9B80 /* GraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9B6B1325DBCDE2001D9B80 /* GraphView.swift */; };
CDA5542825EF734200A2E08C /* TodayCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA5542725EF734200A2E08C /* TodayCellFactory.swift */; };
CDA5542D25EF7C9700A2E08C /* ReusableCellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA5542C25EF7C9700A2E08C /* ReusableCellProtocol.swift */; };
CDA5543025EFA13F00A2E08C /* Measurement+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA5542F25EFA13F00A2E08C /* Measurement+String.swift */; };
CDC6124F25E7964700188DA7 /* CityDayTimesCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC6124E25E7964700188DA7 /* CityDayTimesCell.swift */; }; CDC6124F25E7964700188DA7 /* CityDayTimesCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC6124E25E7964700188DA7 /* CityDayTimesCell.swift */; };
CDC6125325E79C8F00188DA7 /* DayTimeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC6125225E79C8F00188DA7 /* DayTimeView.swift */; }; CDC6125325E79C8F00188DA7 /* DayTimeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC6125225E79C8F00188DA7 /* DayTimeView.swift */; };
CDC6125725E7AB1A00188DA7 /* CityAirQualityCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC6125625E7AB1A00188DA7 /* CityAirQualityCell.swift */; }; CDC6125725E7AB1A00188DA7 /* CityAirQualityCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC6125625E7AB1A00188DA7 /* CityAirQualityCell.swift */; };
...@@ -97,6 +102,8 @@ ...@@ -97,6 +102,8 @@
CD39F2ED25DE858D009FE398 /* NotificationName+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationName+Localization.swift"; sourceTree = "<group>"; }; CD39F2ED25DE858D009FE398 /* NotificationName+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationName+Localization.swift"; sourceTree = "<group>"; };
CD39F2F125DE94C4009FE398 /* CitySunCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CitySunCell.swift; sourceTree = "<group>"; }; CD39F2F125DE94C4009FE398 /* CitySunCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CitySunCell.swift; sourceTree = "<group>"; };
CD39F2F425DE9571009FE398 /* ArrowButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrowButton.swift; sourceTree = "<group>"; }; CD39F2F425DE9571009FE398 /* ArrowButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrowButton.swift; sourceTree = "<group>"; };
CD647D0125ED07D60034578B /* TodayViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayViewModel.swift; sourceTree = "<group>"; };
CD647D0525ED08050034578B /* ViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelProtocol.swift; sourceTree = "<group>"; };
CD6B3035257262C2004B34B3 /* UIColor+Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Highlight.swift"; sourceTree = "<group>"; }; CD6B3035257262C2004B34B3 /* UIColor+Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Highlight.swift"; sourceTree = "<group>"; };
CD6B303A2572680C004B34B3 /* SelfSizingButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelfSizingButton.swift; sourceTree = "<group>"; }; CD6B303A2572680C004B34B3 /* SelfSizingButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelfSizingButton.swift; sourceTree = "<group>"; };
CD6B303D25726960004B34B3 /* ThemeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeProtocol.swift; sourceTree = "<group>"; }; CD6B303D25726960004B34B3 /* ThemeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeProtocol.swift; sourceTree = "<group>"; };
...@@ -115,6 +122,9 @@ ...@@ -115,6 +122,9 @@
CD86246B25E6826A0097F3FB /* InnerShadowLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InnerShadowLayer.swift; sourceTree = "<group>"; }; CD86246B25E6826A0097F3FB /* InnerShadowLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InnerShadowLayer.swift; sourceTree = "<group>"; };
CD9B6B1025DBC723001D9B80 /* CubicCurveAlgorithm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CubicCurveAlgorithm.swift; sourceTree = "<group>"; }; CD9B6B1025DBC723001D9B80 /* CubicCurveAlgorithm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CubicCurveAlgorithm.swift; sourceTree = "<group>"; };
CD9B6B1325DBCDE2001D9B80 /* GraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphView.swift; sourceTree = "<group>"; }; CD9B6B1325DBCDE2001D9B80 /* GraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphView.swift; sourceTree = "<group>"; };
CDA5542725EF734200A2E08C /* TodayCellFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayCellFactory.swift; sourceTree = "<group>"; };
CDA5542C25EF7C9700A2E08C /* ReusableCellProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableCellProtocol.swift; sourceTree = "<group>"; };
CDA5542F25EFA13F00A2E08C /* Measurement+String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Measurement+String.swift"; sourceTree = "<group>"; };
CDC6124E25E7964700188DA7 /* CityDayTimesCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityDayTimesCell.swift; sourceTree = "<group>"; }; CDC6124E25E7964700188DA7 /* CityDayTimesCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityDayTimesCell.swift; sourceTree = "<group>"; };
CDC6125225E79C8F00188DA7 /* DayTimeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayTimeView.swift; sourceTree = "<group>"; }; CDC6125225E79C8F00188DA7 /* DayTimeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayTimeView.swift; sourceTree = "<group>"; };
CDC6125625E7AB1A00188DA7 /* CityAirQualityCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityAirQualityCell.swift; sourceTree = "<group>"; }; CDC6125625E7AB1A00188DA7 /* CityAirQualityCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityAirQualityCell.swift; sourceTree = "<group>"; };
...@@ -201,6 +211,7 @@ ...@@ -201,6 +211,7 @@
CE9D181425ECB8370028D9D7 /* Common */, CE9D181425ECB8370028D9D7 /* Common */,
CEAFF09925DFC78200DF4EBF /* Network */, CEAFF09925DFC78200DF4EBF /* Network */,
CEAFF08125DFC66F00DF4EBF /* Model */, CEAFF08125DFC66F00DF4EBF /* Model */,
CD647D0025ED07AF0034578B /* ViewModels */,
CD17C5F925D15B5500EE884E /* Coordinators */, CD17C5F925D15B5500EE884E /* Coordinators */,
CDD0F1DC2572400200CF5017 /* UI */, CDD0F1DC2572400200CF5017 /* UI */,
CD1237EF255D838600C98139 /* Extensions */, CD1237EF255D838600C98139 /* Extensions */,
...@@ -220,6 +231,7 @@ ...@@ -220,6 +231,7 @@
CD80917A2578E4A8003541A4 /* UIViewController+Alert.swift */, CD80917A2578E4A8003541A4 /* UIViewController+Alert.swift */,
CDE18DC925D165F100C80ED9 /* UITabBarController+Append.swift */, CDE18DC925D165F100C80ED9 /* UITabBarController+Append.swift */,
CD39F2ED25DE858D009FE398 /* NotificationName+Localization.swift */, CD39F2ED25DE858D009FE398 /* NotificationName+Localization.swift */,
CDA5542F25EFA13F00A2E08C /* Measurement+String.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -252,6 +264,15 @@ ...@@ -252,6 +264,15 @@
path = Coordinators; path = Coordinators;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
CD647D0025ED07AF0034578B /* ViewModels */ = {
isa = PBXGroup;
children = (
CD647D0125ED07D60034578B /* TodayViewModel.swift */,
CD647D0525ED08050034578B /* ViewModelProtocol.swift */,
);
path = ViewModels;
sourceTree = "<group>";
};
CD6B3038257267E2004B34B3 /* View controllers */ = { CD6B3038257267E2004B34B3 /* View controllers */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
...@@ -284,6 +305,7 @@ ...@@ -284,6 +305,7 @@
CD822FF325D6814300A05501 /* Cells */ = { CD822FF325D6814300A05501 /* Cells */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
CDA5542725EF734200A2E08C /* TodayCellFactory.swift */,
CD82300125D69DB900A05501 /* CityConditions */, CD82300125D69DB900A05501 /* CityConditions */,
CDEE8AD425DA87DD00C289DE /* CityForecastTimePeriod */, CDEE8AD425DA87DD00C289DE /* CityForecastTimePeriod */,
CD86246325E66E6B0097F3FB /* CityPrecipCell */, CD86246325E66E6B0097F3FB /* CityPrecipCell */,
...@@ -418,6 +440,7 @@ ...@@ -418,6 +440,7 @@
children = ( children = (
CE9D181825ECB9A70028D9D7 /* Logger.swift */, CE9D181825ECB9A70028D9D7 /* Logger.swift */,
CE9D181525ECB8370028D9D7 /* MulticastDelegate.swift */, CE9D181525ECB8370028D9D7 /* MulticastDelegate.swift */,
CDA5542C25EF7C9700A2E08C /* ReusableCellProtocol.swift */,
); );
path = Common; path = Common;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -537,6 +560,7 @@ ...@@ -537,6 +560,7 @@
hasScannedForEncodings = 0; hasScannedForEncodings = 0;
knownRegions = ( knownRegions = (
en, en,
Base,
); );
mainGroup = CD1237B6255D5C5900C98139; mainGroup = CD1237B6255D5C5900C98139;
productRefGroup = CD1237C0255D5C5900C98139 /* Products */; productRefGroup = CD1237C0255D5C5900C98139 /* Products */;
...@@ -612,6 +636,7 @@ ...@@ -612,6 +636,7 @@
CD82300325D69DE400A05501 /* CityConditionsCell.swift in Sources */, CD82300325D69DE400A05501 /* CityConditionsCell.swift in Sources */,
CEC526FD25E795F700DA58A5 /* WdtWeatherSource.swift in Sources */, CEC526FD25E795F700DA58A5 /* WdtWeatherSource.swift in Sources */,
CEAFF09225DFC71D00DF4EBF /* HelperTypes.swift in Sources */, CEAFF09225DFC71D00DF4EBF /* HelperTypes.swift in Sources */,
CDA5543025EFA13F00A2E08C /* Measurement+String.swift in Sources */,
CE9D181925ECB9A70028D9D7 /* Logger.swift in Sources */, CE9D181925ECB9A70028D9D7 /* Logger.swift in Sources */,
CEAFF08925DFC6B200DF4EBF /* CurrentWeather.swift in Sources */, CEAFF08925DFC6B200DF4EBF /* CurrentWeather.swift in Sources */,
CEC5270325E7BB4000DA58A5 /* WdtSurfaceObservation.swift in Sources */, CEC5270325E7BB4000DA58A5 /* WdtSurfaceObservation.swift in Sources */,
...@@ -632,6 +657,7 @@ ...@@ -632,6 +657,7 @@
CEAFF08F25DFC6ED00DF4EBF /* HourlyWeather.swift in Sources */, CEAFF08F25DFC6ED00DF4EBF /* HourlyWeather.swift in Sources */,
CDE18DD825D16CB200C80ED9 /* NavigationCityButton.swift in Sources */, CDE18DD825D16CB200C80ED9 /* NavigationCityButton.swift in Sources */,
CD17C60225D15C8500EE884E /* CoordinatorProtocol.swift in Sources */, CD17C60225D15C8500EE884E /* CoordinatorProtocol.swift in Sources */,
CDA5542825EF734200A2E08C /* TodayCellFactory.swift in Sources */,
CD17C5FF25D15B7C00EE884E /* TodayCoordinator.swift in Sources */, CD17C5FF25D15B7C00EE884E /* TodayCoordinator.swift in Sources */,
CD822FF525D6817000A05501 /* CityForecastCell.swift in Sources */, CD822FF525D6817000A05501 /* CityForecastCell.swift in Sources */,
CEDE4E8525EEFD56007457E9 /* WdtHourlySummariesArray.swift in Sources */, CEDE4E8525EEFD56007457E9 /* WdtHourlySummariesArray.swift in Sources */,
...@@ -652,6 +678,7 @@ ...@@ -652,6 +678,7 @@
CD6B303B2572680C004B34B3 /* SelfSizingButton.swift in Sources */, CD6B303B2572680C004B34B3 /* SelfSizingButton.swift in Sources */,
CD9B6B1125DBC723001D9B80 /* CubicCurveAlgorithm.swift in Sources */, CD9B6B1125DBC723001D9B80 /* CubicCurveAlgorithm.swift in Sources */,
CEC5270025E7BACB00DA58A5 /* WdtLocation.swift in Sources */, CEC5270025E7BACB00DA58A5 /* WdtLocation.swift in Sources */,
CD647D0225ED07D60034578B /* TodayViewModel.swift in Sources */,
CD15DB4225DA806C00024727 /* CityForecastTimePeriodCell.swift in Sources */, CD15DB4225DA806C00024727 /* CityForecastTimePeriodCell.swift in Sources */,
CEC5276025E92DDA00DA58A5 /* WdtHourlySummary.swift in Sources */, CEC5276025E92DDA00DA58A5 /* WdtHourlySummary.swift in Sources */,
CDE18DCA25D165F100C80ED9 /* UITabBarController+Append.swift in Sources */, CDE18DCA25D165F100C80ED9 /* UITabBarController+Append.swift in Sources */,
...@@ -669,6 +696,8 @@ ...@@ -669,6 +696,8 @@
CEAFF08325DFC67F00DF4EBF /* Location.swift in Sources */, CEAFF08325DFC67F00DF4EBF /* Location.swift in Sources */,
CD82300725D6A73F00A05501 /* CityConditionButton.swift in Sources */, CD82300725D6A73F00A05501 /* CityConditionButton.swift in Sources */,
CDC6126A25E90C8800188DA7 /* GraphLineSettings.swift in Sources */, CDC6126A25E90C8800188DA7 /* GraphLineSettings.swift in Sources */,
CDA5542D25EF7C9700A2E08C /* ReusableCellProtocol.swift in Sources */,
CD647D0625ED08050034578B /* ViewModelProtocol.swift in Sources */,
CD86246C25E6826A0097F3FB /* InnerShadowLayer.swift in Sources */, CD86246C25E6826A0097F3FB /* InnerShadowLayer.swift in Sources */,
CEAFF0A325E0FF0800DF4EBF /* LocationManager.swift in Sources */, CEAFF0A325E0FF0800DF4EBF /* LocationManager.swift in Sources */,
CEAD00A12577B2D5003596AD /* StuffThatIsPresentInTheMainProject.swift in Sources */, CEAD00A12577B2D5003596AD /* StuffThatIsPresentInTheMainProject.swift in Sources */,
......
//
// ReusableCellProtocol.swift
// 1Weather
//
// Created by Dmitry Stepanets on 03.03.2021.
//
import UIKit
public protocol ReusableCellProtocol: class {
static var kIdentifier:String { get }
}
extension UITableViewCell: ReusableCellProtocol {
public static var kIdentifier: String {
let identifier = String(describing: self)
if identifier == "UITableViewCell" {
assertionFailure("Invalid reuse identifier name. Please provide a valid name.")
}
return String(describing: self)
}
}
...@@ -9,6 +9,7 @@ import UIKit ...@@ -9,6 +9,7 @@ import UIKit
class TodayCoordinator:Coordinator { class TodayCoordinator:Coordinator {
//Private //Private
private let todayViewModel = TodayViewModel()
private let navigationController = UINavigationController(nibName: nil, bundle: nil) private let navigationController = UINavigationController(nibName: nil, bundle: nil)
private var tabBarController:UITabBarController? private var tabBarController:UITabBarController?
...@@ -21,7 +22,7 @@ class TodayCoordinator:Coordinator { ...@@ -21,7 +22,7 @@ class TodayCoordinator:Coordinator {
} }
func start() { func start() {
let todayViewController = TodayViewController() let todayViewController = TodayViewController(viewModel: todayViewModel)
navigationController.viewControllers = [todayViewController] navigationController.viewControllers = [todayViewController]
tabBarController?.add(viewController: navigationController) tabBarController?.add(viewController: navigationController)
} }
......
//
// Measurement+String.swift
// 1Weather
//
// Created by Dmitry Stepanets on 03.03.2021.
//
import Foundation
extension Measurement {
private static var formatter:MeasurementFormatter {
let fmt = MeasurementFormatter()
fmt.unitStyle = .medium
fmt.numberFormatter.maximumFractionDigits = 0
return fmt
}
private static var shortFormatter:MeasurementFormatter {
let fmt = MeasurementFormatter()
fmt.unitStyle = .short
fmt.numberFormatter.maximumFractionDigits = 0
return fmt
}
var string:String {
return Measurement.formatter.string(from: self)
}
var shortString:String {
return Measurement.shortFormatter.string(from: self)
}
}
extension Temperature {
var localeValue:Double {
return self.converted(to: .fahrenheit).value.rounded(.down)
}
}
...@@ -20,6 +20,10 @@ ...@@ -20,6 +20,10 @@
<string>1</string> <string>1</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>UIAppFonts</key>
<array>
<string>SF-Pro.ttf</string>
</array>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
...@@ -31,8 +35,6 @@ ...@@ -31,8 +35,6 @@
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
...@@ -41,9 +43,5 @@ ...@@ -41,9 +43,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UIAppFonts</key>
<array>
<string>SF-Pro.ttf</string>
</array>
</dict> </dict>
</plist> </plist>
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
import Foundation import Foundation
public enum WeatherType: String { public enum WeatherType: String, CaseIterable {
case clearDay = "clearDay" case clearDay = "clearDay"
case partlyCloudyDay = "partlyCloudyDay" case partlyCloudyDay = "partlyCloudyDay"
case snowyDay = "snowyDay" case snowyDay = "snowyDay"
...@@ -30,9 +30,56 @@ public enum WeatherType: String { ...@@ -30,9 +30,56 @@ public enum WeatherType: String {
case heavyRain = "heavyRain" case heavyRain = "heavyRain"
case smoke = "smoke" case smoke = "smoke"
case unknown = "unknown" case unknown = "unknown"
var localized:String {
switch self {
case .clearDay:
return "forecast.clear".localized()
case .partlyCloudyDay:
return "forecast.cloudy".localized()
case .snowyDay:
return "forecast.snowyDay".localized()
case .snowyNight:
return "forecast.snowyNight".localized()
case .clearNight:
return "forecast.clearNight".localized()
case .partlyCloudyNight:
return "forecast.partlyCloudyNight".localized()
case .thunderstorm:
return "forecast.thunderstorm".localized()
case .heavySnow:
return "forecast.heavySnow".localized()
case .lightSnow:
return "forecast.lightSnow".localized()
case .freezingRain:
return "forecast.freezingRain".localized()
case .freezingFog:
return "forecast.freezingFog".localized()
case .lightHail:
return "forecast.lightHail".localized()
case .blowingDust:
return "forecast.blowingDust".localized()
case .haze:
return "forecast.haze".localized()
case .lightFog:
return "forecast.lightFog".localized()
case .mostlyCloudyDay:
return "forecast.mostlyCloudyDay".localized()
case .mostlyCloudyNight:
return "forecast.mostlyCloudyNight".localized()
case .lightDrizzle:
return "forecast.lightDrizzle".localized()
case .heavyRain:
return "forecast.heavyRain".localized()
case .smoke:
return "forecast.smoke".localized()
case .unknown:
return "forecast.unknown".localized()
}
}
} }
public enum WindDirection: String, Codable { public enum WindDirection: String, Codable, CaseIterable {
case north = "N" case north = "N"
case northNorthEast = "NNE" case northNorthEast = "NNE"
case northEast = "NE" case northEast = "NE"
...@@ -84,7 +131,6 @@ public enum MoonPhase: String, Codable { ...@@ -84,7 +131,6 @@ public enum MoonPhase: String, Codable {
} }
public struct Time: CustomStringConvertible, Codable { public struct Time: CustomStringConvertible, Codable {
public let hours: UInt8 public let hours: UInt8
public let minutes: UInt8 public let minutes: UInt8
...@@ -102,6 +148,11 @@ public struct Time: CustomStringConvertible, Codable { ...@@ -102,6 +148,11 @@ public struct Time: CustomStringConvertible, Codable {
return "\(String(format: "%02d", convertedHours)):\(minutes) \(amOrPm)" return "\(String(format: "%02d", convertedHours)):\(minutes) \(amOrPm)"
} }
public init(hours: UInt8, minutes: UInt8) {
self.hours = hours
self.minutes = minutes
}
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let valueContainer = try decoder.singleValueContainer() let valueContainer = try decoder.singleValueContainer()
let value = try valueContainer.decode(String.self).trimmingCharacters(in: .whitespacesAndNewlines).lowercased() let value = try valueContainer.decode(String.self).trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
......
...@@ -10,6 +10,25 @@ ...@@ -10,6 +10,25 @@
"forecast.clear" = "Clear"; "forecast.clear" = "Clear";
"forecast.cloudy" = "Cloudy"; "forecast.cloudy" = "Cloudy";
"forecast.partyCloudy" = "Partly Cloudy"; "forecast.partyCloudy" = "Partly Cloudy";
"forecast.snowyDay" = "Snowy Day";
"forecast.snowyNight" = "Snowy Night";
"forecast.clearNight" = "Clear Night";
"forecast.partlyCloudyNight" = "Partly Cloudy Night";
"forecast.thunderstorm" = "Thunderstorm";
"forecast.heavySnow" = "Heavy Snow";
"forecast.lightSnow" = "Light Snow";
"forecast.freezingRain" = "Freezing Rain";
"forecast.freezingFog" = "Freezing Fog";
"forecast.lightHail" = "Light Hail";
"forecast.blowingDust" = "Blowing Dust";
"forecast.haze" = "Haze";
"forecast.lightFog" = "Light Fog";
"forecast.mostlyCloudyDay" = "Mostly Cloudy Day";
"forecast.mostlyCloudyNight" = "Mostly Cloudy Night";
"forecast.lightDrizzle" = "Light Drizzle";
"forecast.heavyRain" = "Heavy Rain";
"forecast.smoke" = "Smoke";
"forecast.unknown" = "Unknown";
//Day time //Day time
"dayTime.morning" = "Morning"; "dayTime.morning" = "Morning";
...@@ -24,6 +43,7 @@ ...@@ -24,6 +43,7 @@
"condition.pressure" = "Pressure"; "condition.pressure" = "Pressure";
"condition.dewPoint" = "Dew point"; "condition.dewPoint" = "Dew point";
"condition.visibility" = "Visibility"; "condition.visibility" = "Visibility";
"condition.wind" = "Wind";
//Forecast time period //Forecast time period
"forecast.timePeriod.daily" = "Daily"; "forecast.timePeriod.daily" = "Daily";
......
...@@ -19,6 +19,9 @@ class CubicCurveAlgorithm ...@@ -19,6 +19,9 @@ class CubicCurveAlgorithm
private var secondControlPoints: [CGPoint?] = [] private var secondControlPoints: [CGPoint?] = []
func controlPointsFromPoints(dataPoints: [CGPoint]) -> [CubicCurveSegment] { func controlPointsFromPoints(dataPoints: [CGPoint]) -> [CubicCurveSegment] {
//Clean up
firstControlPoints.removeAll()
secondControlPoints.removeAll()
//Number of Segments //Number of Segments
let count = dataPoints.count - 1 let count = dataPoints.count - 1
......
...@@ -15,7 +15,6 @@ struct LineDot { ...@@ -15,7 +15,6 @@ struct LineDot {
struct GraphLine { struct GraphLine {
//Private //Private
private let cubicCurveAlgorithm = CubicCurveAlgorithm()
private let kIntersectAccuracy:CGFloat = 2 private let kIntersectAccuracy:CGFloat = 2
private var points = [CGPoint]() private var points = [CGPoint]()
private let settings:GraphLineSettings private let settings:GraphLineSettings
...@@ -70,6 +69,7 @@ struct GraphLine { ...@@ -70,6 +69,7 @@ struct GraphLine {
return path return path
} }
let cubicCurveAlgorithm = CubicCurveAlgorithm()
let controlPoints = cubicCurveAlgorithm.controlPointsFromPoints(dataPoints: pointsToAdd) let controlPoints = cubicCurveAlgorithm.controlPointsFromPoints(dataPoints: pointsToAdd)
for index in 1..<pointsToAdd.count { for index in 1..<pointsToAdd.count {
sections.append(.init(p0: pointsToAdd[index - 1], sections.append(.init(p0: pointsToAdd[index - 1],
...@@ -151,8 +151,12 @@ struct GraphLine { ...@@ -151,8 +151,12 @@ struct GraphLine {
self.sections.removeAll() self.sections.removeAll()
self.lineDots.removeAll() self.lineDots.removeAll()
self.points = points self.points = points
self.tintLineShape.path = nil
guard !self.points.isEmpty else { return } guard !self.points.isEmpty else {
lineShape.path = nil
return
}
//Add new dots //Add new dots
self.points.forEach { self.points.forEach {
......
...@@ -8,8 +8,6 @@ ...@@ -8,8 +8,6 @@
import UIKit import UIKit
class CityAirQualityCell: UITableViewCell { class CityAirQualityCell: UITableViewCell {
static let kIdentifier = "cityAirQualityCell"
//Private //Private
private let headingLabel = UILabel() private let headingLabel = UILabel()
private let valueCircle = CAShapeLayer() private let valueCircle = CAShapeLayer()
......
...@@ -14,16 +14,19 @@ enum CityConditionButtonType: CaseIterable { ...@@ -14,16 +14,19 @@ enum CityConditionButtonType: CaseIterable {
case pressure case pressure
case dewPoint case dewPoint
case visibility case visibility
case wind
} }
class CityConditionButton: UIControl { class CityConditionButton: UIControl {
//Private //Private
private let buttonType:CityConditionButtonType
private let imageView = UIImageView() private let imageView = UIImageView()
private let valueLabel = UILabel() private let valueLabel = UILabel()
private let nameLabel = UILabel() private let nameLabel = UILabel()
private let descriptionLabel = UILabel() private let descriptionLabel = UILabel()
init(type: CityConditionButtonType) { init(type: CityConditionButtonType) {
self.buttonType = type
super.init(frame: .zero) super.init(frame: .zero)
prepareButton() prepareButton()
...@@ -31,6 +34,27 @@ class CityConditionButton: UIControl { ...@@ -31,6 +34,27 @@ class CityConditionButton: UIControl {
prepareLabels(type: type) prepareLabels(type: type)
} }
public func configure(with location:Location) {
switch buttonType {
case .precipitation:
valueLabel.text = "\(location.today?.precipitationProbability ?? 0)%"
case .humidity:
valueLabel.text = "\(location.today?.humidity ?? 0)%"
case .uvIndex:
valueLabel.text = "4"
case .pressure:
valueLabel.text = location.today?.pressure?.string
case .dewPoint:
valueLabel.text = "12°"
case .visibility:
valueLabel.text = location.today?.visibility?.string
case .wind:
let speed = location.today?.windSpeed?.string ?? "--"
let direction = location.today?.windDirection?.rawValue ?? ""
valueLabel.text = "\(direction) \(speed)"
}
}
private func image(for type:CityConditionButtonType) -> UIImage? { private func image(for type:CityConditionButtonType) -> UIImage? {
switch type { switch type {
case .precipitation: case .precipitation:
...@@ -45,6 +69,8 @@ class CityConditionButton: UIControl { ...@@ -45,6 +69,8 @@ class CityConditionButton: UIControl {
return UIImage(named: "dewPoint") return UIImage(named: "dewPoint")
case .visibility: case .visibility:
return UIImage(named: "visibility") return UIImage(named: "visibility")
case .wind:
return UIImage(named: "blowingDust")
} }
} }
...@@ -62,6 +88,8 @@ class CityConditionButton: UIControl { ...@@ -62,6 +88,8 @@ class CityConditionButton: UIControl {
return "condition.dewPoint".localized() return "condition.dewPoint".localized()
case .visibility: case .visibility:
return "condition.visibility".localized() return "condition.visibility".localized()
case .wind:
return "condition.wind".localized()
} }
} }
......
...@@ -9,8 +9,6 @@ import UIKit ...@@ -9,8 +9,6 @@ import UIKit
import SnapKit import SnapKit
class CityConditionsCell: UITableViewCell { class CityConditionsCell: UITableViewCell {
static let kIdentifier = "cityConditionsCell"
//Private //Private
private let kIndicatorWidth:CGFloat = 27 private let kIndicatorWidth:CGFloat = 27
private let scrollView = UIScrollView() private let scrollView = UIScrollView()
...@@ -29,6 +27,14 @@ class CityConditionsCell: UITableViewCell { ...@@ -29,6 +27,14 @@ class CityConditionsCell: UITableViewCell {
prepareProgress() prepareProgress()
} }
public func configure(with location:Location) {
stackView.arrangedSubviews.forEach {
if let conditionButton = $0 as? CityConditionButton {
conditionButton.configure(with: location)
}
}
}
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
......
...@@ -8,8 +8,6 @@ ...@@ -8,8 +8,6 @@
import UIKit import UIKit
class CityDayTimesCell: UITableViewCell { class CityDayTimesCell: UITableViewCell {
static let kIdentifier = "cityDayTimesCell"
//Private //Private
private let headingLabel = UILabel() private let headingLabel = UILabel()
private let headingButton = ArrowButton() private let headingButton = ArrowButton()
......
...@@ -9,8 +9,6 @@ import UIKit ...@@ -9,8 +9,6 @@ import UIKit
import SnapKit import SnapKit
class CityForecastCell: UITableViewCell { class CityForecastCell: UITableViewCell {
static let kIdentifier = "cityForecastCell"
//Private //Private
private let container = UIView() private let container = UIView()
private let cityImageView = UIImageView() private let cityImageView = UIImageView()
...@@ -34,6 +32,17 @@ class CityForecastCell: UITableViewCell { ...@@ -34,6 +32,17 @@ class CityForecastCell: UITableViewCell {
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
public func configure(with location:Location) {
cityImageView.image = UIImage(named: location.imageName)
temperatureLabel.text = location.today?.temp?.shortString
let maxTemp = location.today?.maxTemp?.shortString ?? "--"
let minTemp = location.today?.minTemp?.shortString ?? "--"
let feelstemp = location.today?.apparentTemp?.shortString ?? "--"
forecastDescriptionLabel.text = "\(location.today?.type.localized ?? "") | \(maxTemp)/\(minTemp)"
feelsLikeLabel.text = "Feels like \(feelstemp) - Due to high humidity"
forecastImageView.image = UIImage(named: location.today?.type.rawValue ?? "")
}
} }
//MARK:- Prepare //MARK:- Prepare
......
...@@ -7,14 +7,21 @@ ...@@ -7,14 +7,21 @@
import UIKit import UIKit
struct DayTemp { private struct DailyGraphPoints {
let min:CGFloat let maxTempPoints: [CGPoint]
let max:CGFloat let minTempPoints: [CGPoint]
}
private struct HourlyGraphPoints {
let points: [CGPoint]
}
private enum TimePeriod: Int {
case daily = 0
case hourly = 1
} }
class CityForecastTimePeriodCell: UITableViewCell { class CityForecastTimePeriodCell: UITableViewCell {
static let kIdentifier = "cityForecastTimePeriodCell"
//Private //Private
private let periodSegmentedControl = ForecastTimePeriodControl(items: ["forecast.timePeriod.daily".localized(), private let periodSegmentedControl = ForecastTimePeriodControl(items: ["forecast.timePeriod.daily".localized(),
"forecast.timePeriod.hourly".localized()]) "forecast.timePeriod.hourly".localized()])
...@@ -24,17 +31,13 @@ class CityForecastTimePeriodCell: UITableViewCell { ...@@ -24,17 +31,13 @@ class CityForecastTimePeriodCell: UITableViewCell {
private let summaryView = UIView() private let summaryView = UIView()
private let summaryImageView = UIImageView() private let summaryImageView = UIImageView()
private let summaryLabel = UILabel() private let summaryLabel = UILabel()
private var maxTempGraphPoints = [CGPoint]()
private var minTempGraphPoints = [CGPoint]() private var location:Location?
private var currentTimePeriod = TimePeriod.daily
private var dailyGraphPoints: DailyGraphPoints?
private var hourlyGraphPoints: HourlyGraphPoints?
private let graphView = GraphView() private let graphView = GraphView()
private let dayTemps = [DayTemp(min: -23, max: -13),
DayTemp(min: -19, max: -12),
DayTemp(min: -12, max: -10),
DayTemp(min: -10, max: -3),
DayTemp(min: -7, max: -3),
DayTemp(min: -11, max: -1),
DayTemp(min: -14, max: -5),
DayTemp(min: -15, max: -7)]
private var graphIsDrawn = false private var graphIsDrawn = false
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
...@@ -53,6 +56,11 @@ class CityForecastTimePeriodCell: UITableViewCell { ...@@ -53,6 +56,11 @@ class CityForecastTimePeriodCell: UITableViewCell {
} }
//Public //Public
public func configure(with location:Location) {
self.location = location
rebuildStackButtons()
}
public func drawGraphIfNeeded() { public func drawGraphIfNeeded() {
guard !graphIsDrawn else { return } guard !graphIsDrawn else { return }
...@@ -75,6 +83,40 @@ class CityForecastTimePeriodCell: UITableViewCell { ...@@ -75,6 +83,40 @@ class CityForecastTimePeriodCell: UITableViewCell {
} }
//Private //Private
private func rebuildStackButtons() {
//Update graph buttons
stackView.arrangedSubviews.forEach {
stackView.removeArrangedSubview($0)
$0.removeFromSuperview()
}
guard let location = self.location else { return }
switch currentTimePeriod {
case .daily:
for index in 0..<location.daily.count {
let conditionButton = PeriodForecastButton()
conditionButton.configure(dailyWeather: location.daily[index])
conditionButton.index = index
conditionButton.addTarget(self, action: #selector(handleConditionButton(button:)), for: .touchUpInside)
conditionButton.isSelected = index == 1
stackView.addArrangedSubview(conditionButton)
}
case .hourly:
for index in 0..<location.hourly.count {
let conditionButton = PeriodForecastButton()
conditionButton.configure(hourlyWeather: location.hourly[index])
conditionButton.index = index
conditionButton.addTarget(self, action: #selector(handleConditionButton(button:)), for: .touchUpInside)
conditionButton.isSelected = index == 1
stackView.addArrangedSubview(conditionButton)
}
}
stackView.layoutIfNeeded()
graphIsDrawn = false
drawGraphIfNeeded()
}
private func drawGraph() { private func drawGraph() {
guard guard
let periodButtons = stackView.arrangedSubviews as? [PeriodForecastButton], let periodButtons = stackView.arrangedSubviews as? [PeriodForecastButton],
...@@ -83,30 +125,70 @@ class CityForecastTimePeriodCell: UITableViewCell { ...@@ -83,30 +125,70 @@ class CityForecastTimePeriodCell: UITableViewCell {
return return
} }
self.graphView.drawMainGraph(with: maxTempGraphPoints) switch currentTimePeriod {
self.graphView.drawAdditionalGraph(with: minTempGraphPoints) case .daily:
self.tintGraphAt(button: selectedButton) guard
let maxTempPoints = dailyGraphPoints?.maxTempPoints,
let minTempsPoints = dailyGraphPoints?.minTempPoints
else {
return
}
self.graphView.drawMainGraph(with: maxTempPoints)
self.graphView.drawAdditionalGraph(with: minTempsPoints)
self.tintGraphAt(button: selectedButton)
case .hourly:
guard let hourlyPoints = hourlyGraphPoints?.points else { return }
self.graphView.drawMainGraph(with: hourlyPoints)
self.graphView.drawAdditionalGraph(with: [CGPoint]())
self.tintGraphAt(button: selectedButton)
}
print("Draw graph!") print("Draw graph!")
} }
private func tintGraphAt(button:PeriodForecastButton) { private func tintGraphAt(button:PeriodForecastButton) {
self.graphView.tintGraphFrom(startPointX: button.frame.origin.x, switch currentTimePeriod {
endPointX: button.frame.origin.x + button.bounds.width) case .daily:
self.graphView.tintMainDotAt(point: maxTempGraphPoints[button.index]) guard
self.graphView.tintAdditionalDotAt(point: minTempGraphPoints[button.index]) let maxTempPoints = dailyGraphPoints?.maxTempPoints,
let minTempsPoints = dailyGraphPoints?.minTempPoints
else {
return
}
self.graphView.tintGraphFrom(startPointX: button.frame.origin.x,
endPointX: button.frame.origin.x + button.bounds.width)
self.graphView.tintMainDotAt(point: maxTempPoints[button.index])
self.graphView.tintAdditionalDotAt(point: minTempsPoints[button.index])
case .hourly:
break
}
} }
private func updateGraphPoints(){ private func updateGraphPoints(){
self.maxTempGraphPoints.removeAll() switch currentTimePeriod {
self.minTempGraphPoints.removeAll() case .daily:
self.updateDailyGraphPoints()
let maxTemps = dayTemps.map{$0.max} case .hourly:
let topMaxTemp = maxTemps.max() ?? 0 self.updateHourlyGraphPoints()
}
let minTemps = dayTemps.map{$0.min} }
let topMinTemp = minTemps.max() ?? 0
private func updateDailyGraphPoints() {
guard
let daysCount = location?.daily.count,
let maxTemps = (location?.daily.map{ CGFloat($0.maxTemp?.localeValue ?? 0) }),
let topMaxTemp = maxTemps.max(),
let minTemps = (location?.daily.map{ CGFloat($0.minTemp?.localeValue ?? 0) }),
let topMinTemp = minTemps.max()
else {
return
}
for index in 0..<dayTemps.count { var maxPoints = [CGPoint]()
var minPoints = [CGPoint]()
for index in 0..<daysCount {
guard let stackButton = stackView.arrangedSubviews[index] as? PeriodForecastButton else { continue } guard let stackButton = stackView.arrangedSubviews[index] as? PeriodForecastButton else { continue }
let buttonRightSide = stackButton.frame.origin.x + stackButton.bounds.width let buttonRightSide = stackButton.frame.origin.x + stackButton.bounds.width
...@@ -127,9 +209,41 @@ class CityForecastTimePeriodCell: UITableViewCell { ...@@ -127,9 +209,41 @@ class CityForecastTimePeriodCell: UITableViewCell {
//Min //Min
var minPointLevel = minTempFrame.origin.y + ((topMinTemp - minTemps[index]) * levelHeight) + levelHeight var minPointLevel = minTempFrame.origin.y + ((topMinTemp - minTemps[index]) * levelHeight) + levelHeight
minPointLevel = min(minPointLevel, minTempFrame.height + minTempFrame.origin.y - 5) minPointLevel = min(minPointLevel, minTempFrame.height + minTempFrame.origin.y - 5)
maxTempGraphPoints.append(.init(x: buttonCenterX, y: maxPointLevel))
minTempGraphPoints.append(.init(x: buttonCenterX, y: minPointLevel)) maxPoints.append(.init(x: buttonCenterX, y: maxPointLevel))
minPoints.append(.init(x: buttonCenterX, y: minPointLevel))
} }
dailyGraphPoints = DailyGraphPoints(maxTempPoints: maxPoints, minTempPoints: minPoints)
}
private func updateHourlyGraphPoints() {
guard
let hoursCount = location?.hourly.count,
let temps = (location?.hourly.map{ CGFloat($0.temp?.localeValue ?? 0) }),
let maxTemp = temps.max()
else {
return
}
var points = [CGPoint]()
for index in 0..<hoursCount {
guard let stackButton = stackView.arrangedSubviews[index] as? PeriodForecastButton else { continue }
let buttonRightSide = stackButton.frame.origin.x + stackButton.bounds.width
let buttonCenterX = (buttonRightSide + stackButton.frame.origin.x) / 2
let levelsCount = CGFloat(Set(temps).count)
let levelHeight = (graphView.frame.height / CGFloat(levelsCount)).rounded(.down)
let tempFrame = CGRect(x: 0, y: 0, width: graphView.frame.width, height: levelsCount * levelHeight)
var pointLevel = tempFrame.origin.y + ((maxTemp - temps[index]) * levelHeight) + levelHeight
pointLevel = min(pointLevel, tempFrame.height + 5)
points.append(.init(x: buttonCenterX, y: pointLevel))
}
hourlyGraphPoints = HourlyGraphPoints(points: points)
} }
@objc private func handleConditionButton(button: PeriodForecastButton) { @objc private func handleConditionButton(button: PeriodForecastButton) {
...@@ -143,6 +257,11 @@ class CityForecastTimePeriodCell: UITableViewCell { ...@@ -143,6 +257,11 @@ class CityForecastTimePeriodCell: UITableViewCell {
} }
} }
} }
@objc private func handleSegmentDidChange() {
self.currentTimePeriod = TimePeriod(rawValue: self.periodSegmentedControl.selectedSegmentIndex)!
self.rebuildStackButtons()
}
} }
private extension CityForecastTimePeriodCell { private extension CityForecastTimePeriodCell {
...@@ -153,6 +272,7 @@ private extension CityForecastTimePeriodCell { ...@@ -153,6 +272,7 @@ private extension CityForecastTimePeriodCell {
func prepareSegmentedControl() { func prepareSegmentedControl() {
periodSegmentedControl.selectedSegmentIndex = 0 periodSegmentedControl.selectedSegmentIndex = 0
periodSegmentedControl.addTarget(self, action: #selector(handleSegmentDidChange), for: .valueChanged)
contentView.addSubview(periodSegmentedControl) contentView.addSubview(periodSegmentedControl)
periodSegmentedControl.snp.makeConstraints { (make) in periodSegmentedControl.snp.makeConstraints { (make) in
...@@ -171,7 +291,7 @@ private extension CityForecastTimePeriodCell { ...@@ -171,7 +291,7 @@ private extension CityForecastTimePeriodCell {
scrollView.snp.makeConstraints { (make) in scrollView.snp.makeConstraints { (make) in
make.left.equalToSuperview() make.left.equalToSuperview()
make.right.equalToSuperview() make.right.equalToSuperview()
make.top.equalTo(periodSegmentedControl.snp.bottom).offset(30) make.top.equalTo(periodSegmentedControl.snp.bottom).offset(30).priority(.medium)
make.height.equalTo(240) make.height.equalTo(240)
} }
} }
...@@ -189,15 +309,6 @@ private extension CityForecastTimePeriodCell { ...@@ -189,15 +309,6 @@ private extension CityForecastTimePeriodCell {
stackView.snp.makeConstraints { (make) in stackView.snp.makeConstraints { (make) in
make.edges.height.equalToSuperview() make.edges.height.equalToSuperview()
} }
for index in 0..<dayTemps.count {
let conditionButton = PeriodForecastButton()
conditionButton.configure(dayTemp: dayTemps[index])
conditionButton.index = index
conditionButton.addTarget(self, action: #selector(handleConditionButton(button:)), for: .touchUpInside)
conditionButton.isSelected = index == 1
stackView.addArrangedSubview(conditionButton)
}
} }
func prepareGraphView() { func prepareGraphView() {
......
...@@ -70,9 +70,14 @@ class PeriodForecastButton: UIControl { ...@@ -70,9 +70,14 @@ class PeriodForecastButton: UIControl {
} }
//Public //Public
public func configure(dayTemp:DayTemp) { public func configure(dailyWeather: DailyWeather) {
self.tempLabel.text = "\(Int(dayTemp.max))°" self.tempLabel.text = dailyWeather.maxTemp?.shortString
self.minTempLabel.text = "\(Int(dayTemp.min))°" self.minTempLabel.text = dailyWeather.minTemp?.shortString
}
public func configure(hourlyWeather: HourlyWeather) {
self.tempLabel.text = hourlyWeather.temp?.shortString
self.minTempLabel.text = nil
} }
} }
......
...@@ -8,8 +8,6 @@ ...@@ -8,8 +8,6 @@
import UIKit import UIKit
class CityMoonCell: UITableViewCell { class CityMoonCell: UITableViewCell {
static let kIdentifier = "cityMoonCell"
//Private //Private
private let kCircleInset:CGFloat = 55 private let kCircleInset:CGFloat = 55
private let headingLabel = UILabel() private let headingLabel = UILabel()
......
...@@ -8,8 +8,6 @@ ...@@ -8,8 +8,6 @@
import UIKit import UIKit
class CityPrecipCell: UITableViewCell { class CityPrecipCell: UITableViewCell {
static let kIdentifier = "cityPrecipCell"
//Private //Private
private let headingLabel = UILabel() private let headingLabel = UILabel()
private let headingButton = ArrowButton() private let headingButton = ArrowButton()
......
...@@ -27,7 +27,6 @@ private struct CircleSegment { ...@@ -27,7 +27,6 @@ private struct CircleSegment {
} }
class CitySunCell: UITableViewCell { class CitySunCell: UITableViewCell {
static let kIdentifier = "citySunCell"
//Private //Private
//Heading //Heading
private let headingLabel = UILabel() private let headingLabel = UILabel()
......
...@@ -8,8 +8,6 @@ ...@@ -8,8 +8,6 @@
import UIKit import UIKit
class TodayAdCell: UITableViewCell { class TodayAdCell: UITableViewCell {
static let kIdentifier = "todayAdCell"
//Private //Private
private let container = UIView() private let container = UIView()
private let gradientView = GradientView(startColor: UIColor.white.withAlphaComponent(0), private let gradientView = GradientView(startColor: UIColor.white.withAlphaComponent(0),
......
//
// TodayCellFactory.swift
// 1Weather
//
// Created by Dmitry Stepanets on 03.03.2021.
//
import UIKit
private enum TodayCellType:Int, CaseIterable {
case forecast = 0
case ad
case conditions
case forecastPeriod
case precipitation
case dayTime
case sun
case moon
}
class TodayCellFactory {
public var onGetLocation:(() -> Location?)?
public var numberOfRows:Int {
return TodayCellType.allCases.count
}
public func registerCells(on tableView:UITableView) {
registerCell(type: CityForecastCell.self, tableView: tableView)
registerCell(type: TodayAdCell.self, tableView: tableView)
registerCell(type: CityConditionsCell.self, tableView: tableView)
registerCell(type: CityForecastTimePeriodCell.self, tableView: tableView)
registerCell(type: CityPrecipCell.self, tableView: tableView)
registerCell(type: CityDayTimesCell.self, tableView: tableView)
registerCell(type: CityAirQualityCell.self, tableView: tableView)
registerCell(type: CitySunCell.self, tableView: tableView)
registerCell(type: CityMoonCell.self, tableView: tableView)
}
public func cellFromTableView(tableView:UITableView, indexPath:IndexPath) -> UITableViewCell {
guard let cellType = TodayCellType(rawValue: indexPath.row) else {
return UITableViewCell()
}
switch cellType {
case .forecast:
let cell = dequeueReusableCell(type: CityForecastCell.self, tableView: tableView, indexPath: indexPath)
if let loc = self.onGetLocation?() {
cell.configure(with: loc)
}
return cell
case .ad:
let cell = dequeueReusableCell(type: TodayAdCell.self, tableView: tableView, indexPath: indexPath)
return cell
case .conditions:
let cell = dequeueReusableCell(type: CityConditionsCell.self, tableView: tableView, indexPath: indexPath)
if let loc = self.onGetLocation?() {
cell.configure(with: loc)
}
return cell
case .forecastPeriod:
let cell = dequeueReusableCell(type: CityForecastTimePeriodCell.self, tableView: tableView, indexPath: indexPath)
if let loc = self.onGetLocation?() {
cell.configure(with: loc)
}
return cell
case .precipitation:
let cell = dequeueReusableCell(type: CityPrecipCell.self, tableView: tableView, indexPath: indexPath)
return cell
case .dayTime:
let cell = dequeueReusableCell(type: CityDayTimesCell.self, tableView: tableView, indexPath: indexPath)
return cell
case .sun:
let cell = dequeueReusableCell(type: CitySunCell.self, tableView: tableView, indexPath: indexPath)
return cell
case .moon:
let cell = dequeueReusableCell(type: CityMoonCell.self, tableView: tableView, indexPath: indexPath)
return cell
}
}
public func willDisplay(cell:UITableViewCell) {
switch cell {
case let timePeriodCell as CityForecastTimePeriodCell:
timePeriodCell.drawGraphIfNeeded()
case let sunCell as CitySunCell:
sunCell.updateSunPosition()
case let moonCell as CityMoonCell:
moonCell.updateMoonPosition()
default:
break
}
}
//Private
private func registerCell<T: ReusableCellProtocol>(type:T.Type, tableView:UITableView) {
tableView.register(type, forCellReuseIdentifier: T.kIdentifier)
}
private func dequeueReusableCell<T: ReusableCellProtocol>(type:T.Type, tableView:UITableView, indexPath: IndexPath) -> T {
let cell = tableView.dequeueReusableCell(withIdentifier: T.kIdentifier, for: indexPath) as! T
return cell
}
}
...@@ -8,21 +8,10 @@ ...@@ -8,21 +8,10 @@
import UIKit import UIKit
import CoreLocation import CoreLocation
private enum TodayTableCell {
case forecast
case ad
case conditions
case forecastPeriod
case precipitation
case dayTime
case sun
case moon
}
class TodayViewController: UIViewController { class TodayViewController: UIViewController {
//Private //Private
private let viewModel:TodayViewModel
private let tableView = UITableView() private let tableView = UITableView()
private let tableCells:[TodayTableCell] = [.forecast, .ad, .conditions, .forecastPeriod, .precipitation, .dayTime, .sun, .moon]
private var localizationObserver:Any? private var localizationObserver:Any?
deinit { deinit {
...@@ -30,6 +19,16 @@ class TodayViewController: UIViewController { ...@@ -30,6 +19,16 @@ class TodayViewController: UIViewController {
NotificationCenter.default.removeObserver(observer) NotificationCenter.default.removeObserver(observer)
} }
} }
init(viewModel: TodayViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
...@@ -87,15 +86,7 @@ private extension TodayViewController { ...@@ -87,15 +86,7 @@ private extension TodayViewController {
} }
func prepareTableView() { func prepareTableView() {
tableView.register(CityForecastCell.self, forCellReuseIdentifier: CityForecastCell.kIdentifier) viewModel.todayCellFactory.registerCells(on: tableView)
tableView.register(TodayAdCell.self, forCellReuseIdentifier: TodayAdCell.kIdentifier)
tableView.register(CityConditionsCell.self, forCellReuseIdentifier: CityConditionsCell.kIdentifier)
tableView.register(CityForecastTimePeriodCell.self, forCellReuseIdentifier: CityForecastTimePeriodCell.kIdentifier)
tableView.register(CityPrecipCell.self, forCellReuseIdentifier: CityPrecipCell.kIdentifier)
tableView.register(CityDayTimesCell.self, forCellReuseIdentifier: CityDayTimesCell.kIdentifier)
tableView.register(CityAirQualityCell.self, forCellReuseIdentifier: CityAirQualityCell.kIdentifier)
tableView.register(CitySunCell.self, forCellReuseIdentifier: CitySunCell.kIdentifier)
tableView.register(CityMoonCell.self, forCellReuseIdentifier: CityMoonCell.kIdentifier)
tableView.contentInset = .init(top: 0, left: 0, bottom: 15, right: 0) tableView.contentInset = .init(top: 0, left: 0, bottom: 15, right: 0)
tableView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor tableView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
tableView.separatorStyle = .none tableView.separatorStyle = .none
...@@ -115,52 +106,17 @@ private extension TodayViewController { ...@@ -115,52 +106,17 @@ private extension TodayViewController {
//MARK:- UITableView Data Source //MARK:- UITableView Data Source
extension TodayViewController: UITableViewDataSource { extension TodayViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tableCells.count return viewModel.todayCellFactory.numberOfRows
} }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch self.tableCells[indexPath.row] { return viewModel.todayCellFactory.cellFromTableView(tableView: tableView, indexPath: indexPath)
case .forecast:
let cell = tableView.dequeueReusableCell(withIdentifier: CityForecastCell.kIdentifier, for: indexPath) as! CityForecastCell
return cell
case .ad:
let cell = tableView.dequeueReusableCell(withIdentifier: TodayAdCell.kIdentifier, for: indexPath)
return cell
case .conditions:
let cell = tableView.dequeueReusableCell(withIdentifier: CityConditionsCell.kIdentifier, for: indexPath)
return cell
case .forecastPeriod:
let cell = tableView.dequeueReusableCell(withIdentifier: CityForecastTimePeriodCell.kIdentifier, for: indexPath) as! CityForecastTimePeriodCell
return cell
case .precipitation:
let cell = tableView.dequeueReusableCell(withIdentifier: CityPrecipCell.kIdentifier, for: indexPath) as! CityPrecipCell
return cell
case .dayTime:
let cell = tableView.dequeueReusableCell(withIdentifier: CityDayTimesCell.kIdentifier, for: indexPath) as! CityDayTimesCell
return cell
case .sun:
let cell = tableView.dequeueReusableCell(withIdentifier: CitySunCell.kIdentifier, for: indexPath) as! CitySunCell
return cell
case .moon:
let cell = tableView.dequeueReusableCell(withIdentifier: CityMoonCell.kIdentifier, for: indexPath) as! CityMoonCell
return cell
}
} }
} }
//MARK:- UITableView Delegate //MARK:- UITableView Delegate
extension TodayViewController: UITableViewDelegate { extension TodayViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let timePeriodCell = cell as? CityForecastTimePeriodCell { viewModel.todayCellFactory.willDisplay(cell: cell)
timePeriodCell.drawGraphIfNeeded()
}
if let sunCell = cell as? CitySunCell {
sunCell.updateSunPosition()
}
if let moonCell = cell as? CityMoonCell {
moonCell.updateMoonPosition()
}
} }
} }
//
// TodayViewModel.swift
// 1Weather
//
// Created by Dmitry Stepanets on 01.03.2021.
//
import UIKit
class TodayViewModel {
//Public
public let todayCellFactory = TodayCellFactory()
public weak var delegate:ViewModelDelegate?
public private(set) var location:Location
init() {
func weekDay(byIndex:Int) -> WeekDay {
switch byIndex {
case 0:
return .monday
case 1:
return .tuesday
case 2:
return .wednesday
case 3:
return .thursday
case 4:
return .friday
case 5:
return .saturday
default:
return .sunday
}
}
let today = CurrentWeather(date: Date(),
timeZone: .current,
weekDay: .monday,
type: .heavyRain,
isDay: true,
minTemp: .init(value: -4, unit: .celsius),
maxTemp: .init(value: 9, unit: .celsius),
windSpeed: .init(value: 5, unit: .milesPerHour),
windDirection: .north,
precipitationProbability: .some(30),
temp: .init(value: 4, unit: .celsius),
apparentTemp: .init(value: 5, unit: .celsius),
humidity: .some(30),
visibility: .init(value: 8, unit: .miles),
pressure: .init(value: 29.7, unit: .inchesOfMercury),
sunrise: .init(hours: 6, minutes: 26),
sunset: .init(hours: 17, minutes: 46),
sunState: .alwaysUp,
moonrise: .init(hours: 20, minutes: 00),
moonset: .init(hours: 5, minutes: 30),
moonState: .normal)
var daily = [DailyWeather]()
for index in 0..<7 {
let day = DailyWeather(date: Date(),
timeZone: .current,
weekDay: weekDay(byIndex: index),
type: WeatherType.allCases.randomElement() ?? .unknown,
minTemp: .init(value: Double.random(in: -3..<5), unit: .celsius),
maxTemp: .init(value: Double.random(in: 5..<10), unit: .celsius),
windSpeed: .init(value: Double.random(in: 0..<10), unit: .milesPerHour),
windDirection: WindDirection.allCases.randomElement() ?? .east,
precipitationProbability: .some(UInt.random(in: 0..<100)),
sunrise: .init(hours: 5, minutes: 23),
sunset: .init(hours: 17, minutes: 40),
sunState: .normal,
moonrise: .init(hours: 22, minutes: 15),
moonset: .init(hours: 2, minutes: 30),
moonState: .normal)
daily.append(day)
}
var hourly = [HourlyWeather]()
var startDate = Date()
for _ in 0..<24 {
let hour = HourlyWeather(date: startDate,
timeZone: TimeZone(abbreviation: "EST") ?? .current,
weekDay: .monday,
type: WeatherType.allCases.randomElement() ?? .unknown,
isDay: true,
temp: .init(value: Double.random(in: -3..<10), unit: .celsius),
windSpeed: .init(value: Double.random(in: 0..<10), unit: .milesPerHour),
windDirection: WindDirection.allCases.randomElement() ?? .east,
precipitationProbability: .some(UInt.random(in: 0..<100)),
humidity: .some(UInt.random(in: 0..<100)))
startDate = startDate + TimeInterval(3600)
hourly.append(hour)
}
self.location = Location(coordinates: nil,
imageName: "ny_bridge",
countryCode: "us",
countryName: "United States",
region: "New York",
cityName: "New York",
nickname: "ny",
fipsCode: "36",
today: today,
daily: daily,
hourly: hourly)
//Setup factory callback
todayCellFactory.onGetLocation = {[weak self] in
return self?.location
}
}
}
//
// ViewModelProtocol.swift
// 1Weather
//
// Created by Dmitry Stepanets on 01.03.2021.
//
import UIKit
protocol ViewModelDelegate : class {
func viewModelDidChange<P: ViewModelProtocol>(model:P)
func viewModel<P: ViewModelProtocol>(model:P, errorHasOccured error:String?)
}
//Makes delegate methods optional
extension ViewModelDelegate {
func viewModelDidChange<P: ViewModelProtocol>(model:P) {}
func viewModel<P: ViewModelProtocol>(model:P, errorHasOccured error:String?) {}
}
protocol ViewModelProtocol {
}
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1100" LastUpgradeVersion = "1240"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
......
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1100" LastUpgradeVersion = "1240"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForAnalyzing = "YES"
buildForTesting = "YES" buildForTesting = "YES"
buildForRunning = "YES" buildForRunning = "YES"
buildForProfiling = "YES" buildForProfiling = "YES"
buildForArchiving = "YES"> buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "19622742EBA51E823D6DAE3F8CDBFAD4" BlueprintIdentifier = "19622742EBA51E823D6DAE3F8CDBFAD4"
...@@ -23,14 +23,15 @@ ...@@ -23,14 +23,15 @@
</BuildActionEntries> </BuildActionEntries>
</BuildAction> </BuildAction>
<TestAction <TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES">
buildConfiguration = "Debug"> <Testables>
<AdditionalOptions> </Testables>
</AdditionalOptions>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0" launchStyle = "0"
...@@ -38,17 +39,14 @@ ...@@ -38,17 +39,14 @@
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugServiceExtension = "internal" debugServiceExtension = "internal"
buildConfiguration = "Debug"
allowLocationSimulation = "YES"> allowLocationSimulation = "YES">
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = "" savedToolIdentifier = ""
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES">
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES">
</ProfileAction> </ProfileAction>
<AnalyzeAction <AnalyzeAction
buildConfiguration = "Debug"> buildConfiguration = "Debug">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment