Commit 4555a346 by Dmitry Stepanets

Merge branch 'feature/rainfall-dimension' into release/5.4.1

# Conflicts:
#	1Weather/UI/SharedViews/MinutelyForecastView/MinutelyForecastView.swift
parents 08bed76c 24b1d100
...@@ -269,6 +269,7 @@ ...@@ -269,6 +269,7 @@
"settings.unit.wind" = "Wind"; "settings.unit.wind" = "Wind";
"settings.unit.pressure" = "Pressure"; "settings.unit.pressure" = "Pressure";
"settings.unit.distance" = "Distance"; "settings.unit.distance" = "Distance";
"settings.unit.rainfall" = "Rainfall";
"settings.language" = "Language"; "settings.language" = "Language";
"settings.manageNotifications" = "Manage Notifications"; "settings.manageNotifications" = "Manage Notifications";
"settings.locationAccess" = "Locations Access"; "settings.locationAccess" = "Locations Access";
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
import UIKit import UIKit
import OneWeatherCore import OneWeatherCore
import OneWeatherAnalytics
class PrecipitationCell: UITableViewCell { class PrecipitationCell: UITableViewCell {
//Private //Private
...@@ -176,6 +177,7 @@ class PrecipitationCell: UITableViewCell { ...@@ -176,6 +177,7 @@ class PrecipitationCell: UITableViewCell {
else { else {
scrollView.isHidden = true scrollView.isHidden = true
minutelyForecastView.isHidden = false minutelyForecastView.isHidden = false
AppAnalytics.shared.log(event: .ANALYTICS_MINUTELY_PRECIP_TODAY_SEEN)
} }
} }
} }
......
...@@ -108,20 +108,23 @@ private extension MinutelyForecastDetailsView { ...@@ -108,20 +108,23 @@ private extension MinutelyForecastDetailsView {
separator.snp.makeConstraints { make in separator.snp.makeConstraints { make in
make.width.equalTo(1) make.width.equalTo(1)
make.height.equalToSuperview().multipliedBy(0.65) make.height.equalToSuperview().multipliedBy(0.65)
make.right.equalTo(tempLabel.snp.left).offset(-6) make.left.equalToSuperview().inset(70)
make.centerY.equalToSuperview()
}
tempLabel.snp.makeConstraints { make in
make.left.equalTo(separator.snp.right).offset(8)
make.centerY.equalToSuperview() make.centerY.equalToSuperview()
} }
forecastImage.snp.makeConstraints { make in forecastImage.snp.makeConstraints { make in
make.width.height.equalTo(28) make.width.height.equalTo(28)
make.centerY.equalToSuperview() make.centerY.equalToSuperview()
make.left.equalTo(tempLabel.snp.right).offset(4)
make.right.equalToSuperview().inset(8) make.right.equalToSuperview().inset(8)
} }
tempLabel.snp.makeConstraints { make in
make.right.equalTo(forecastImage.snp.left).offset(-2)
make.centerY.equalToSuperview()
}
} }
func prepareTriangle() { func prepareTriangle() {
......
...@@ -9,6 +9,7 @@ import UIKit ...@@ -9,6 +9,7 @@ import UIKit
import OneWeatherCore import OneWeatherCore
import Accelerate import Accelerate
import SnapKit import SnapKit
import OneWeatherAnalytics
enum MinutelyForecastType { enum MinutelyForecastType {
case temperature case temperature
...@@ -117,6 +118,7 @@ private class SwipeNudgeView: UIView { ...@@ -117,6 +118,7 @@ private class SwipeNudgeView: UIView {
class MinutelyForecastView: UIView { class MinutelyForecastView: UIView {
//Private //Private
private let kLevelWidth = 3 private let kLevelWidth = 3
private let kScrollViewRightInset: CGFloat = 20
private let detailsInfoView = MinutelyForecastDetailsView() private let detailsInfoView = MinutelyForecastDetailsView()
private let swipeNudgeView = SwipeNudgeView() private let swipeNudgeView = SwipeNudgeView()
private let levelsStackView = UIStackView() private let levelsStackView = UIStackView()
...@@ -134,6 +136,7 @@ class MinutelyForecastView: UIView { ...@@ -134,6 +136,7 @@ class MinutelyForecastView: UIView {
private var lastSelectedLevelIndex = 0 private var lastSelectedLevelIndex = 0
private var minutelyForecast = [MinutelyItem]() private var minutelyForecast = [MinutelyItem]()
private var timer: Timer? private var timer: Timer?
private var trackUserSwipe = false
private var forecastType = MinutelyForecastType.temperature private var forecastType = MinutelyForecastType.temperature
private var smartTextProvider: SmartTextProvider = SmartTextProvider(prioritizedSmartTexts: []) private var smartTextProvider: SmartTextProvider = SmartTextProvider(prioritizedSmartTexts: [])
...@@ -168,7 +171,7 @@ class MinutelyForecastView: UIView { ...@@ -168,7 +171,7 @@ class MinutelyForecastView: UIView {
override func layoutSubviews() { override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
let leftInset = detailsInfoView.frame.origin.x - scrollView.frame.origin.x + detailsInfoView.frame.width / 2 let leftInset = (scrollView.bounds.width - kScrollViewRightInset) / 2
if scrollView.contentInset.left != leftInset { if scrollView.contentInset.left != leftInset {
scrollView.contentInset = .init(top: 0, left: leftInset, bottom: 0, right: leftInset + 20) scrollView.contentInset = .init(top: 0, left: leftInset, bottom: 0, right: leftInset + 20)
scrollView.setContentOffset(.init(x: -leftInset, y: 0), animated: false) scrollView.setContentOffset(.init(x: -leftInset, y: 0), animated: false)
...@@ -271,7 +274,7 @@ class MinutelyForecastView: UIView { ...@@ -271,7 +274,7 @@ class MinutelyForecastView: UIView {
timeZone: location?.timeZone ?? .current, timeZone: location?.timeZone ?? .current,
colors: kTemperatureColors) colors: kTemperatureColors)
case .precipitation: case .precipitation:
self.detailsInfoView.configure(valueStirng: "\(Int(minutelyItem.precipitation * 100))%", self.detailsInfoView.configure(valueStirng: minutelyItem.precipitation.settingsConverted.string,
date: minutelyItem.time, date: minutelyItem.time,
weatherImage: minutelyItem.weatherTypeImage, weatherImage: minutelyItem.weatherTypeImage,
timeZone: location?.timeZone ?? .current, timeZone: location?.timeZone ?? .current,
...@@ -403,14 +406,14 @@ class MinutelyForecastView: UIView { ...@@ -403,14 +406,14 @@ class MinutelyForecastView: UIView {
private func updatePrecipitationChart() { private func updatePrecipitationChart() {
guard guard
let maxPrecip = (minutelyForecast.compactMap{ $0.precipitation }.max{ $0 < $1 }), let maxPrecip = (minutelyForecast.compactMap{ $0.precipitation }.max{ $0.settingsConvertedValue < $1.settingsConvertedValue }),
let minPrecip = (minutelyForecast.compactMap{ $0.precipitation }.min{ $0 < $1 }) let minPrecip = (minutelyForecast.compactMap{ $0.precipitation }.min{ $0.settingsConvertedValue < $1.settingsConvertedValue })
else { else {
return return
} }
var uniqPrecips = minutelyForecast.compactMap{ $0.precipitation }.unique().sorted{$0 > $1} var uniqPrecips = minutelyForecast.compactMap{ $0.precipitation }.unique().sorted{$0.settingsConvertedValue > $1.settingsConvertedValue}
if uniqPrecips.count > 4 { if uniqPrecips.count > 4 {
let uniqMax = uniqPrecips.removeFirst() let uniqMax = uniqPrecips.removeFirst()
let uniqMin = uniqPrecips.removeLast() let uniqMin = uniqPrecips.removeLast()
...@@ -426,7 +429,7 @@ class MinutelyForecastView: UIView { ...@@ -426,7 +429,7 @@ class MinutelyForecastView: UIView {
for precipitation in uniqPrecips { for precipitation in uniqPrecips {
let label = UILabel() let label = UILabel()
label.text = "\(Int(precipitation * 100))%" label.text = precipitation.settingsConverted.shortString
label.font = AppFont.SFPro.regular(size: 10) label.font = AppFont.SFPro.regular(size: 10)
label.setContentHuggingPriority(.fittingSizeLevel, for: .vertical) label.setContentHuggingPriority(.fittingSizeLevel, for: .vertical)
label.sizeToFit() label.sizeToFit()
...@@ -436,8 +439,9 @@ class MinutelyForecastView: UIView { ...@@ -436,8 +439,9 @@ class MinutelyForecastView: UIView {
for index in 0..<minutelyForecast.count { for index in 0..<minutelyForecast.count {
let view = MinutelyLevelView(forecastType: .precipitation) let view = MinutelyLevelView(forecastType: .precipitation)
levelsStackView.addArrangedSubview(view) levelsStackView.addArrangedSubview(view)
let diff = maxPrecip - minPrecip == 0 ? 1 : maxPrecip - minPrecip let diff = maxPrecip.settingsConvertedValue - minPrecip.settingsConvertedValue == 0 ? 1
let level = (0.05 + 0.9 * ((minutelyForecast[index].precipitation - minPrecip) / diff)) : maxPrecip.settingsConvertedValue - minPrecip.settingsConvertedValue
let level = (0.05 + 0.9 * ((minutelyForecast[index].precipitation.settingsConvertedValue - minPrecip.settingsConvertedValue) / diff))
view.snp.makeConstraints { make in view.snp.makeConstraints { make in
make.width.equalTo(kLevelWidth) make.width.equalTo(kLevelWidth)
make.height.equalToSuperview().multipliedBy(level) make.height.equalToSuperview().multipliedBy(level)
...@@ -486,7 +490,6 @@ private extension MinutelyForecastView { ...@@ -486,7 +490,6 @@ private extension MinutelyForecastView {
//Constraints //Constraints
detailsInfoView.snp.makeConstraints { make in detailsInfoView.snp.makeConstraints { make in
make.width.equalTo(158)
make.height.equalTo(50) make.height.equalTo(50)
make.top.equalToSuperview() make.top.equalToSuperview()
make.centerX.equalToSuperview() make.centerX.equalToSuperview()
...@@ -525,7 +528,7 @@ private extension MinutelyForecastView { ...@@ -525,7 +528,7 @@ private extension MinutelyForecastView {
scrollView.snp.makeConstraints { make in scrollView.snp.makeConstraints { make in
make.left.equalToSuperview().inset(42) make.left.equalToSuperview().inset(42)
make.top.equalTo(detailsInfoView.snp.bottom).offset(8) make.top.equalTo(detailsInfoView.snp.bottom).offset(8)
make.right.equalToSuperview().inset(20) make.right.equalToSuperview().inset(kScrollViewRightInset)
} }
} }
...@@ -578,8 +581,14 @@ private extension MinutelyForecastView { ...@@ -578,8 +581,14 @@ private extension MinutelyForecastView {
//MARK:- UIScrollView Delegate //MARK:- UIScrollView Delegate
extension MinutelyForecastView: UIScrollViewDelegate { extension MinutelyForecastView: UIScrollViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.trackUserSwipe = false
}
func scrollViewDidScroll(_ scrollView: UIScrollView) { func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.panGestureRecognizer.state == .began { if scrollView.panGestureRecognizer.state == .began {
self.trackUserSwipe = true
AppAnalytics.shared.log(event: .ANALYTICS_MINUTELY_SWIPE)
timer?.invalidate() timer?.invalidate()
timer = nil timer = nil
...@@ -601,7 +610,7 @@ extension MinutelyForecastView: UIScrollViewDelegate { ...@@ -601,7 +610,7 @@ extension MinutelyForecastView: UIScrollViewDelegate {
self.updateDetailsView(minutelyItem: minutelyForecast[cachedValue.key]) self.updateDetailsView(minutelyItem: minutelyForecast[cachedValue.key])
if lastSelectedLevelIndex != cachedValue.key { if trackUserSwipe && lastSelectedLevelIndex != cachedValue.key {
lastSelectedLevelIndex = cachedValue.key lastSelectedLevelIndex = cachedValue.key
feedbackGenerator.prepare() feedbackGenerator.prepare()
feedbackGenerator.selectionChanged() feedbackGenerator.selectionChanged()
......
...@@ -149,10 +149,20 @@ class ForecastCellFactory: CellFactory { ...@@ -149,10 +149,20 @@ class ForecastCellFactory: CellFactory {
return cell return cell
case .forecastHourly: case .forecastHourly:
let cell = dequeueReusableCell(type: ForecastHourlyCell.self, tableView: tableView, indexPath: indexPath) let cell = dequeueReusableCell(type: ForecastHourlyCell.self, tableView: tableView, indexPath: indexPath)
if let hourly = forecastViewModel.location?.hourly { if timePeriod == .minutely {
if cellsToUpdate.contains(.forecastHourly) { if let location = forecastViewModel.location {
cell.configure(hourly: hourly) if cellsToUpdate.contains(.forecastHourly) {
cellsToUpdate.remove(.forecastHourly) cell.configure(withMinutelyLocation: location)
cellsToUpdate.remove(.forecastHourly)
}
}
}
else {
if let hourly = forecastViewModel.location?.hourly {
if cellsToUpdate.contains(.forecastHourly) {
cell.configure(hourly: hourly)
cellsToUpdate.remove(.forecastHourly)
}
} }
} }
return cell return cell
...@@ -187,10 +197,8 @@ class ForecastCellFactory: CellFactory { ...@@ -187,10 +197,8 @@ class ForecastCellFactory: CellFactory {
switch timePeriod { switch timePeriod {
case .daily: case .daily:
return self.dailySection.rows[indexPath.row] return self.dailySection.rows[indexPath.row]
case .hourly: case .hourly, .minutely:
return self.hourlySection.rows[indexPath.row] return self.hourlySection.rows[indexPath.row]
default:
return .minutely
} }
} }
......
...@@ -11,6 +11,7 @@ import OneWeatherCore ...@@ -11,6 +11,7 @@ import OneWeatherCore
class ForecastHourlyCell: UITableViewCell { class ForecastHourlyCell: UITableViewCell {
//Private //Private
private let tempLabel = UILabel() private let tempLabel = UILabel()
private let minutelyForecastView = MinutelyForecastView()
private let forecastTimePeriodView = ForecastTimePeriodView() private let forecastTimePeriodView = ForecastTimePeriodView()
private let descriptionView = ForecastDescriptionView(lightStyleBackgroundColor: UIColor(hex: 0xfaedda).withAlphaComponent(0.5), private let descriptionView = ForecastDescriptionView(lightStyleBackgroundColor: UIColor(hex: 0xfaedda).withAlphaComponent(0.5),
gradientColors: [UIColor(hex: 0xe81e15).withAlphaComponent(0.33).cgColor, gradientColors: [UIColor(hex: 0xe81e15).withAlphaComponent(0.33).cgColor,
...@@ -22,6 +23,7 @@ class ForecastHourlyCell: UITableViewCell { ...@@ -22,6 +23,7 @@ class ForecastHourlyCell: UITableViewCell {
prepareCell() prepareCell()
prepareTempLabel() prepareTempLabel()
prepareTimePeriodView() prepareTimePeriodView()
prepareMinutelyView()
prepareDescriptionView() prepareDescriptionView()
updateUI() updateUI()
} }
...@@ -45,6 +47,14 @@ class ForecastHourlyCell: UITableViewCell { ...@@ -45,6 +47,14 @@ class ForecastHourlyCell: UITableViewCell {
public func configure(hourly:[HourlyWeather]) { public func configure(hourly:[HourlyWeather]) {
self.forecastTimePeriodView.set(daily: nil, hourly: hourly) self.forecastTimePeriodView.set(daily: nil, hourly: hourly)
self.forecastTimePeriodView.set(forecastType: .hourly, buttonType: ForecastPeriodButton.self) self.forecastTimePeriodView.set(forecastType: .hourly, buttonType: ForecastPeriodButton.self)
self.forecastTimePeriodView.isHidden = false
self.minutelyForecastView.isHidden = true
}
public func configure(withMinutelyLocation location: Location) {
self.minutelyForecastView.configure(with: location, forecastType: .temperature)
self.minutelyForecastView.isHidden = false
self.forecastTimePeriodView.isHidden = true
} }
} }
...@@ -74,6 +84,14 @@ private extension ForecastHourlyCell { ...@@ -74,6 +84,14 @@ private extension ForecastHourlyCell {
} }
} }
func prepareMinutelyView() {
contentView.addSubview(minutelyForecastView)
minutelyForecastView.snp.makeConstraints { make in
make.edges.equalTo(forecastTimePeriodView)
}
}
func prepareDescriptionView() { func prepareDescriptionView() {
//TODO: Hide the description for now //TODO: Hide the description for now
descriptionView.isHidden = true descriptionView.isHidden = true
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
import UIKit import UIKit
import OneWeatherAnalytics import OneWeatherAnalytics
import OneWeatherCore
class ForecastViewController: UIViewController { class ForecastViewController: UIViewController {
//Private //Private
...@@ -19,6 +20,7 @@ class ForecastViewController: UIViewController { ...@@ -19,6 +20,7 @@ class ForecastViewController: UIViewController {
private let tableView = UITableView() private let tableView = UITableView()
private let viewModel:ForecastViewModel private let viewModel:ForecastViewModel
private var timePeriodCellFrame = CGRect.zero private var timePeriodCellFrame = CGRect.zero
private let featureAvailability = FeatureAvailabilityManager.shared
private var localizationObserver:Any? private var localizationObserver:Any?
private var timePeriod:TimePeriod { private var timePeriod:TimePeriod {
return TimePeriod(rawValue: timePeriodControl.selectedSegmentIndex) ?? .daily return TimePeriod(rawValue: timePeriodControl.selectedSegmentIndex) ?? .daily
...@@ -57,6 +59,7 @@ class ForecastViewController: UIViewController { ...@@ -57,6 +59,7 @@ class ForecastViewController: UIViewController {
super.viewDidAppear(animated) super.viewDidAppear(animated)
analytics(log: .ANALYTICS_VIEW_FORECAST) analytics(log: .ANALYTICS_VIEW_FORECAST)
analyticsLogCurrentTimePeriod() analyticsLogCurrentTimePeriod()
refreshTimePeriodControl()
} }
override func viewDidLayoutSubviews() { override func viewDidLayoutSubviews() {
...@@ -89,6 +92,18 @@ class ForecastViewController: UIViewController { ...@@ -89,6 +92,18 @@ class ForecastViewController: UIViewController {
} }
} }
private func refreshTimePeriodControl() {
if featureAvailability?.isAvailable(feature: .minutelyForecast) == true {
self.timePeriodControl.set(items: ["forecast.timePeriod.daily".localized(),
"forecast.timePeriod.hourly".localized(),
"forecast.timePeriod.minutely".localized()])
}
else {
self.timePeriodControl.set(items: ["forecast.timePeriod.daily".localized(),
"forecast.timePeriod.hourly".localized()])
}
}
public func switchTo(timePeriod: TimePeriod) { public func switchTo(timePeriod: TimePeriod) {
if self.timePeriodControl.selectedSegmentIndex != timePeriod.rawValue { if self.timePeriodControl.selectedSegmentIndex != timePeriod.rawValue {
self.timePeriodControl.selectedSegmentIndex = timePeriod.rawValue self.timePeriodControl.selectedSegmentIndex = timePeriod.rawValue
...@@ -260,6 +275,7 @@ extension ForecastViewController: UITableViewDataSource { ...@@ -260,6 +275,7 @@ extension ForecastViewController: UITableViewDataSource {
extension ForecastViewController: ForecastViewModelDelegate { extension ForecastViewController: ForecastViewModelDelegate {
func viewModelDidChange<P>(model: P) where P : ViewModelProtocol { func viewModelDidChange<P>(model: P) where P : ViewModelProtocol {
refreshCityButton() refreshCityButton()
refreshTimePeriodControl()
forecastCellFactory.setNeedsUpdate() forecastCellFactory.setNeedsUpdate()
tableView.reloadData() tableView.reloadData()
refreshDayButtons() refreshDayButtons()
......
...@@ -16,6 +16,7 @@ public enum SettingsRow { ...@@ -16,6 +16,7 @@ public enum SettingsRow {
case wind case wind
case pressure case pressure
case distance case distance
case rainfall
case language case language
case manageNotifications case manageNotifications
case locationAccess case locationAccess
...@@ -26,7 +27,7 @@ public enum SettingsRow { ...@@ -26,7 +27,7 @@ public enum SettingsRow {
return [.layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner, .layerMaxXMinYCorner] return [.layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner, .layerMaxXMinYCorner]
case .temperature: case .temperature:
return [.layerMinXMinYCorner, .layerMaxXMinYCorner] return [.layerMinXMinYCorner, .layerMaxXMinYCorner]
case .distance: case .rainfall:
return [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] return [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
case .language: case .language:
return [.layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner, .layerMaxXMinYCorner] return [.layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner, .layerMaxXMinYCorner]
...@@ -51,6 +52,8 @@ public enum SettingsRow { ...@@ -51,6 +52,8 @@ public enum SettingsRow {
return "settings.unit.pressure".localized() return "settings.unit.pressure".localized()
case .distance: case .distance:
return "settings.unit.distance".localized() return "settings.unit.distance".localized()
case .rainfall:
return "settings.unit.rainfall".localized()
case .language: case .language:
return "settings.language".localized() return "settings.language".localized()
case .manageNotifications: case .manageNotifications:
...@@ -95,7 +98,7 @@ class SettingsCellFactory: CellFactory { ...@@ -95,7 +98,7 @@ class SettingsCellFactory: CellFactory {
} }
array.append(contentsOf: [ array.append(contentsOf: [
SettingsDataSource(section: .units, rows: [.temperature, .wind, .pressure, .distance]), SettingsDataSource(section: .units, rows: [.temperature, .wind, .pressure, .distance, .rainfall]),
// SettingsDataSource(section: .language, rows: [.language]), // SettingsDataSource(section: .language, rows: [.language]),
SettingsDataSource(section: .other, rows: [ SettingsDataSource(section: .other, rows: [
// .manageNotifications, // .manageNotifications,
...@@ -149,6 +152,9 @@ class SettingsCellFactory: CellFactory { ...@@ -149,6 +152,9 @@ class SettingsCellFactory: CellFactory {
case .distance: case .distance:
name = viewModel.settings.distanceType.name name = viewModel.settings.distanceType.name
symbol = viewModel.settings.distanceType.symbol symbol = viewModel.settings.distanceType.symbol
case .rainfall:
name = viewModel.settings.rainfallType.name
symbol = viewModel.settings.rainfallType.symbol
default: default:
break break
} }
......
...@@ -32,6 +32,8 @@ class SettingsDetailsCellFactory: CellFactory { ...@@ -32,6 +32,8 @@ class SettingsDetailsCellFactory: CellFactory {
return viewModel.pressures.count return viewModel.pressures.count
case .distance: case .distance:
return viewModel.distances.count return viewModel.distances.count
case .rainfall:
return viewModel.rainFalls.count
case .language: case .language:
return viewModel.languages.count return viewModel.languages.count
default: default:
...@@ -77,6 +79,10 @@ class SettingsDetailsCellFactory: CellFactory { ...@@ -77,6 +79,10 @@ class SettingsDetailsCellFactory: CellFactory {
cell.configure(item: viewModel.distances[indexPath.row].name, cell.configure(item: viewModel.distances[indexPath.row].name,
isSelected: indexPath.row == viewModel.selectedDistanceIndex, isSelected: indexPath.row == viewModel.selectedDistanceIndex,
roundedConrers: mask) roundedConrers: mask)
case .rainfall:
cell.configure(item: viewModel.rainFalls[indexPath.row].name,
isSelected: indexPath.row == viewModel.selectedRainfallIndex,
roundedConrers: mask)
case .language: case .language:
cell.configure(item: Localize.displayNameForLanguage(viewModel.languages[indexPath.row]), cell.configure(item: Localize.displayNameForLanguage(viewModel.languages[indexPath.row]),
isSelected: indexPath.row == viewModel.selectedLanguageIndex, isSelected: indexPath.row == viewModel.selectedLanguageIndex,
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
import UIKit import UIKit
import OneWeatherCore import OneWeatherCore
import OneWeatherAnalytics
class TodayForecastTimePeriodCell: UITableViewCell { class TodayForecastTimePeriodCell: UITableViewCell {
//Private //Private
...@@ -67,6 +68,8 @@ class TodayForecastTimePeriodCell: UITableViewCell { ...@@ -67,6 +68,8 @@ class TodayForecastTimePeriodCell: UITableViewCell {
let forecastType = timePeriod == .daily ? ForecastType.daily : ForecastType.hourly let forecastType = timePeriod == .daily ? ForecastType.daily : ForecastType.hourly
self.forecastTimePeriodView.set(forecastType: forecastType, buttonType: ForecastPeriodButton.self) self.forecastTimePeriodView.set(forecastType: forecastType, buttonType: ForecastPeriodButton.self)
case .minutely: case .minutely:
AppAnalytics.shared.log(event: .ANALYTICS_VIEW_FORECAST_MINUTELY)
AppAnalytics.shared.log(event: .ANALYTICS_MINUTELY_TEMP_TODAY_SEEN)
forecastTimePeriodView.isHidden = true forecastTimePeriodView.isHidden = true
minutelyForecastView.isHidden = false minutelyForecastView.isHidden = false
} }
......
...@@ -26,6 +26,9 @@ class SettingsDetailsViewModel: ViewModelProtocol { ...@@ -26,6 +26,9 @@ class SettingsDetailsViewModel: ViewModelProtocol {
public var distances:[UnitLength] { public var distances:[UnitLength] {
return [.miles, .kilometers] return [.miles, .kilometers]
} }
public var rainFalls: [UnitRainfall] {
return [.millimetersPerHour, .inchesPerHour]
}
public var languages:[String] { public var languages:[String] {
return Localize.availableLanguages() return Localize.availableLanguages()
} }
...@@ -37,12 +40,15 @@ class SettingsDetailsViewModel: ViewModelProtocol { ...@@ -37,12 +40,15 @@ class SettingsDetailsViewModel: ViewModelProtocol {
public var selectedWindIndex: Int { public var selectedWindIndex: Int {
return winds.firstIndex{ $0 == Settings.shared.windSpeedType } ?? -1 return winds.firstIndex{ $0 == Settings.shared.windSpeedType } ?? -1
} }
public var selectedPressureIndex:Int { public var selectedPressureIndex: Int {
return pressures.firstIndex{ $0 == Settings.shared.pressureType } ?? -1 return pressures.firstIndex{ $0 == Settings.shared.pressureType } ?? -1
} }
public var selectedDistanceIndex:Int { public var selectedDistanceIndex: Int {
return distances.firstIndex{ $0 == Settings.shared.distanceType } ?? -1 return distances.firstIndex{ $0 == Settings.shared.distanceType } ?? -1
} }
public var selectedRainfallIndex: Int {
return rainFalls.firstIndex{ $0 == Settings.shared.rainfallType} ?? -1
}
public var selectedLanguageIndex:Int { public var selectedLanguageIndex:Int {
return Localize.availableLanguages().firstIndex{ $0 == Localize.currentLanguage() } ?? -1 return Localize.availableLanguages().firstIndex{ $0 == Localize.currentLanguage() } ?? -1
} }
...@@ -66,6 +72,8 @@ class SettingsDetailsViewModel: ViewModelProtocol { ...@@ -66,6 +72,8 @@ class SettingsDetailsViewModel: ViewModelProtocol {
Settings.shared.pressureType = pressures[index] Settings.shared.pressureType = pressures[index]
case .distance: case .distance:
Settings.shared.distanceType = distances[index] Settings.shared.distanceType = distances[index]
case .rainfall:
Settings.shared.rainfallType = rainFalls[index]
default: default:
break break
} }
......
...@@ -139,7 +139,7 @@ public class BlendMinutelySource: MinutelyForecastSource { ...@@ -139,7 +139,7 @@ public class BlendMinutelySource: MinutelyForecastSource {
private func covnertToAppModel(itemToConvert: BlendMinutelyForecast) -> MinutelyForecast { private func covnertToAppModel(itemToConvert: BlendMinutelyForecast) -> MinutelyForecast {
let items = itemToConvert.forecast.map { MinutelyItem(time: $0.time, let items = itemToConvert.forecast.map { MinutelyItem(time: $0.time,
temp: .init(value: Double($0.temp), unit: .fahrenheit), temp: .init(value: Double($0.temp), unit: .fahrenheit),
precipitation: $0.precipitation, precipitation: .init(value: $0.precipitation, unit: .inchesPerHour),
windSpeed: .init(value: Double($0.windSpeed), unit: .milesPerHour), windSpeed: .init(value: Double($0.windSpeed), unit: .milesPerHour),
pressure: .init(value: Double($0.pressure), unit: .inchesOfMercury)) } pressure: .init(value: Double($0.pressure), unit: .inchesOfMercury)) }
let minutelyForecast = MinutelyForecast(lastUpdateTime: Date(), let minutelyForecast = MinutelyForecast(lastUpdateTime: Date(),
......
...@@ -108,7 +108,7 @@ ...@@ -108,7 +108,7 @@
<relationship name="location" maxCount="1" deletionRule="Nullify" destinationEntity="CoreLocation" inverseName="minutely" inverseEntity="CoreLocation"/> <relationship name="location" maxCount="1" deletionRule="Nullify" destinationEntity="CoreLocation" inverseName="minutely" inverseEntity="CoreLocation"/>
</entity> </entity>
<entity name="CoreMinutelyItem" representedClassName="CoreMinutelyItem" syncable="YES"> <entity name="CoreMinutelyItem" representedClassName="CoreMinutelyItem" syncable="YES">
<attribute name="precipitation" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/> <attribute name="precipitation" attributeType="Binary"/>
<attribute name="pressure" attributeType="Binary"/> <attribute name="pressure" attributeType="Binary"/>
<attribute name="temp" attributeType="Binary"/> <attribute name="temp" attributeType="Binary"/>
<attribute name="time" attributeType="Date" usesScalarValueType="NO"/> <attribute name="time" attributeType="Date" usesScalarValueType="NO"/>
...@@ -146,10 +146,10 @@ ...@@ -146,10 +146,10 @@
<element name="CoreHealth" positionX="277.1640625" positionY="-156.77734375" width="128" height="103"/> <element name="CoreHealth" positionX="277.1640625" positionY="-156.77734375" width="128" height="103"/>
<element name="CoreHourlyWeather" positionX="279.15234375" positionY="-29.234375" width="128" height="253"/> <element name="CoreHourlyWeather" positionX="279.15234375" positionY="-29.234375" width="128" height="253"/>
<element name="CoreLocation" positionX="113.6640625" positionY="-337.08984375" width="128" height="329"/> <element name="CoreLocation" positionX="113.6640625" positionY="-337.08984375" width="128" height="329"/>
<element name="CoreMinutelyForecast" positionX="-90" positionY="-324" width="128" height="74"/>
<element name="CoreMinutelyItem" positionX="-81" positionY="-315" width="128" height="119"/>
<element name="CoreNotifications" positionX="-153.28125" positionY="-336.6484375" width="128" height="88"/> <element name="CoreNotifications" positionX="-153.28125" positionY="-336.6484375" width="128" height="88"/>
<element name="CoreNWSAlert" positionX="-315.77734375" positionY="-337.84375" width="128" height="208"/> <element name="CoreNWSAlert" positionX="-315.77734375" positionY="-337.84375" width="128" height="208"/>
<element name="CorePollutant" positionX="438.171875" positionY="-103.41015625" width="128" height="88"/> <element name="CorePollutant" positionX="438.171875" positionY="-103.41015625" width="128" height="88"/>
<element name="CoreMinutelyForecast" positionX="-90" positionY="-324" width="128" height="74"/>
<element name="CoreMinutelyItem" positionX="-81" positionY="-315" width="128" height="119"/>
</elements> </elements>
</model> </model>
\ No newline at end of file
...@@ -14,6 +14,9 @@ open class CoreMinutelyItem: _CoreMinutelyItem, CoreDataAppModelConvertable { ...@@ -14,6 +14,9 @@ open class CoreMinutelyItem: _CoreMinutelyItem, CoreDataAppModelConvertable {
guard let pressure: Pressure = try CoreDataUtils.measurement(from: self.pressure, in: self, attributeName: "pressure") else { guard let pressure: Pressure = try CoreDataUtils.measurement(from: self.pressure, in: self, attributeName: "pressure") else {
throw CoreDataError.LoadAttributeError(entity: self, attributeName: "pressure", value: self.pressure, nestedError: nil) throw CoreDataError.LoadAttributeError(entity: self, attributeName: "pressure", value: self.pressure, nestedError: nil)
} }
guard let precipitation: Rainfall = try CoreDataUtils.measurement(from: self.precipitation, in: self, attributeName: "precipitation") else {
throw CoreDataError.LoadAttributeError(entity: self, attributeName: "precipitation", value: self.precipitation, nestedError: nil)
}
return MinutelyItem(time: time, temp: temp, precipitation: precipitation, windSpeed: windSpeed, pressure: pressure) return MinutelyItem(time: time, temp: temp, precipitation: precipitation, windSpeed: windSpeed, pressure: pressure)
} }
...@@ -30,7 +33,7 @@ open class CoreMinutelyItem: _CoreMinutelyItem, CoreDataAppModelConvertable { ...@@ -30,7 +33,7 @@ open class CoreMinutelyItem: _CoreMinutelyItem, CoreDataAppModelConvertable {
self.init(managedObjectContext: context) self.init(managedObjectContext: context)
self.time = appModel.time self.time = appModel.time
self.precipitation = appModel.precipitation self.precipitation = try CoreDataUtils.measurementToData(appModel.precipitation, in: self, attributeName: "precipitation")
self.temp = try CoreDataUtils.measurementToData(appModel.temp, in: self, attributeName: "temp") self.temp = try CoreDataUtils.measurementToData(appModel.temp, in: self, attributeName: "temp")
self.windSpeed = try CoreDataUtils.measurementToData(appModel.windSpeed, in: self, attributeName: "windSpeed") self.windSpeed = try CoreDataUtils.measurementToData(appModel.windSpeed, in: self, attributeName: "windSpeed")
......
...@@ -47,7 +47,7 @@ open class _CoreMinutelyItem: NSManagedObject { ...@@ -47,7 +47,7 @@ open class _CoreMinutelyItem: NSManagedObject {
// MARK: - Properties // MARK: - Properties
@NSManaged open @NSManaged open
var precipitation: Double var precipitation: Data!
@NSManaged open @NSManaged open
var pressure: Data! var pressure: Data!
......
...@@ -151,6 +151,7 @@ public enum AnalyticsEvent: String { ...@@ -151,6 +151,7 @@ public enum AnalyticsEvent: String {
// MARK: Minutely // MARK: Minutely
case ANALYTICS_MINUTELY_PRECIP_TODAY_SEEN = "MINUTELY_PRECIP_TODAY_SEEN" case ANALYTICS_MINUTELY_PRECIP_TODAY_SEEN = "MINUTELY_PRECIP_TODAY_SEEN"
case ANALYTICS_MINUTELY_PRECIP_TODAY_TAP = "MINUTELY_PRECIP_TODAY_TAP" case ANALYTICS_MINUTELY_PRECIP_TODAY_TAP = "MINUTELY_PRECIP_TODAY_TAP"
case ANALYTICS_MINUTELY_SWIPE = "MINUTELY_SWIPE"
/// When the user taps on the title of the minutely precipitation graph /// When the user taps on the title of the minutely precipitation graph
case ANALYTICS_MINUTELY_PRECIP_TODAY_TITLE = "MINUTELY_PRECIP_TODAY_TITLE" case ANALYTICS_MINUTELY_PRECIP_TODAY_TITLE = "MINUTELY_PRECIP_TODAY_TITLE"
case ANALYTICS_MINUTELY_TEMP_TODAY_SEEN = "MINUTELY_TEMP_TODAY_SEEN" case ANALYTICS_MINUTELY_TEMP_TODAY_SEEN = "MINUTELY_TEMP_TODAY_SEEN"
......
...@@ -14,7 +14,18 @@ public extension Dimension { ...@@ -14,7 +14,18 @@ public extension Dimension {
return fmt return fmt
} }
var name:String { var name: String {
if let unitRainfall = self as? UnitRainfall {
switch unitRainfall {
case .inchesPerHour:
return "inches per hour"
case .millimetersPerHour:
return "millimeters per hour"
default:
return Dimension.fmt.string(from: self)
}
}
return Dimension.fmt.string(from: self) return Dimension.fmt.string(from: self)
} }
} }
......
...@@ -21,6 +21,15 @@ public extension Measurement { ...@@ -21,6 +21,15 @@ public extension Measurement {
return fmt return fmt
} }
private static func fractionFormatter(style: Formatter.UnitStyle, unitOptions: MeasurementFormatter.UnitOptions = .providedUnit) -> MeasurementFormatter {
let fmt = MeasurementFormatter.oneWeatherSharedFormatter
fmt.locale = Settings.shared.locale
fmt.unitStyle = style
fmt.unitOptions = unitOptions
fmt.numberFormatter.maximumFractionDigits = 2
return fmt
}
var string:String { var string:String {
return Measurement.formatter(style: .medium).string(from: self) return Measurement.formatter(style: .medium).string(from: self)
} }
...@@ -78,3 +87,21 @@ public extension Pressure { ...@@ -78,3 +87,21 @@ public extension Pressure {
} }
} }
public extension Rainfall {
var settingsConvertedValue: Double {
return self.converted(to: Settings.shared.rainfallType).value
}
var settingsConverted: Measurement {
return self.converted(to: Settings.shared.rainfallType)
}
var string:String {
return Measurement.fractionFormatter(style: .medium).string(from: self)
}
var shortString: String {
return String(format: "%.2f", self.value)
}
}
...@@ -343,6 +343,16 @@ public enum MoonPhase: String, Codable { ...@@ -343,6 +343,16 @@ public enum MoonPhase: String, Codable {
} }
} }
public final class UnitRainfall: Dimension {
public static let inchesPerHour = UnitRainfall(symbol: "in/h", converter: UnitConverterLinear(coefficient: 1.0))
public static let millimetersPerHour = UnitRainfall(symbol: "mm/h", converter: UnitConverterLinear(coefficient: 0.03937))
public override class func baseUnit() -> UnitRainfall {
return inchesPerHour
}
}
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
...@@ -433,6 +443,7 @@ public typealias WindSpeed = Measurement<UnitSpeed> ...@@ -433,6 +443,7 @@ public typealias WindSpeed = Measurement<UnitSpeed>
public typealias Percent = UInt public typealias Percent = UInt
public typealias Visibility = Measurement<UnitLength> public typealias Visibility = Measurement<UnitLength>
public typealias Pressure = Measurement<UnitPressure> public typealias Pressure = Measurement<UnitPressure>
public typealias Rainfall = Measurement<UnitRainfall>
extension Measurement { extension Measurement {
public init?(valueString: String?, unit: UnitType) { public init?(valueString: String?, unit: UnitType) {
......
...@@ -11,12 +11,12 @@ import UIKit ...@@ -11,12 +11,12 @@ import UIKit
public struct MinutelyItem { public struct MinutelyItem {
public let time: Date public let time: Date
public let temp: Temperature public let temp: Temperature
public let precipitation: Double public let precipitation: Rainfall
public let windSpeed: WindSpeed public let windSpeed: WindSpeed
public let pressure: Pressure public let pressure: Pressure
public var weatherTypeImage: UIImage? public var weatherTypeImage: UIImage?
public init(time: Date, temp: Temperature, precipitation: Double, windSpeed: WindSpeed, pressure: Pressure) { public init(time: Date, temp: Temperature, precipitation: Rainfall, windSpeed: WindSpeed, pressure: Pressure) {
self.time = time self.time = time
self.temp = temp self.temp = temp
self.precipitation = precipitation self.precipitation = precipitation
......
...@@ -12,6 +12,7 @@ public protocol DefaultSettings { ...@@ -12,6 +12,7 @@ public protocol DefaultSettings {
var windSpeedType: UnitSpeed { get } var windSpeedType: UnitSpeed { get }
var pressureType: UnitPressure { get } var pressureType: UnitPressure { get }
var distanceType: UnitLength { get } var distanceType: UnitLength { get }
var rainfallType: UnitRainfall { get }
var pinnedLayerIds: [String] { get } var pinnedLayerIds: [String] { get }
var selectedLayerId: String { get } var selectedLayerId: String { get }
} }
...@@ -12,6 +12,7 @@ struct DefaultSettingsImperial: DefaultSettings { ...@@ -12,6 +12,7 @@ struct DefaultSettingsImperial: DefaultSettings {
let windSpeedType: UnitSpeed = .milesPerHour let windSpeedType: UnitSpeed = .milesPerHour
let pressureType: UnitPressure = .inchesOfMercury let pressureType: UnitPressure = .inchesOfMercury
let distanceType: UnitLength = .miles let distanceType: UnitLength = .miles
let rainfallType: UnitRainfall = .inchesPerHour
let pinnedLayerIds: [String] = [] let pinnedLayerIds: [String] = []
let selectedLayerId: String = WeatherLayerType.surfaceTemp.rawValue let selectedLayerId: String = WeatherLayerType.surfaceTemp.rawValue
} }
...@@ -12,6 +12,7 @@ struct DefaultSettingsMetric: DefaultSettings { ...@@ -12,6 +12,7 @@ struct DefaultSettingsMetric: DefaultSettings {
let windSpeedType: UnitSpeed = .metersPerSecond let windSpeedType: UnitSpeed = .metersPerSecond
let pressureType: UnitPressure = .millimetersOfMercury let pressureType: UnitPressure = .millimetersOfMercury
let distanceType: UnitLength = .kilometers let distanceType: UnitLength = .kilometers
let rainfallType: UnitRainfall = .millimetersPerHour
let pinnedLayerIds: [String] = [] let pinnedLayerIds: [String] = []
let selectedLayerId: String = WeatherLayerType.surfaceTemp.rawValue let selectedLayerId: String = WeatherLayerType.surfaceTemp.rawValue
} }
...@@ -67,6 +67,9 @@ public class Settings { ...@@ -67,6 +67,9 @@ public class Settings {
@UserDefaultsUnitValue(key: "distance_type", userDefaults: UserDefaults.appDefaults) @UserDefaultsUnitValue(key: "distance_type", userDefaults: UserDefaults.appDefaults)
public var distanceType = DefaultSettingsFactory().getSettings().distanceType public var distanceType = DefaultSettingsFactory().getSettings().distanceType
@UserDefaultsUnitValue(key: "rainfall_type", userDefaults: UserDefaults.appDefaults)
public var rainfallType = DefaultSettingsFactory().getSettings().rainfallType
public var locale:Locale { public var locale:Locale {
return Locale(identifier: Localize.currentLanguage()) return Locale(identifier: Localize.currentLanguage())
} }
......
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