Commit a0b2dc91 by Dmitriy Stepanets

Added Forecast hourly cells

parent 92fda9d3
......@@ -7,7 +7,7 @@
<key>1Weather.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>5</integer>
<integer>6</integer>
</dict>
<key>PG (Playground) 1.xcscheme</key>
<dict>
......
//
// Date+Now.swift
// 1Weather
//
// Created by Dmitry Stepanets on 22.03.2021.
//
import UIKit
extension Date {
private static var formatter:DateFormatter = {
let fmt = DateFormatter()
fmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
return fmt
}()
static func nowDate(timeZone:TimeZone) -> Self? {
formatter.timeZone = timeZone
let nowDateString = formatter.string(from: Date())
return formatter.date(from: nowDateString)
}
}
//
// UITableView+HeaderSize.swift
// 1Weather
//
// Created by Dmitry Stepanets on 22.03.2021.
//
import UIKit
extension UITableView {
//Variable-height UITableView tableHeaderView with autolayout
func layoutTableHeaderView() {
guard let headerView = self.tableHeaderView else { return }
headerView.translatesAutoresizingMaskIntoConstraints = false
let headerWidth = headerView.bounds.size.width;
let temporaryWidthConstraints = NSLayoutConstraint.constraints(withVisualFormat: "[headerView(width)]",
options: NSLayoutConstraint.FormatOptions(rawValue: UInt(0)),
metrics: ["width": headerWidth],
views: ["headerView": headerView])
headerView.addConstraints(temporaryWidthConstraints)
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
let headerSize = headerView.systemLayoutSizeFitting(UITableView.layoutFittingCompressedSize)
let height = headerSize.height
var frame = headerView.frame
frame.size.height = height
headerView.frame = frame
self.tableHeaderView = headerView
headerView.removeConstraints(temporaryWidthConstraints)
headerView.translatesAutoresizingMaskIntoConstraints = true
}
}
......@@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x34",
"green" : "0x23",
"red" : "0x21"
"blue" : "52",
"green" : "35",
"red" : "33"
}
},
"idiom" : "universal"
......
......@@ -32,6 +32,7 @@
//Day
"day.today" = "Today";
"day.now" = "Now";
//Day time
"dayTime.morning" = "Morning";
......@@ -40,6 +41,7 @@
"dayTime.night" = "Night";
//Condition
"condition.temperature" = "Temperature";
"condition.precipitation" = "Precipitation";
"condition.humidity" = "Humidity";
"condition.uvIndex" = "UV-Index";
......@@ -52,7 +54,6 @@
"forecast.timePeriod.daily" = "Daily";
"forecast.timePeriod.hourly" = "Hourly";
"forecast.timePeriod.minutely" = "Minutely";
"forecast.timePeriod.now" = "now";
//Sun
"sun.title" = "sun";
......
......@@ -111,7 +111,7 @@ private extension ForecastDetailPeriodButton {
clipsToBounds = false
backgroundColor = UIColor.white
layer.cornerRadius = 12
layer.borderColor = UIColor(hex: 0xeceef6).cgColor
layer.borderColor = UIColor(hex: 0xdcdcdc).cgColor
layer.borderWidth = 1 / UIScreen.main.scale
}
......
......@@ -18,6 +18,7 @@ private struct HourlyGraphPoints {
protocol ForecastTimePeriodViewDelegate:class {
func forecastTimePeriodView(view:ForecastTimePeriodView, didSelectButtonAt index:Int)
func offsetDidChange(offset:CGFloat)
}
class ForecastTimePeriodView: UIView {
......@@ -34,6 +35,9 @@ class ForecastTimePeriodView: UIView {
//Public
weak var delegate:ForecastTimePeriodViewDelegate?
var isEmpty:Bool {
return dailyGraphPoints.maxTempPoints.isEmpty && hourlyGraphPoints.points.isEmpty
}
//MARK:- View life cycle
init() {
......@@ -72,10 +76,12 @@ class ForecastTimePeriodView: UIView {
buttons.enumerated().forEach {
$1.isSelected = $0 == index
if $1.isSelected {
self.scrollView.scrollRectToVisible($1.frame, animated: true)
}
}
}
public func update(offset:CGFloat) {
if self.scrollView.contentOffset.x != offset {
self.scrollView.setContentOffset(.init(x: offset, y: 0), animated: false)
}
}
......@@ -251,12 +257,14 @@ class ForecastTimePeriodView: UIView {
}
}
//MARK:- Prepare
private extension ForecastTimePeriodView {
func preapreView() {
backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
backgroundColor = .clear
}
func prepareScrollView() {
scrollView.delegate = self
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
scrollView.clipsToBounds = false
......@@ -289,3 +297,10 @@ private extension ForecastTimePeriodView {
scrollView.addSubview(graphView)
}
}
//MARK:- UIScrollView Delegate
extension ForecastTimePeriodView: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.delegate?.offsetDidChange(offset: scrollView.contentOffset.x)
}
}
......@@ -15,7 +15,7 @@ struct LineDot {
struct GraphLine {
//Private
private let kIntersectAccuracy:CGFloat = 2
private let kIntersectAccuracy:CGFloat = 0
private var points = [CGPoint]()
private let settings:GraphLineSettings
private let onGetGraphRect: GraphRectClosure
......
//
// TimePeriodOffsetHolder.swift
// 1Weather
//
// Created by Dmitry Stepanets on 22.03.2021.
//
import UIKit
protocol TimePeriodOffsetDelegate:class {
func offsetDidChange(newOffset:CGFloat)
}
class TimePeriodOffsetHolder {
//Public
private(set) var currentOffset:CGFloat = 0
weak var delegate: TimePeriodOffsetDelegate?
public func update(offset:CGFloat) {
self.currentOffset = offset
self.delegate?.offsetDidChange(newOffset: offset)
}
}
......@@ -7,8 +7,8 @@
import UIKit
private enum ForecastCellType:Int, CaseIterable {
case forecastPeriod = 0
private enum DailyForecastCellType:Int, CaseIterable {
case forecast = 0
case forecastInfo
// case forecast
// case conditions
......@@ -18,40 +18,76 @@ private enum ForecastCellType:Int, CaseIterable {
case moon
}
private enum HourlyForecastCellType: Int, CaseIterable {
case day
case tempInfo
case precipitation
// case wind
}
class ForecastCellFactory {
//Private
private let forecastViewModel:ForecastViewModel
private var currentTimePeriod = TimePeriod.daily
//Public
public var numberOfRows:Int {
return ForecastCellType.allCases.count
return currentTimePeriod == .daily ? DailyForecastCellType.allCases.count : HourlyForecastCellType.allCases.count
}
public init(viewModel: ForecastViewModel) {
self.forecastViewModel = viewModel
}
public func setTimePeriod(timePeriod:TimePeriod) {
self.currentTimePeriod = timePeriod
}
public func registerCells(on tableView:UITableView) {
registerCell(type: ForecastTimePeriodCell.self, tableView: tableView)
registerCell(type: ForecastDailyCell.self, tableView: tableView)
registerCell(type: ForecastDayCell.self, tableView: tableView)
registerCell(type: ForecastHourlyCell.self, tableView: tableView)
registerCell(type: ForecastInfoCell.self, tableView: tableView)
registerCell(type: PrecipitationCell.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 = ForecastCellType(rawValue: indexPath.row) else {
switch currentTimePeriod {
case .daily:
return dailyCellFor(tableView: tableView, indexPath: indexPath)
case .hourly:
return hourlyCellFor(tableView: tableView, indexPath: indexPath)
}
}
public func willDisplay(cell:UITableViewCell) {
switch cell {
case let sunCell as CitySunCell:
sunCell.updateSunPosition()
case let moonCell as CityMoonCell:
moonCell.updateMoonPosition()
default:
break
}
}
//Private
private func dailyCellFor(tableView:UITableView, indexPath:IndexPath) -> UITableViewCell {
guard let cellType = DailyForecastCellType(rawValue: indexPath.row) else {
return UITableViewCell()
}
switch cellType {
case .forecastPeriod:
let cell = dequeueReusableCell(type: ForecastTimePeriodCell.self, tableView: tableView, indexPath: indexPath)
case .forecast:
let cell = dequeueReusableCell(type: ForecastDailyCell.self, tableView: tableView, indexPath: indexPath)
cell.delegate = self
cell.selectDayButtonAt(index: forecastViewModel.selectedDailyWeatherIndex)
if let daily = forecastViewModel.location?.daily,
let hourly = forecastViewModel.location?.hourly {
cell.configure(daily: daily, hourly: hourly)
if let daily = forecastViewModel.location?.daily {
cell.configure(daily: daily,
offset: forecastViewModel.offsetHolder.currentOffset,
selectedButtonIndex: forecastViewModel.selectedDailyWeatherIndex)
}
return cell
case .forecastInfo:
......@@ -76,18 +112,33 @@ class ForecastCellFactory {
}
}
public func willDisplay(cell:UITableViewCell) {
switch cell {
case let sunCell as CitySunCell:
sunCell.updateSunPosition()
case let moonCell as CityMoonCell:
moonCell.updateMoonPosition()
default:
break
private func hourlyCellFor(tableView:UITableView, indexPath:IndexPath) -> UITableViewCell {
guard let cellType = HourlyForecastCellType(rawValue: indexPath.row) else {
return UITableViewCell()
}
switch cellType {
case .day:
let cell = dequeueReusableCell(type: ForecastDayCell.self, tableView: tableView, indexPath: indexPath)
if let today = forecastViewModel.location?.today{
cell.configure(today: today)
}
return cell
case .tempInfo:
let cell = dequeueReusableCell(type: ForecastHourlyCell.self, tableView: tableView, indexPath: indexPath)
if let hourly = forecastViewModel.location?.hourly {
cell.configure(hourly: hourly)
}
return cell
case .precipitation:
let cell = dequeueReusableCell(type: PrecipitationCell.self, tableView: tableView, indexPath: indexPath)
if let hourly = forecastViewModel.location?.hourly {
cell.configure(with: hourly)
}
return cell
}
}
//Private
private func registerCell<T: ReusableCellProtocol>(type:T.Type, tableView:UITableView) {
tableView.register(type, forCellReuseIdentifier: T.kIdentifier)
}
......@@ -99,13 +150,12 @@ class ForecastCellFactory {
}
//MARK:- ForecastTimePeriodCell Delegate
extension ForecastCellFactory: ForecastTimePeriodCellDelegate {
func timePeriodCell(cell: ForecastTimePeriodCell, didSelectButtonAt index: Int) {
guard forecastViewModel.currentTimePeriod == .daily else { return }
forecastViewModel.selectDailyWeather(at: index)
extension ForecastCellFactory: ForecastDailyCellDelegate {
func timePeriodCell(cell: ForecastDailyCell, didSelectButtonAt index: Int) {
forecastViewModel.selectDailyWeatherAt(index: index)
}
func timePeriodCell(cell: ForecastTimePeriodCell, didSelectTimePeriod timePeriod: TimePeriod) {
forecastViewModel.setTimePeriod(timePeriod: timePeriod)
func timePeriodCell(cell: ForecastDailyCell, offsetDidChage offset: CGFloat) {
forecastViewModel.offsetHolder.update(offset: offset)
}
}
//
// ForecastTimePeriodCell.swift
// ForecastDailyCell.swift
// 1Weather
//
// Created by Dmitry Stepanets on 10.03.2021.
// Created by Dmitry Stepanets on 22.03.2021.
//
import UIKit
protocol ForecastTimePeriodCellDelegate:class {
func timePeriodCell(cell:ForecastTimePeriodCell, didSelectButtonAt index:Int)
func timePeriodCell(cell:ForecastTimePeriodCell, didSelectTimePeriod timePeriod:TimePeriod)
protocol ForecastDailyCellDelegate:class {
func timePeriodCell(cell:ForecastDailyCell, didSelectButtonAt index:Int)
func timePeriodCell(cell:ForecastDailyCell, offsetDidChage offset: CGFloat)
}
class ForecastTimePeriodCell: UITableViewCell {
class ForecastDailyCell: UITableViewCell {
//Private
private let periodSegmentedControl = ForecastTimePeriodControl(items: ["forecast.timePeriod.daily".localized(),
"forecast.timePeriod.hourly".localized()])
private let forecastTimePeriodView = ForecastTimePeriodView()
private let gradientView = GradientView(startColor: UIColor(hex: 0xffffff).withAlphaComponent(0),
endColor: UIColor(hex: 0xdaddec),
......@@ -24,93 +21,64 @@ class ForecastTimePeriodCell: UITableViewCell {
private var graphIsDrawn = false
//Public
weak var delegate:ForecastTimePeriodCellDelegate?
weak var delegate:ForecastDailyCellDelegate?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
prepareCell()
prepareSegmentedControl()
prepareGradient()
prepareForecastTimePeriodView()
prepareTimePeriodView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//Public
public func configure(daily:[DailyWeather], hourly:[HourlyWeather]) {
self.forecastTimePeriodView.set(daily: daily, hourly: hourly)
if graphIsDrawn == false {
self.handleSegmentDidChange()
self.graphIsDrawn = true
}
}
public func selectDayButtonAt(index:Int) {
self.forecastTimePeriodView.selectButtonAt(index: index)
}
@objc private func handleSegmentDidChange() {
guard let timePeriod = TimePeriod(rawValue: self.periodSegmentedControl.selectedSegmentIndex) else {
return
}
switch timePeriod {
case .daily:
self.forecastTimePeriodView.set(timePeriod: timePeriod, buttonType: ForecastDetailPeriodButton.self)
case .hourly:
self.forecastTimePeriodView.set(timePeriod: timePeriod, buttonType: ForecastPeriodButton.self)
public func configure(daily:[DailyWeather], offset:CGFloat = 0, selectedButtonIndex:Int = 0) {
self.forecastTimePeriodView.set(daily: daily, hourly: nil)
if self.forecastTimePeriodView.isEmpty {
self.forecastTimePeriodView.set(timePeriod: .daily, buttonType: ForecastDetailPeriodButton.self)
}
delegate?.timePeriodCell(cell: self, didSelectTimePeriod: timePeriod)
self.forecastTimePeriodView.selectButtonAt(index: selectedButtonIndex)
self.forecastTimePeriodView.update(offset: offset)
}
}
private extension ForecastTimePeriodCell {
private extension ForecastDailyCell {
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 prepareForecastTimePeriodView() {
func prepareTimePeriodView() {
forecastTimePeriodView.delegate = self
contentView.addSubview(forecastTimePeriodView)
forecastTimePeriodView.snp.makeConstraints { (make) in
make.left.equalToSuperview()
make.right.equalToSuperview()
make.top.equalTo(periodSegmentedControl.snp.bottom).offset(20).priority(.medium)
make.top.equalToSuperview().inset(20).priority(.medium)
make.bottom.equalToSuperview().inset(30)
make.height.equalTo(267)
}
}
func prepareGradient() {
contentView.addSubview(gradientView)
gradientView.snp.makeConstraints { (make) in
make.left.right.bottom.equalToSuperview()
make.height.equalTo(172)
}
}
}
//MARK:- ForecastTimePeriodView Delegate
extension ForecastTimePeriodCell: ForecastTimePeriodViewDelegate {
extension ForecastDailyCell: ForecastTimePeriodViewDelegate {
func forecastTimePeriodView(view: ForecastTimePeriodView, didSelectButtonAt index: Int) {
self.delegate?.timePeriodCell(cell: self, didSelectButtonAt: index)
}
func offsetDidChange(offset: CGFloat) {
self.delegate?.timePeriodCell(cell: self, offsetDidChage: offset)
}
}
//
// ForecastDayCell.swift
// 1Weather
//
// Created by Dmitry Stepanets on 22.03.2021.
//
import UIKit
class ForecastDayCell: UITableViewCell {
//Private
private let dateLabel = UILabel()
private let forecastLabel = UILabel()
private let gradientView = GradientView(startColor: UIColor(hex: 0xffffff).withAlphaComponent(0),
endColor: UIColor(hex: 0xdaddec),
opacity: 0.5)
private static var formatter:DateFormatter = {
let fmt = DateFormatter()
fmt.dateFormat = "d, E"
return fmt
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
prepareCell()
prepareGradient()
prepareLabels()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func configure(today:CurrentWeather) {
ForecastDayCell.formatter.timeZone = today.timeZone
dateLabel.text = ForecastDayCell.formatter.string(from: today.date)
let maxTemp = today.maxTemp?.shortString ?? "--"
let minTemp = today.minTemp?.shortString ?? "--"
forecastLabel.text = "\(today.type.localized(isDay: today.isDay)) | \(maxTemp)/\(minTemp)"
}
}
//MARK:- Prepare
private extension ForecastDayCell {
func prepareCell() {
selectionStyle = .none
contentView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
}
func prepareGradient() {
contentView.addSubview(gradientView)
gradientView.snp.makeConstraints { (make) in
make.left.right.bottom.equalToSuperview()
make.height.equalTo(41)
}
}
func prepareLabels() {
dateLabel.font = AppFont.SFPro.bold(size: 34)
dateLabel.textColor = ThemeManager.currentTheme.primaryTextColor
dateLabel.setContentHuggingPriority(.fittingSizeLevel, for: .vertical)
contentView.addSubview(dateLabel)
dateLabel.snp.makeConstraints { (make) in
make.left.top.equalToSuperview().inset(18)
}
forecastLabel.font = AppFont.SFPro.bold(size: 16)
forecastLabel.textColor = ThemeManager.currentTheme.primaryTextColor
contentView.addSubview(forecastLabel)
forecastLabel.snp.makeConstraints { (make) in
make.left.equalToSuperview().inset(18)
make.top.equalTo(dateLabel.snp.bottom)
make.bottom.equalToSuperview().inset(18)
}
}
}
//
// ForecastHourlyCell.swift
// 1Weather
//
// Created by Dmitry Stepanets on 22.03.2021.
//
import UIKit
class ForecastHourlyCell: UITableViewCell {
//Private
private let tempLabel = UILabel()
private let forecastTimePeriodView = ForecastTimePeriodView()
private let summaryView = UIView()
private let summaryImageView = UIImageView()
private let summaryLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
prepareCell()
prepareTempLabel()
prepareTimePeriodView()
prepareSummaryView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func configure(hourly:[HourlyWeather]) {
self.forecastTimePeriodView.set(daily: nil, hourly: hourly)
if self.forecastTimePeriodView.isEmpty {
self.forecastTimePeriodView.set(timePeriod: .hourly, buttonType: ForecastPeriodButton.self)
}
}
}
//MARK:- Prepare
private extension ForecastHourlyCell {
func prepareCell() {
selectionStyle = .none
contentView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
}
func prepareTempLabel() {
tempLabel.font = AppFont.SFPro.bold(size: 18)
tempLabel.textColor = ThemeManager.currentTheme.primaryTextColor
tempLabel.text = "condition.temperature".localized()
contentView.addSubview(tempLabel)
tempLabel.snp.makeConstraints { (make) in
make.left.top.equalToSuperview().inset(18)
}
}
func prepareTimePeriodView() {
contentView.addSubview(forecastTimePeriodView)
forecastTimePeriodView.snp.makeConstraints { (make) in
make.left.equalToSuperview()
make.right.equalToSuperview()
make.top.equalTo(tempLabel.snp.bottom).offset(18).priority(.medium)
make.height.equalTo(267)
}
}
func prepareSummaryView() {
summaryImageView.contentMode = .scaleAspectFit
summaryImageView.image = UIImage(named: "hot_indicator")
summaryView.addSubview(summaryImageView)
summaryImageView.snp.makeConstraints { (make) in
make.left.equalToSuperview().inset(20)
make.centerY.equalToSuperview()
make.width.height.equalTo(12)
}
summaryLabel.font = AppFont.SFPro.regular(size: 13)
summaryLabel.textColor = ThemeManager.currentTheme.secondaryTextColor
summaryLabel.text = "Hottest part of the day 12 AM - 1 PM"
summaryView.addSubview(summaryLabel)
summaryLabel.snp.makeConstraints { (make) in
make.left.equalTo(summaryImageView.snp.right).offset(8)
make.right.equalToSuperview().inset(8)
make.centerY.equalToSuperview()
}
summaryView.backgroundColor = UIColor(hex: 0xfaedda).withAlphaComponent(0.5)
summaryView.layer.cornerRadius = 12
contentView.addSubview(summaryView)
summaryView.snp.makeConstraints { (make) in
make.left.right.equalToSuperview().inset(18)
make.height.equalTo(40)
make.top.equalTo(forecastTimePeriodView.snp.bottom).offset(20)
make.bottom.equalToSuperview().inset(15)
}
}
}
......@@ -9,6 +9,7 @@ import UIKit
protocol DaysControlViewDelegate:class {
func didSelectButtonAt(index:Int)
func offsetDidChange(offset:CGFloat)
}
class DaysControlView: UIView {
......@@ -73,6 +74,12 @@ class DaysControlView: UIView {
}
}
public func update(offset:CGFloat) {
if self.scrollView.contentOffset.x != offset {
self.scrollView.setContentOffset(.init(x: offset, y: 0), animated: false)
}
}
@objc private func handleDayButton(button:DayControlButton) {
guard let buttons = stackView.arrangedSubviews as? [DayControlButton] else { return }
......@@ -100,6 +107,7 @@ private extension DaysControlView {
func prepareScrollView() {
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
scrollView.delegate = self
addSubview(scrollView)
scrollView.snp.makeConstraints { (make) in
......@@ -126,6 +134,13 @@ private extension DaysControlView {
}
}
//MARK:- UIScrollView Delegate
extension DaysControlView: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
delegate?.offsetDidChange(offset: scrollView.contentOffset.x)
}
}
//MARK:- Button
private class DayControlButton: UIControl {
//Private
......
......@@ -12,10 +12,15 @@ class ForecastViewController: UIViewController {
private let forecastCellFactory:ForecastCellFactory
private let cityButton = NavigationCityButton()
private let daysControlView = DaysControlView()
private let timePeriodControl = ForecastTimePeriodControl(items: ["forecast.timePeriod.daily".localized(),
"forecast.timePeriod.hourly".localized()])
private let tableView = UITableView()
private let viewModel:ForecastViewModel
private var timePeriodCellFrame = CGRect.zero
private var localizationObserver:Any?
private var timePeriod:TimePeriod {
return TimePeriod(rawValue: timePeriodControl.selectedSegmentIndex) ?? .daily
}
init(viewModel: ForecastViewModel) {
self.viewModel = viewModel
......@@ -31,16 +36,23 @@ class ForecastViewController: UIViewController {
super.viewDidLoad()
viewModel.delegate = self
viewModel.offsetHolder.delegate = self
prepareViewController()
prepareNavigationBar()
prepareTableView()
prepareTimePeriodControl()
prepareDayControlsView()
refreshCityButton()
refreshDayButtons()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.tableView.layoutTableHeaderView()
}
private func refreshCityButton() {
cityButton.configure(with: viewModel.location)
cityButton.isHidden = false
......@@ -55,6 +67,15 @@ class ForecastViewController: UIViewController {
}
}
@objc private func handleSegmentDidChange() {
guard let timePeriod = TimePeriod(rawValue: self.timePeriodControl.selectedSegmentIndex) else {
return
}
forecastCellFactory.setTimePeriod(timePeriod: timePeriod)
self.tableView.reloadData()
}
@objc private func handleCityButton() {
print("Handle city button")
}
......@@ -121,6 +142,22 @@ private extension ForecastViewController {
make.edges.equalToSuperview()
}
}
func prepareTimePeriodControl() {
let container = UIView()
container.addSubview(self.timePeriodControl)
timePeriodControl.selectedSegmentIndex = 0
timePeriodControl.addTarget(self, action: #selector(handleSegmentDidChange), for: .valueChanged)
self.timePeriodControl.snp.makeConstraints { (make) in
make.left.right.equalToSuperview().inset(18).priority(.init(999))
make.top.equalToSuperview().inset(30)
make.bottom.equalToSuperview().priority(.init(999))
make.height.equalTo(40).priority(.init(999))
}
tableView.tableHeaderView = container
}
}
//MARK:- UITableView Delegate
......@@ -129,7 +166,7 @@ extension ForecastViewController: UITableViewDelegate {
guard
let navVC = self.navigationController,
viewModel.location?.daily.isEmpty == false,
viewModel.currentTimePeriod == .daily
self.timePeriod == .daily
else {
return
}
......@@ -143,6 +180,7 @@ extension ForecastViewController: UITableViewDelegate {
if scrollView.contentOffset.y >= startPointY {
if !navVC.isNavigationBarHidden {
navVC.setNavigationBarHidden(true, animated: true)
self.daysControlView.update(offset: self.viewModel.offsetHolder.currentOffset)
UIView.animate(withDuration: 0.35) {
self.daysControlView.alpha = 1
}
......@@ -183,30 +221,37 @@ extension ForecastViewController: ForecastViewModelDelegate {
refreshDayButtons()
}
func selectedDailyWeatherDidChange() {
var indexPathToReload = [IndexPath]()
for index in 0..<forecastCellFactory.numberOfRows {
if index == 0 { continue }
func selectedWeatherDidChange() {
switch timePeriod {
case .daily:
var indexPathToReload = [IndexPath]()
for index in 0..<forecastCellFactory.numberOfRows {
if index == 0 { continue }
indexPathToReload.append([0, index])
}
indexPathToReload.append([0, index])
}
tableView.reloadRows(at: indexPathToReload, with: .none)
daysControlView.selectDayAt(index: viewModel.selectedDailyWeatherIndex)
}
func selectedTimePeriodDidChange() {
guard let timePeriodCell = tableView.cellForRow(at: [0,0]) as? ForecastTimePeriodCell else {
return
tableView.reloadRows(at: indexPathToReload, with: .none)
daysControlView.selectDayAt(index: viewModel.selectedDailyWeatherIndex)
case .hourly:
tableView.reloadData()
}
timePeriodCell.selectDayButtonAt(index: viewModel.selectedDailyWeatherIndex)
}
}
//MARK:- DaysControlView Delegate
extension ForecastViewController: DaysControlViewDelegate {
func didSelectButtonAt(index: Int) {
viewModel.selectDailyWeather(at: index)
viewModel.selectDailyWeatherAt(index: index)
}
func offsetDidChange(offset: CGFloat) {
viewModel.offsetHolder.update(offset: offset)
}
}
//MARK:- TimePeriodOffset Delegate
extension ForecastViewController: TimePeriodOffsetDelegate {
func offsetDidChange(newOffset: CGFloat) {
}
}
......@@ -84,7 +84,7 @@ private extension CityDayTimesCell {
func prepareStackView() {
stackView.axis = .horizontal
stackView.distribution = .fillProportionally
stackView.distribution = .equalCentering
stackView.alignment = .center
stackView.spacing = 0
stackView.clipsToBounds = false
......
......@@ -45,7 +45,7 @@ private extension DayTimeView {
addSubview(dayTimeLabel)
forecastImageView.contentMode = .scaleAspectFit
forecastImageView.image = nil //TODO: we need a placeholder here?
forecastImageView.image = WeatherType.unknown.image(isDay: true)
addSubview(forecastImageView)
tempLabel.font = AppFont.SFPro.bold(size: 18)
......@@ -53,7 +53,7 @@ private extension DayTimeView {
tempLabel.text = "--"
addSubview(tempLabel)
dayTimeConditionLabel.numberOfLines = 2
dayTimeConditionLabel.numberOfLines = 3
dayTimeConditionLabel.lineBreakMode = .byWordWrapping
dayTimeConditionLabel.textAlignment = .center
dayTimeConditionLabel.font = AppFont.SFPro.regular(size: 14)
......@@ -70,19 +70,24 @@ private extension DayTimeView {
forecastImageView.snp.makeConstraints { (make) in
make.width.height.equalTo(28)
make.centerX.equalToSuperview()
make.top.equalTo(dayTimeLabel.snp.bottom).offset(33)
make.top.equalTo(dayTimeLabel.snp.bottom).offset(18)
}
tempLabel.snp.makeConstraints { (make) in
make.centerX.equalToSuperview()
make.top.equalTo(forecastImageView.snp.bottom).offset(30)
make.top.equalTo(forecastImageView.snp.bottom).offset(18)
}
dayTimeConditionLabel.snp.makeConstraints { (make) in
make.left.right.equalToSuperview().inset(2)
make.top.equalTo(tempLabel.snp.bottom).offset(14)
make.bottom.equalToSuperview().inset(24)
make.left.right.equalToSuperview().inset(8)
make.top.equalTo(tempLabel.snp.bottom).offset(14).priority(.low)
make.bottom.equalToSuperview().inset(18)
make.height.equalTo(52)
}
self.snp.makeConstraints { (make) in
make.height.equalTo(210)
make.width.equalTo(90)
}
}
......@@ -94,7 +99,7 @@ private extension DayTimeView {
separatorView.snp.makeConstraints { (make) in
make.top.equalTo(dayTimeLabel)
make.bottom.equalTo(dayTimeConditionLabel)
make.bottom.equalToSuperview().inset(18)
make.right.equalToSuperview()
make.width.equalTo(1)
}
......
......@@ -9,10 +9,14 @@ import UIKit
class PrecipButton: UIControl {
//Private
private static let formatter:DateFormatter = {
private static var dailyFormatter:DateFormatter = {
let fmt = DateFormatter()
fmt.dateFormat = "d, E"
return fmt
}()
private static var hourlyFormatter:DateFormatter = {
let fmt = DateFormatter()
fmt.dateFormat = "h a"
return fmt
}()
private let valueLabel = UILabel()
......@@ -65,11 +69,37 @@ class PrecipButton: UIControl {
self.precipView.set(value: CGFloat(percent)/100.0)
self.valueLabel.text = "\(Int(percent))%"
if Calendar.timeZoneCalendar(timeZone: daily.timeZone).isDateInToday(daily.date) {
self.timeLabel.text = "day.today".localized()
}
else {
self.timeLabel.text = PrecipButton.formatter.string(from: daily.date)
PrecipButton.dailyFormatter.timeZone = daily.timeZone
self.timeLabel.text = PrecipButton.dailyFormatter.string(from: daily.date)
}
}
public func configure(with hourly:HourlyWeather) {
guard let percent = hourly.precipitationProbability else {
self.precipView.set(value: 0)
self.valueLabel.text = "0%"
self.timeLabel.text = nil
return
}
self.precipView.set(value: CGFloat(percent)/100.0)
self.valueLabel.text = "\(Int(percent))%"
if let nowDate = Date.nowDate(timeZone: hourly.timeZone) {
if Calendar.timeZoneCalendar(timeZone: hourly.timeZone).isDate(hourly.date, equalTo: nowDate, toGranularity: .hour) {
self.timeLabel.text = "day.now".localized().uppercased()
}
else {
PrecipButton.hourlyFormatter.timeZone = hourly.timeZone
self.timeLabel.text = PrecipButton.hourlyFormatter.string(from: hourly.date)
}
}
else {
self.timeLabel.text = "--"
}
}
}
......
//
// CityPrecipCell.swift
// PrecipitationCell.swift
// 1Weather
//
// Created by Dmitry Stepanets on 24.02.2021.
......@@ -7,7 +7,7 @@
import UIKit
class CityPrecipCell: UITableViewCell {
class PrecipitationCell: UITableViewCell {
//Private
private let headingLabel = UILabel()
private let headingButton = ArrowButton()
......@@ -32,11 +32,9 @@ class CityPrecipCell: UITableViewCell {
}
public func configure(with dayily:[DailyWeather]) {
stackView.arrangedSubviews.forEach {
stackView.removeArrangedSubview($0)
$0.removeFromSuperview()
}
guard stackView.arrangedSubviews.isEmpty else { return }
self.headingButton.isHidden = false
for index in 0..<dayily.count {
let precipButton = PrecipButton()
precipButton.isSelected = index == 1
......@@ -47,6 +45,23 @@ class CityPrecipCell: UITableViewCell {
stackView.layoutIfNeeded()
}
public func configure(with hourly:[HourlyWeather]) {
guard stackView.arrangedSubviews.isEmpty else { return }
self.headingLabel.font = AppFont.SFPro.bold(size: 18)
self.headingButton.isHidden = true
self.headingLabel.text = "precipitation.title".localized().capitalized
self.headingLabel.textColor = ThemeManager.currentTheme.primaryTextColor
for index in 0..<hourly.count {
let precipButton = PrecipButton()
precipButton.isSelected = index == 0
precipButton.configure(with: hourly[index])
precipButton.addTarget(self, action: #selector(handlePrecipButton(button:)), for: .touchUpInside)
stackView.addArrangedSubview(precipButton)
}
stackView.layoutIfNeeded()
}
//Private
@objc private func handleArrowButton() {
......@@ -62,7 +77,7 @@ class CityPrecipCell: UITableViewCell {
}
//MARK:- Prepare
private extension CityPrecipCell {
private extension PrecipitationCell {
func prepareCell() {
selectionStyle = .none
contentView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
......
......@@ -29,7 +29,7 @@ class TodayCellFactory {
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: PrecipitationCell.self, tableView: tableView)
registerCell(type: CityDayTimesCell.self, tableView: tableView)
registerCell(type: CityAirQualityCell.self, tableView: tableView)
registerCell(type: CitySunCell.self, tableView: tableView)
......@@ -62,7 +62,7 @@ class TodayCellFactory {
cell.configure(with: loc)
return cell
case .precipitation:
let cell = dequeueReusableCell(type: CityPrecipCell.self, tableView: tableView, indexPath: indexPath)
let cell = dequeueReusableCell(type: PrecipitationCell.self, tableView: tableView, indexPath: indexPath)
cell.configure(with: loc.daily)
return cell
case .dayTime:
......
......@@ -8,16 +8,16 @@
import UIKit
protocol ForecastViewModelDelegate:ViewModelDelegate {
func selectedDailyWeatherDidChange()
func selectedTimePeriodDidChange()
func selectedWeatherDidChange()
}
class ForecastViewModel: ViewModelProtocol {
//Public
public let offsetHolder = TimePeriodOffsetHolder()
public weak var delegate:ForecastViewModelDelegate?
public private(set) var location: Location?
public private(set) var selectedDailyWeather:DailyWeather?
public private(set) var currentTimePeriod = TimePeriod.daily
public private(set) var selectedHourlyWeather:HourlyWeather?
public var selectedDailyWeatherIndex:Int {
guard let loc = self.location else { return -1 }
......@@ -46,24 +46,24 @@ class ForecastViewModel: ViewModelProtocol {
locationManager.updateWeather()
}
public func select(dailyWeather:DailyWeather) {
self.selectedDailyWeather = dailyWeather
self.delegate?.selectedDailyWeatherDidChange()
}
public func selectDailyWeather(at index:Int) {
public func selectDailyWeatherAt(index:Int) {
guard let daily = location?.daily else { return }
daily.enumerated().forEach {
if $0 == index {
self.selectedDailyWeather = $1
}
}
self.delegate?.selectedDailyWeatherDidChange()
self.delegate?.selectedWeatherDidChange()
}
public func setTimePeriod(timePeriod:TimePeriod) {
self.currentTimePeriod = timePeriod
self.delegate?.selectedTimePeriodDidChange()
public func selectHourlyWeatherAt(index:Int ) {
guard let hourly = location?.hourly else { return }
hourly.enumerated().forEach {
if $0 == index {
self.selectedHourlyWeather = $1
}
}
self.delegate?.selectedWeatherDidChange()
}
}
......@@ -73,7 +73,8 @@ extension ForecastViewModel: LocationManagerDelegate {
DispatchQueue.main.async {
print("TVM-Forecast")
self.location = newLocation
self.selectDailyWeather(at: 0)
self.selectDailyWeatherAt(index: 0)
self.selectHourlyWeatherAt(index: 0)
self.delegate?.viewModelDidChange(model: self)
}
}
......
......@@ -52,7 +52,7 @@
<key>XMLCoder.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>6</integer>
<integer>5</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