Commit 5faf43ad by Dmitriy Stepanets

Working on forecast controller

parent f977bec8
......@@ -40,6 +40,9 @@
CD86246925E672A20097F3FB /* PrecipButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD86246825E672A20097F3FB /* PrecipButton.swift */; };
CD86246C25E6826A0097F3FB /* InnerShadowLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD86246B25E6826A0097F3FB /* InnerShadowLayer.swift */; };
CD86C22225F0DCCB00F38A16 /* PrecipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD86C22125F0DCCB00F38A16 /* PrecipView.swift */; };
CD8E040D25F8F3D2001785B6 /* ForecastTimePeriodCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD8E040C25F8F3D2001785B6 /* ForecastTimePeriodCell.swift */; };
CD8E041225F8F775001785B6 /* ForecastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD8E041125F8F775001785B6 /* ForecastViewModel.swift */; };
CD8E041625F8F91B001785B6 /* ForecastCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD8E041525F8F91B001785B6 /* ForecastCellFactory.swift */; };
CD9B6B1125DBC723001D9B80 /* CubicCurveAlgorithm.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9B6B1025DBC723001D9B80 /* CubicCurveAlgorithm.swift */; };
CD9B6B1425DBCDE2001D9B80 /* GraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9B6B1325DBCDE2001D9B80 /* GraphView.swift */; };
CDA5542825EF734200A2E08C /* TodayCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA5542725EF734200A2E08C /* TodayCellFactory.swift */; };
......@@ -123,6 +126,9 @@
CD86246825E672A20097F3FB /* PrecipButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrecipButton.swift; sourceTree = "<group>"; };
CD86246B25E6826A0097F3FB /* InnerShadowLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InnerShadowLayer.swift; sourceTree = "<group>"; };
CD86C22125F0DCCB00F38A16 /* PrecipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrecipView.swift; sourceTree = "<group>"; };
CD8E040C25F8F3D2001785B6 /* ForecastTimePeriodCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastTimePeriodCell.swift; sourceTree = "<group>"; };
CD8E041125F8F775001785B6 /* ForecastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastViewModel.swift; sourceTree = "<group>"; };
CD8E041525F8F91B001785B6 /* ForecastCellFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastCellFactory.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>"; };
CDA5542725EF734200A2E08C /* TodayCellFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayCellFactory.swift; sourceTree = "<group>"; };
......@@ -272,6 +278,7 @@
isa = PBXGroup;
children = (
CD647D0125ED07D60034578B /* TodayViewModel.swift */,
CD8E041125F8F775001785B6 /* ForecastViewModel.swift */,
CD647D0525ED08050034578B /* ViewModelProtocol.swift */,
);
path = ViewModels;
......@@ -352,6 +359,15 @@
path = CityPrecipCell;
sourceTree = "<group>";
};
CD8E040B25F8F39B001785B6 /* Cells */ = {
isa = PBXGroup;
children = (
CD8E041525F8F91B001785B6 /* ForecastCellFactory.swift */,
CD8E040C25F8F3D2001785B6 /* ForecastTimePeriodCell.swift */,
);
path = Cells;
sourceTree = "<group>";
};
CDC6124D25E7960D00188DA7 /* CityDayTimesCell */ = {
isa = PBXGroup;
children = (
......@@ -426,6 +442,7 @@
CDE18DCF25D166DD00C80ED9 /* Forecast */ = {
isa = PBXGroup;
children = (
CD8E040B25F8F39B001785B6 /* Cells */,
CDE18DD025D166F900C80ED9 /* ForecastViewController.swift */,
);
path = Forecast;
......@@ -676,6 +693,7 @@
CDE18DD125D166F900C80ED9 /* ForecastViewController.swift in Sources */,
CD39F2F525DE9571009FE398 /* ArrowButton.swift in Sources */,
CEDE4E8325EEFD56007457E9 /* WdtLocationResponse.swift in Sources */,
CD8E041625F8F91B001785B6 /* ForecastCellFactory.swift in Sources */,
CDC6125325E79C8F00188DA7 /* DayTimeView.swift in Sources */,
CD86246925E672A20097F3FB /* PrecipButton.swift in Sources */,
CEDE4E8225EEFD56007457E9 /* WdtWeatherCode.swift in Sources */,
......@@ -703,9 +721,11 @@
CD86245E25E646350097F3FB /* SunUvView.swift in Sources */,
CEAFF08325DFC67F00DF4EBF /* Location.swift in Sources */,
CD82300725D6A73F00A05501 /* CityConditionButton.swift in Sources */,
CD8E040D25F8F3D2001785B6 /* ForecastTimePeriodCell.swift in Sources */,
CDC6126A25E90C8800188DA7 /* GraphLineSettings.swift in Sources */,
CDA5542D25EF7C9700A2E08C /* ReusableCellProtocol.swift in Sources */,
CD647D0625ED08050034578B /* ViewModelProtocol.swift in Sources */,
CD8E041225F8F775001785B6 /* ForecastViewModel.swift in Sources */,
CD86246C25E6826A0097F3FB /* InnerShadowLayer.swift in Sources */,
CEAFF0A325E0FF0800DF4EBF /* LocationManager.swift in Sources */,
CEAD00A12577B2D5003596AD /* StuffThatIsPresentInTheMainProject.swift in Sources */,
......
......@@ -7,7 +7,7 @@
<key>1Weather.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>6</integer>
<integer>5</integer>
</dict>
<key>PG (Playground) 1.xcscheme</key>
<dict>
......
......@@ -9,6 +9,8 @@ import UIKit
class ForecastCoordinator: Coordinator {
//Private
private let forecastViewModel = ForecastViewModel(locationManager: LocationManager.shared)
private let navigationController = UINavigationController(nibName: nil, bundle: nil)
private var tabBarController:UITabBarController?
//Public
......@@ -20,7 +22,8 @@ class ForecastCoordinator: Coordinator {
}
func start() {
let forecastViewController = ForecastViewController()
self.tabBarController?.add(viewController: forecastViewController)
let forecastViewController = ForecastViewController(viewModel: forecastViewModel)
navigationController.viewControllers = [forecastViewController]
tabBarController?.add(viewController: navigationController)
}
}
......@@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "254",
"green" : "248",
"red" : "247"
"blue" : "0xFE",
"green" : "0xF8",
"red" : "0xF7"
}
},
"idiom" : "universal"
......
......@@ -7,6 +7,11 @@
import UIKit
enum TimePeriod: Int {
case daily = 0
case hourly = 1
}
class ForecastTimePeriodControl: UISegmentedControl {
private let kTopInset: CGFloat = 2
private let kSideInset:CGFloat = 11
......
//
// ForecastCellFactory.swift
// 1Weather
//
// Created by Dmitry Stepanets on 10.03.2021.
//
import UIKit
private enum ForecastCellType:Int, CaseIterable {
case forecastPeriod = 0
// case forecast
// case conditions
// case precipitation
// case dayTime
// case sun
// case moon
}
class ForecastCellFactory {
public var onGetLocation:(() -> Location?)?
public var numberOfRows:Int {
return ForecastCellType.allCases.count
}
public func registerCells(on tableView:UITableView) {
registerCell(type: ForecastTimePeriodCell.self, tableView: tableView)
}
public func cellFromTableView(tableView:UITableView, indexPath:IndexPath) -> UITableViewCell {
guard let cellType = ForecastCellType(rawValue: indexPath.row) else {
return UITableViewCell()
}
guard let loc = self.onGetLocation?() else {
return UITableViewCell()
}
switch cellType {
case .forecastPeriod:
let cell = dequeueReusableCell(type: ForecastTimePeriodCell.self, tableView: tableView, indexPath: indexPath)
return cell
}
}
//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
}
}
//
// ForecastTimePeriodCell.swift
// 1Weather
//
// Created by Dmitry Stepanets on 10.03.2021.
//
import UIKit
private struct DailyGraphPoints {
let maxTempPoints: [CGPoint]
let minTempPoints: [CGPoint]
}
private struct HourlyGraphPoints {
let points: [CGPoint]
}
class ForecastTimePeriodCell: UITableViewCell {
//Private
private let periodSegmentedControl = ForecastTimePeriodControl(items: ["forecast.timePeriod.daily".localized(),
"forecast.timePeriod.hourly".localized()])
private let kMinGraphHeight:CGFloat = 20
private let gradientView = GradientView(startColor: UIColor(hex: 0xffffff).withAlphaComponent(0),
endColor: UIColor(hex: 0xdaddec),
opacity: 0.5)
private let scrollView = UIScrollView()
private let stackView = UIStackView()
private var location:Location?
private var currentTimePeriod = TimePeriod.daily
private var dailyGraphPoints: DailyGraphPoints?
private var hourlyGraphPoints: HourlyGraphPoints?
private let graphView = GraphView()
private var graphIsDrawn = false
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
prepareCell()
prepareSegmentedControl()
prepareGradient()
prepareScrollView()
prepareStackView()
prepareGraphView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//Public
public func configure(with location:Location) {
self.location = location
rebuildStackButtons()
}
public func drawGraphIfNeeded() {
guard graphIsDrawn == false else { return }
//Checking for correct height
guard
let periodButton = stackView.arrangedSubviews.first as? PeriodForecastButton,
periodButton.graphRect.height >= kMinGraphHeight
else {
return
}
graphView.frame = .init(x: 0,
y: periodButton.graphRect.origin.y,
width: stackView.frame.width,
height: periodButton.graphRect.height)
updateGraphPoints()
drawGraph()
graphIsDrawn = true
}
//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()
}
@objc private func handleSegmentDidChange() {
}
}
private extension ForecastTimePeriodCell {
func prepareCell() {
selectionStyle = .none
contentView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
}
func prepareSegmentedControl() {
periodSegmentedControl.selectedSegmentIndex = 0
periodSegmentedControl.addTarget(self, action: #selector(handleSegmentDidChange), for: .valueChanged)
contentView.addSubview(periodSegmentedControl)
periodSegmentedControl.snp.makeConstraints { (make) in
make.top.equalToSuperview().inset(15)
make.left.right.equalToSuperview().inset(16)
make.height.equalTo(40)
}
}
func prepareGradient() {
contentView.addSubview(gradientView)
gradientView.snp.makeConstraints { (make) in
make.left.right.bottom.equalToSuperview()
make.height.equalTo(172)
}
}
func prepareScrollView() {
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
scrollView.clipsToBounds = false
contentView.addSubview(scrollView)
scrollView.snp.makeConstraints { (make) in
make.left.equalToSuperview()
make.right.equalToSuperview()
make.top.equalTo(periodSegmentedControl.snp.bottom).offset(20).priority(.medium)
make.bottom.equalToSuperview().inset(15)
make.height.equalTo(267)
}
}
func prepareStackView() {
stackView.axis = .horizontal
stackView.distribution = .equalSpacing
stackView.alignment = .center
stackView.spacing = 10
stackView.clipsToBounds = false
stackView.isLayoutMarginsRelativeArrangement = true
stackView.layoutMargins = .init(top: 0, left: 6, bottom: 0, right: 6)
scrollView.addSubview(stackView)
stackView.snp.makeConstraints { (make) in
make.edges.height.equalToSuperview()
}
}
func prepareGraphView() {
//Graph view
graphView.frame = .zero
graphView.backgroundColor = .clear
scrollView.addSubview(graphView)
}
}
......@@ -8,10 +8,116 @@
import UIKit
class ForecastViewController: UIViewController {
//Private
private let cityButton = NavigationCityButton()
private let tableView = UITableView()
private let viewModel:ForecastViewModel
private var localizationObserver:Any?
init(viewModel: ForecastViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .blue
viewModel.delegate = self
prepareViewController()
prepareNavigationBar()
prepareTableView()
refreshCityButton()
}
private func refreshCityButton() {
cityButton.configure(with: viewModel.location)
cityButton.isHidden = false
}
@objc private func handleCityButton() {
print("Handle city button")
}
@objc private func handleNotificationButton() {
print("Handle notification button")
}
}
//MARk:- Prepare
private extension ForecastViewController {
func prepareViewController() {
view.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
localizationObserver = NotificationCenter.default.addObserver(forName: .localizationChange, object: nil, queue: .main, using: { _ in
self.tableView.reloadData()
})
}
func prepareNavigationBar() {
//City button
cityButton.isHidden = true
cityButton.addTarget(self, action: #selector(handleCityButton), for: .touchUpInside)
let cityBarItem = UIBarButtonItem(customView: cityButton)
let barSpacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
barSpacer.width = 18
//Notification button
let notificationButton = UIButton()
notificationButton.frame = .init(origin: .zero, size: .init(width: 40, height: 40))
notificationButton.setImage(UIImage(named: "bell"), for: .normal)
notificationButton.tintColor = ThemeManager.currentTheme.primaryTextColor
notificationButton.addTarget(self, action: #selector(handleNotificationButton), for: .touchUpInside)
let notificationBarButton = UIBarButtonItem(customView: notificationButton)
notificationButton.contentEdgeInsets = .init(top: 0, left: 16, bottom: 0, right: 0)
self.navigationItem.leftBarButtonItems = [barSpacer, cityBarItem]
self.navigationItem.rightBarButtonItem = notificationBarButton
}
func prepareTableView() {
viewModel.forecastCellFactory.registerCells(on: tableView)
tableView.contentInset = .init(top: 0, left: 0, bottom: 15, right: 0)
tableView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
tableView.separatorStyle = .none
tableView.tableFooterView = UIView()
tableView.estimatedRowHeight = UITableView.automaticDimension
tableView.rowHeight = UITableView.automaticDimension
tableView.delegate = self
tableView.dataSource = self
view.addSubview(tableView)
tableView.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
}
}
//MARK:- UITableView Delegate
extension ForecastViewController: UITableViewDelegate {
}
//MARK:- UITableView DataSource
extension ForecastViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.forecastCellFactory.numberOfRows
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return viewModel.forecastCellFactory.cellFromTableView(tableView: tableView, indexPath: indexPath)
}
}
//MARK:- ViewModel Delegate
extension ForecastViewController: ViewModelDelegate {
func viewModelDidChange<P>(model: P) where P : ViewModelProtocol {
refreshCityButton()
tableView.reloadData()
}
}
......@@ -32,6 +32,7 @@ class DayTimeView: UIView {
dayTimeLabel.text = dayTimeWeather.dayTime.localized
forecastImageView.image = dayTimeWeather.type.image(isDay: dayTimeWeather.isDay)
tempLabel.text = dayTimeWeather.temp?.shortString ?? "--"
dayTimeConditionLabel.text = dayTimeWeather.type.localized(isDay: dayTimeWeather.isDay)
}
}
......
......@@ -16,11 +16,6 @@ private struct HourlyGraphPoints {
let points: [CGPoint]
}
private enum TimePeriod: Int {
case daily = 0
case hourly = 1
}
class CityForecastTimePeriodCell: UITableViewCell {
//Private
private let periodSegmentedControl = ForecastTimePeriodControl(items: ["forecast.timePeriod.daily".localized(),
......
//
// ForecastViewModel.swift
// 1Weather
//
// Created by Dmitry Stepanets on 10.03.2021.
//
import UIKit
class ForecastViewModel: ViewModelProtocol {
//Public
public let forecastCellFactory = ForecastCellFactory()
public weak var delegate:ViewModelDelegate?
public private(set) var location: Location?
//Private
private var locationManager: LocationManager
deinit {
self.locationManager.remove(delegate: self)
}
public init(locationManager: LocationManager) {
self.locationManager = locationManager
locationManager.add(delegate: self)
//Setup factory callback
forecastCellFactory.onGetLocation = {[weak self] in
return self?.location
}
}
public func updateWeather() {
locationManager.updateWeather()
}
}
//MARK:- LocationManager Delegate
extension ForecastViewModel: LocationManagerDelegate {
func locationManager(_ locationManager: LocationManager, changedCurrentLocation newLocation: Location?) {
DispatchQueue.main.async {
print("TVM-Forecast")
self.location = newLocation
self.delegate?.viewModelDidChange(model: self)
}
}
}
......@@ -16,6 +16,10 @@ class TodayViewModel: ViewModelProtocol {
//Private
private var locationManager: LocationManager
deinit {
self.locationManager.remove(delegate: self)
}
public init(locationManager: LocationManager) {
self.locationManager = locationManager
locationManager.add(delegate: self)
......@@ -29,12 +33,9 @@ class TodayViewModel: ViewModelProtocol {
public func updateWeather() {
locationManager.updateWeather()
}
deinit {
self.locationManager.remove(delegate: self)
}
}
//MARK:- LocationManager Delegate
extension TodayViewModel: LocationManagerDelegate {
func locationManager(_ locationManager: LocationManager, changedCurrentLocation newLocation: Location?) {
DispatchQueue.main.async {
......
......@@ -52,7 +52,7 @@
<key>XMLCoder.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>5</integer>
<integer>6</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
......
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