Commit 04edf1ff by Dmitriy Stepanets

Merge branch 'feature/air-quality'

# Conflicts:
#	1Weather.xcworkspace/xcuserdata/dstepanets.xcuserdatad/UserInterfaceState.xcuserstate
parents 5ddfebfc ad821a19
......@@ -32,4 +32,25 @@ extension UIColor {
}
self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(255 * alpha) / 255)
}
public func toHex(alpha: Bool = false) -> String? {
guard let components = cgColor.components, components.count >= 3 else {
return nil
}
let r = Float(components[0])
let g = Float(components[1])
let b = Float(components[2])
var a = Float(1.0)
if components.count >= 4 {
a = Float(components[3])
}
if alpha {
return String(format: "%02lX%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255), lroundf(a * 255))
} else {
return String(format: "%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255))
}
}
}
//
// Health.swift
// 1Weather
//
// Created by Demid Merzlyakov on 01.04.2021.
//
import Foundation
import UIKit
public struct Health: Equatable, Hashable {
public let lastUpdateTime: Date
public let airQuality: AirQuality?
public let pollutants: [String: Pollutant]
}
public struct AirQuality: Equatable, Hashable {
public let index: Double
public let advice: String //TODO: support for localization
public var status: HealthStatus {
get {
HealthStatus(value: index)
}
}
}
public enum HealthStatus: String {
case good = "health.airquality.status.good"
case moderate = "health.airquality.status.moderate"
case unhealthyForSensitiveGroups = "health.airquality.status.unhealthyForSensitiveGroups"
case unhealthy = "health.airquality.status.unhealthy"
case veryUnhealthy = "health.airquality.status.veryUnhealthy"
case hazardous = "health.airquality.status.hazardous"
public init(value: Double) {
switch value {
case 0...50:
self = .good
case 51...100:
self = .moderate
case 101...150:
self = .unhealthyForSensitiveGroups
case 151...200:
self = .unhealthy
case 201...300:
self = .veryUnhealthy
default:
self = .hazardous
}
}
public var localized: String {
return self.rawValue.localized()
}
public var gradientColorStart: UIColor {
get {
#warning("Not implemented!")
//TODO: Implement!
return UIColor.green
}
}
public var gradientColorEnd: UIColor {
get {
#warning("Not implemented!")
//TODO: Implement!
return UIColor.red
}
}
}
public struct Pollutant: Equatable, Hashable {
public let name: String
public let value: Double
public var status: HealthStatus {
get {
HealthStatus(value: value)
}
}
}
......@@ -59,6 +59,7 @@ public struct Location {
}
}
public private (set) var dayTimeForecast: [DayTimeWeather] = [DayTimeWeather]()
public var health: Health?
// MARK: - Derived fields
public var cityId: String {
......@@ -118,6 +119,7 @@ extension Location: UpdatableModelObject {
result.today = result.today?.mergedWith(incrementalChanges: incrementalChanges.today) ?? incrementalChanges.today
result.daily = result.daily.mergedWith(incrementalChanges: incrementalChanges.daily)
result.hourly = result.hourly.mergedWith(incrementalChanges: incrementalChanges.hourly)
result.health = incrementalChanges.health ?? result.health
return result
}
}
......
//
// BlendHealthSource.swift
// 1Weather
//
// Created by Demid Merzlyakov on 01.04.2021.
//
import Foundation
public enum BlendHealthError: Error {
case insufficientLocationInfo
case badUrl
case networkError(Error?)
case badServerResponse(Error?)
case dataEncodingError(String)
case alreadyBeingUpdated
}
public class BlendHealthSource {
}
//
// HealthSource.swift
// 1Weather
//
// Created by Demid Merzlyakov on 01.04.2021.
//
import Foundation
public typealias HealthSourceCompletion = (Health?, Error?) -> ()
public protocol HealthSource {
var healthUpdateInterval: TimeInterval { get }
func updateHelath(for location: Location, completion: @escaping HealthSourceCompletion)
}
//
// BlendHealthCenterModels.swift
// 1Weather
//
// Created by Demid Merzlyakov on 01.04.2021.
//
import Foundation
import UIKit
// MARK: - HealthCenter
struct BlendHealthCenter: Codable {
public let s2CellID: String
public let updatedOn: Date
public let airQuality: BlendAirQuality
public let fire: BlendFire
public let pollutants, pollen: [BlendPoll]
enum CodingKeys: String, CodingKey {
case s2CellID = "s2_cell_id"
case updatedOn = "updated_on"
case airQuality = "air_quality"
case fire, pollutants, pollen
}
}
// MARK: - AirQuality
struct BlendAirQuality: Codable {
public let aqi: Double
public let airQualityDescription: String
public let healthAdvice: BlendHealthAdvice
public let color: BlendColor
public let imageURL: String
enum CodingKeys: String, CodingKey {
case aqi
case airQualityDescription = "description"
case healthAdvice = "health_advice"
case color = "color_code"
case imageURL = "image_url"
}
}
// MARK: - HealthAdvice
struct BlendHealthAdvice: Codable {
public let general, sensitive, active: String
}
// MARK: - Fire
struct BlendFire: Codable {
public let windSpeed: Double
public let windDirection, siUnit, fireDescription: String
enum CodingKeys: String, CodingKey {
case windSpeed = "wind_speed"
case windDirection = "wind_direction"
case siUnit = "si_unit"
case fireDescription = "description"
}
}
// MARK: - Poll
struct BlendPoll: Codable {
public let name: String
public let value: Double?
public let siUnit: String
public let status, colorCode: String?
enum CodingKeys: String, CodingKey {
case name, value
case siUnit = "si_unit"
case status
case colorCode = "color_code"
}
}
struct BlendColor: Codable {
public let uiColor: UIColor
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let hexString = try container.decode(String.self)
self.uiColor = UIColor(hexString: hexString)
}
func encode(to encoder: Encoder) throws {
guard let hexRepresentation = uiColor.toHex() else {
throw BlendHealthError.dataEncodingError("BlendColor to HEX conversion failed for color \(uiColor)")
}
var container = encoder.singleValueContainer()
try container.encode("#\(hexRepresentation)")
}
}
......@@ -141,3 +141,11 @@
"settings.language" = "Language";
"settings.manageNotifications" = "Manage Notifications";
"settings.locationAccess" = "Locations Access";
// Health
"health.airquality.status.good" = "Good";
"health.airquality.status.moderate" = "Moderate";
"health.airquality.status.unhealthyForSensitiveGroups" = "Unhealthy For Sensitive Groups";
"health.airquality.status.unhealthy" = "Unhealthy";
"health.airquality.status.veryUnhealthy" = "Very Unhealthy";
"health.airquality.status.hazardous" = "Hazardous";
//
// PollutionView.swift
// 1Weather
//
// Created by Dmitry Stepanets on 01.04.2021.
//
import UIKit
class PollutionView: UIView {
//Private
private let typeLabel = UILabel()
private let valueLabel = UILabel()
private let statusLabel = UILabel()
private let progressContainer = UIView()
private let progressGradient = CAGradientLayer()
init() {
super.init(frame: .zero)
prepareLabels()
prepareProgress()
}
func configure(pollutant: Pollutant) {
typeLabel.text = pollutant.name.localized
valueLabel.text = "\(pollutant.value)"
statusLabel.text = pollutant.status.localized
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//MRAK:- Prepare
private extension PollutionView {
func prepareLabels() {
typeLabel.font = AppFont.SFPro.bold(size: 18)
addSubview(typeLabel)
valueLabel.font = AppFont.SFPro.bold(size: 18)
addSubview(valueLabel)
statusLabel.textAlignment = .right
statusLabel.font = AppFont.SFPro.bold(size: 14)
addSubview(statusLabel)
//Constraints
typeLabel.snp.makeConstraints { (make) in
make.left.equalToSuperview()
make.top.equalToSuperview().inset(15)
}
valueLabel.snp.makeConstraints { (make) in
make.left.equalTo(typeLabel.snp.right).offset(30)
make.centerY.equalTo(typeLabel)
}
statusLabel.snp.makeConstraints { (make) in
make.right.equalToSuperview()
make.centerY.equalTo(typeLabel)
}
}
func prepareProgress() {
progressContainer.layer.cornerRadius = 2
progressContainer.backgroundColor = UIColor(hex: 0xe9ebfc)
addSubview(progressContainer)
progressContainer.snp.makeConstraints { (make) in
make.height.equalTo(4)
make.left.right.equalToSuperview()
make.top.equalTo(typeLabel.snp.bottom).offset(18)
make.bottom.equalToSuperview().inset(15)
}
}
}
//
// PollutantView.swift
// 1Weather
//
// Created by Dmitry Stepanets on 01.04.2021.
//
import UIKit
class PollutantView: UIView {
//Private
private let typeLabel = UILabel()
private let valueLabel = UILabel()
private let statusLabel = UILabel()
private let progressContainer = UIView()
private let progressGradient = CAGradientLayer()
init() {
super.init(frame: .zero)
prepareLabels()
prepareProgress()
updateUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateUI()
}
func configure(pollutant: Pollutant) {
typeLabel.text = pollutant.name.localized
valueLabel.text = "\(Int(pollutant.value))"
statusLabel.text = pollutant.status.localized
}
private func updateUI() {
switch interfaceStyle {
case .light:
typeLabel.textColor = ThemeManager.currentTheme.secondaryTextColor
valueLabel.textColor = ThemeManager.currentTheme.secondaryTextColor
statusLabel.textColor = ThemeManager.currentTheme.secondaryTextColor
case .dark:
typeLabel.textColor = ThemeManager.currentTheme.primaryTextColor
valueLabel.textColor = ThemeManager.currentTheme.primaryTextColor
statusLabel.textColor = ThemeManager.currentTheme.primaryTextColor
}
}
}
//MRAK:- Prepare
private extension PollutantView {
func prepareLabels() {
typeLabel.font = AppFont.SFPro.bold(size: 18)
typeLabel.setContentHuggingPriority(.fittingSizeLevel, for: .vertical)
addSubview(typeLabel)
valueLabel.font = AppFont.SFPro.bold(size: 18)
addSubview(valueLabel)
statusLabel.lineBreakMode = .byWordWrapping
statusLabel.numberOfLines = 2
statusLabel.textAlignment = .right
statusLabel.font = AppFont.SFPro.bold(size: 14)
addSubview(statusLabel)
//Constraints
typeLabel.snp.makeConstraints { (make) in
make.left.equalToSuperview()
make.top.equalToSuperview().inset(15)
}
valueLabel.snp.makeConstraints { (make) in
make.left.equalToSuperview().inset(100)
make.centerY.equalTo(typeLabel)
}
}
func prepareProgress() {
progressContainer.layer.cornerRadius = 2
progressContainer.backgroundColor = UIColor(hex: 0xe9ebfc)
addSubview(progressContainer)
progressContainer.snp.makeConstraints { (make) in
make.height.equalTo(4)
make.left.right.equalToSuperview()
make.top.equalTo(typeLabel.snp.bottom).offset(18)
make.bottom.equalToSuperview().inset(15)
}
statusLabel.snp.makeConstraints { (make) in
make.right.equalToSuperview()
make.centerY.equalTo(typeLabel)
make.width.equalTo(progressContainer).multipliedBy(0.4)
}
}
}
......@@ -11,7 +11,6 @@ class TodayAirQualityCell: UITableViewCell {
//Private
private let headingLabel = UILabel()
private let valueCircle = CAShapeLayer()
private let valueProgressGradient = CAGradientLayer()
private let airQualityValueLabel = UILabel()
private let airQualityLabel = UILabel()
private let airDescLabel = UILabel()
......@@ -24,8 +23,7 @@ class TodayAirQualityCell: UITableViewCell {
prepareHeading()
prepareAirLabels()
prepareValueProgress()
setAirQuality(value: 48)
prepareStackView()
}
override func layoutSubviews() {
......@@ -36,33 +34,16 @@ class TodayAirQualityCell: UITableViewCell {
startAngle: 0,
endAngle: 2 * .pi,
clockwise: false).cgPath
let mask = CAShapeLayer()
mask.lineWidth = 6
mask.lineCap = .round
mask.strokeColor = UIColor.red.cgColor
mask.fillColor = UIColor.clear.cgColor
mask.path = UIBezierPath(arcCenter: airQualityValueLabel.center,
radius: 36,
startAngle: -.pi/2,
endAngle: 0,
clockwise: true).cgPath
valueProgressGradient.frame = .init(x: 0,
y: 0,
width: 100,
height: 100)
// valueProgressGradient.mask = valueProgressShape
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setAirQuality(value:CGFloat) {
airQualityValueLabel.text = "\(Int(value))"
public func configure(health: Health) {
airQualityValueLabel.text = "\(Int(health.airQuality?.index ?? 0))"
let aqiText = "air.quality.is".localized()
let aqiConditionText = "air.quality.good".localized().uppercased()
let aqiConditionText = health.airQuality?.status.localized ?? ""
let attrString = NSMutableAttributedString(string: "\(aqiText)\n\(aqiConditionText)",
attributes: [.font : AppFont.SFPro.regular(size: 24),
.foregroundColor :ThemeManager.currentTheme.secondaryTextColor])
......@@ -70,6 +51,15 @@ class TodayAirQualityCell: UITableViewCell {
range: NSRange(location: aqiText.count + 1,
length: aqiConditionText.count))
airQualityLabel.attributedText = attrString
//Fill pollutions
stackView.removeAll()
health.pollutants.map{$1}.forEach {
let pollutionView = PollutantView()
pollutionView.configure(pollutant: $0)
stackView.addArrangedSubview(pollutionView)
}
stackView.layoutIfNeeded()
}
}
......@@ -93,6 +83,8 @@ private extension TodayAirQualityCell {
}
func prepareAirLabels() {
airQualityLabel.lineBreakMode = .byWordWrapping
airQualityLabel.numberOfLines = 0
airQualityValueLabel.font = AppFont.SFPro.bold(size: 18)
airQualityValueLabel.textColor = ThemeManager.currentTheme.secondaryTextColor
contentView.addSubview(airQualityValueLabel)
......@@ -113,7 +105,7 @@ private extension TodayAirQualityCell {
//Constraints
airQualityValueLabel.snp.makeConstraints { (make) in
make.left.equalToSuperview().inset(72)
make.top.equalTo(headingLabel.snp.bottom).offset(72)
make.top.equalTo(headingLabel.snp.bottom).offset(72).priority(999)
}
airQualityLabel.snp.makeConstraints { (make) in
......@@ -124,7 +116,6 @@ private extension TodayAirQualityCell {
airDescLabel.snp.makeConstraints { (make) in
make.left.right.equalToSuperview().inset(50)
make.top.equalTo(airQualityLabel.snp.bottom).offset(9)
make.bottom.equalToSuperview().inset(30)
}
}
......@@ -133,11 +124,18 @@ private extension TodayAirQualityCell {
valueCircle.fillColor = UIColor.clear.cgColor
valueCircle.lineWidth = 2
contentView.layer.addSublayer(valueCircle)
}
func prepareStackView() {
stackView.axis = .vertical
stackView.distribution = .fillProportionally
stackView.clipsToBounds = false
contentView.addSubview(stackView)
valueProgressGradient.type = .radial
valueProgressGradient.startPoint = .init(x: 0.5, y: 0.5)
valueProgressGradient.endPoint = .init(x: 0, y: 0)
valueProgressGradient.colors = [UIColor.red.cgColor, UIColor.yellow.cgColor]
contentView.layer.addSublayer(valueProgressGradient)
stackView.snp.makeConstraints { (make) in
make.left.right.equalToSuperview().inset(50)
make.top.equalTo(airDescLabel.snp.bottom).offset(15)
make.bottom.equalToSuperview().inset(15)
}
}
}
......@@ -14,6 +14,7 @@ private enum TodayCellType:Int {
case conditions
case forecastPeriod
case precipitation
case airQuality
case dayTime
case sun
case moon
......@@ -38,7 +39,13 @@ class TodayCellFactory: CellFactoryProtocol {
private let todayViewModel:TodayViewModel
private var todaySection = TodaySection(rows: [.alert, .forecast, .ad,
.conditions, .forecastPeriod, .precipitation,
.dayTime, .sun, .moon])
.airQuality, .dayTime, .sun, .moon])
private let health = Health(lastUpdateTime: Date(),
airQuality: .init(index: 48, advice: "some"),
pollutants: ["pm25" : .init(name: "PM 2.5", value: 48),
"pm10" : .init(name: "PM 10", value: 42),
"no2" : .init(name: "NO2", value: 74),
"so2" : .init(name: "SO2", value: 135)])
//Public
init(viewModel: TodayViewModel) {
......@@ -103,6 +110,10 @@ class TodayCellFactory: CellFactoryProtocol {
cellsToUpdate.remove(.precipitation)
}
return cell
case .airQuality:
let cell = dequeueReusableCell(type: TodayAirQualityCell.self, tableView: tableView, indexPath: indexPath)
cell.configure(health: self.health)
return cell
case .dayTime:
let cell = dequeueReusableCell(type: TodayDayTimesCell.self, tableView: tableView, indexPath: indexPath)
if cellsToUpdate.contains(.dayTime) {
......
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