Commit 31b6a632 by Dmitry Stepanets

[IOS-172]: Finished minutely UI & user iteractions. Merged with release 5.4

parent 40406fb5
......@@ -31,7 +31,7 @@
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Release"
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
......@@ -50,6 +50,9 @@
ReferencedContainer = "container:1Weather.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<StoreKitConfigurationFileReference
identifier = "../1Weather/InApps/Configuration.storekit">
</StoreKitConfigurationFileReference>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
......
......@@ -17,6 +17,9 @@
location = "group:BlendHealthSource/BlendHealthSource.xcodeproj">
</FileRef>
<FileRef
location = "group:BlendMinutelySource/BlendMinutelySource.xcodeproj">
</FileRef>
<FileRef
location = "group:CoreDataStorage/CoreDataStorage.xcodeproj">
</FileRef>
<FileRef
......
......@@ -21,6 +21,7 @@ import OneWeatherUI
import WDTWeatherSource
import BlendHealthSource
import BlendMinutelySource
import BlendFIPSSource
import CoreDataStorage
import DelayedSaveStorage
......@@ -65,6 +66,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
//TODO: introduce dependency management (dependency injection engine coupled with a factory or something of the sort).
LocationManager.shared = LocationManager(weatherUpdateSource: WdtWeatherSource(),
healthSource: BlendHealthSource(),
minutelyForecastSource: BlendMinutelySource(),
nwsAlertsManager: NWSAlertsManager(),
fipsSource: BlendFIPSSource(),
pushNotificationsManager: PushNotificationsManager.shared,
......
//
// Array+Unique.swift
// 1Weather
//
// Created by Dmitry Stepanets on 27.08.2021.
//
import Foundation
extension Sequence where Iterator.Element: Hashable {
func unique() -> [Iterator.Element] {
var seen: Set<Iterator.Element> = []
return filter { seen.insert($0).inserted }
}
}
......@@ -10,6 +10,7 @@ import UIKit
enum TimePeriod: Int {
case daily = 0
case hourly = 1
case minutely = 2
}
class ForecastTimePeriodControl: UISegmentedControl {
......@@ -29,6 +30,27 @@ class ForecastTimePeriodControl: UISegmentedControl {
updateUI()
}
public func set(items: [String]?) {
defer {
updateUI()
layoutSubviews()
}
guard
let itemsToAdd = items,
!itemsToAdd.isEmpty
else {
self.removeAllSegments()
return
}
self.removeAllSegments()
for (index, item) in itemsToAdd.enumerated() {
self.insertSegment(withTitle: item, at: index, animated: false)
}
selectedSegmentIndex = 0
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
......
//
// MinutelyForecastCell.swift
// 1Weather
//
// Created by Dmitry Stepanets on 25.08.2021.
//
import UIKit
class MinutelyForecastCell: UITableViewCell {
private let horizontalStackView = UIStackView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
......@@ -12,8 +12,11 @@ class PrecipitationCell: UITableViewCell {
//Private
private let headingLabel = UILabel()
private let headingButton = ArrowButton()
private let minutelyForecastView = MinutelyForecastView()
private let featureAvailability = FeatureAvailabilityManager.shared
private let scrollView = UIScrollView()
private let stackView = UIStackView()
private let periodSegmentedControl = ForecastTimePeriodControl(items: nil)
private let descriptionView = ForecastDescriptionView(lightStyleBackgroundColor: UIColor(hex: 0xd9ebfe),
gradientColors: [UIColor(hex: 0x44a4ff).withAlphaComponent(0.65).cgColor,
UIColor(hex: 0x73bbff).withAlphaComponent(0).cgColor])
......@@ -23,6 +26,8 @@ class PrecipitationCell: UITableViewCell {
prepareCell()
prepareHeading()
prepareSegmentControl()
prepareMinutelyForecastView()
prepareScrollView()
prepareStackView()
prepareSummaryView()
......@@ -32,9 +37,28 @@ class PrecipitationCell: UITableViewCell {
fatalError("init(coder:) has not been implemented")
}
public func configure(with dayily:[DailyWeather]) {
public func configure(with dayily:[DailyWeather], location: Location) {
//TODO: Hide button for now
headingButton.isHidden = true
minutelyForecastView.configure(with: location, forecastType: .precipitation)
//Update segment control
if featureAvailability?.isAvailable(feature: .minutelyForecast) == true {
periodSegmentedControl.set(items: ["forecast.timePeriod.daily".localized,
"forecast.timePeriod.minutely".localized()])
periodSegmentedControl.isHidden = false
periodSegmentedControl.snp.updateConstraints { update in
update.height.equalTo(40)
}
}
else {
periodSegmentedControl.set(items: ["forecast.timePeriod.daily".localized])
periodSegmentedControl.isHidden = true
periodSegmentedControl.snp.updateConstraints { update in
update.height.equalTo(0)
}
}
if stackView.arrangedSubviews.count != dayily.count {
let diff = stackView.arrangedSubviews.count - dayily.count
......@@ -67,14 +91,35 @@ class PrecipitationCell: UITableViewCell {
precipButton.configure(with: dayily[index])
}
self.handleSegmentDidChange()
}
public func configure(with hourly:[HourlyWeather]) {
public func configure(with hourly:[HourlyWeather], location: Location) {
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
minutelyForecastView.configure(with: location, forecastType: .precipitation)
//Update segment control
if featureAvailability?.isAvailable(feature: .minutelyForecast) == true {
periodSegmentedControl.set(items: ["forecast.timePeriod.hourly".localized,
"forecast.timePeriod.minutely".localized()])
periodSegmentedControl.isHidden = false
periodSegmentedControl.snp.updateConstraints { update in
update.height.equalTo(40)
}
}
else {
periodSegmentedControl.set(items: ["forecast.timePeriod.hourly".localized])
periodSegmentedControl.isHidden = true
periodSegmentedControl.snp.updateConstraints { update in
update.height.equalTo(0)
}
}
if stackView.arrangedSubviews.count != hourly.count {
let diff = stackView.arrangedSubviews.count - hourly.count
for _ in 0..<abs(diff) {
......@@ -106,6 +151,8 @@ class PrecipitationCell: UITableViewCell {
precipButton.configure(with: hourly[index])
}
handleSegmentDidChange()
}
//Private
......@@ -120,6 +167,17 @@ class PrecipitationCell: UITableViewCell {
}
}
}
@objc private func handleSegmentDidChange() {
if self.periodSegmentedControl.selectedSegmentIndex == 0 {
scrollView.isHidden = false
minutelyForecastView.isHidden = true
}
else {
scrollView.isHidden = true
minutelyForecastView.isHidden = false
}
}
}
//MARK:- Prepare
......@@ -152,6 +210,18 @@ private extension PrecipitationCell {
}
}
func prepareSegmentControl() {
periodSegmentedControl.selectedSegmentIndex = 0
periodSegmentedControl.addTarget(self, action: #selector(handleSegmentDidChange), for: .valueChanged)
contentView.addSubview(periodSegmentedControl)
periodSegmentedControl.snp.makeConstraints { (make) in
make.top.equalTo(headingLabel.snp.bottom).offset(18)
make.left.right.equalToSuperview().inset(16)
make.height.equalTo(40)
}
}
func prepareScrollView() {
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
......@@ -161,7 +231,19 @@ private extension PrecipitationCell {
scrollView.snp.makeConstraints { (make) in
make.left.equalToSuperview()
make.right.equalToSuperview()
make.top.equalTo(headingLabel.snp.bottom).offset(18)
make.top.equalTo(periodSegmentedControl.snp.bottom).offset(18)
make.height.equalTo(240)
}
}
func prepareMinutelyForecastView() {
minutelyForecastView.isHidden = true
contentView.addSubview(minutelyForecastView)
minutelyForecastView.snp.makeConstraints { (make) in
make.left.equalToSuperview()
make.right.equalToSuperview()
make.top.equalTo(periodSegmentedControl.snp.bottom).offset(40).priority(.medium)
make.height.equalTo(240)
}
}
......
//
// MinutelyForecastDetailsView.swift
// 1Weather
//
// Created by Dmitry Stepanets on 27.08.2021.
//
import UIKit
import OneWeatherCore
class MinutelyForecastDetailsView: UIView {
//Private
private let container = UIView()
private let gradient = CAGradientLayer()
private let tempLabel = UILabel()
private let timeLabel = UILabel()
private let forecastImage = UIImageView()
private let triangle = CAShapeLayer()
private let triangleView = UIView()
private let formatter: DateFormatter
init() {
self.formatter = DateFormatter()
self.formatter.dateFormat = "h:mm a"
super.init(frame: .zero)
prepareTriangle()
prepareView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
//Gradient
gradient.frame = container.bounds
//Triangle
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: triangleView.bounds.width / 2, y: triangleView.bounds.height))
path.addLine(to: CGPoint(x: triangleView.bounds.width, y: 0))
path.addLine(to: CGPoint(x: 0, y: 0))
triangle.frame = triangleView.bounds
triangle.path = path
}
func configure(valueStirng: String, date: Date?, weatherImage: UIImage?, timeZone: TimeZone, colors: [UIColor]) {
gradient.colors = colors.map{ $0.cgColor }
triangle.fillColor = colors.last?.cgColor
formatter.timeZone = timeZone
if let forecastDate = date {
timeLabel.text = formatter.string(from: forecastDate)
}
else {
timeLabel.text = "--"
}
tempLabel.text = valueStirng
forecastImage.image = weatherImage
}
}
private extension MinutelyForecastDetailsView {
func prepareView() {
addSubview(container)
gradient.startPoint = .init(x: 0.5, y: 0)
gradient.endPoint = .init(x: 0.5, y: 1)
gradient.cornerRadius = 8
container.layer.addSublayer(gradient)
timeLabel.font = AppFont.SFPro.light(size: 12)
timeLabel.textColor = .white
container.addSubview(timeLabel)
tempLabel.font = AppFont.SFPro.bold(size: 24)
tempLabel.textColor = .white
container.addSubview(tempLabel)
forecastImage.contentMode = .scaleAspectFit
forecastImage.clipsToBounds = true
container.addSubview(forecastImage)
let separator = UIView()
separator.backgroundColor = .white.withAlphaComponent(0.5)
container.addSubview(separator)
//Constraints
container.snp.makeConstraints { make in
make.left.top.right.equalToSuperview()
make.bottom.equalTo(triangleView.snp.top)
}
timeLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().inset(12)
}
separator.snp.makeConstraints { make in
make.width.equalTo(1)
make.height.equalToSuperview().multipliedBy(0.65)
make.left.equalToSuperview().inset(70)
make.centerY.equalToSuperview()
}
forecastImage.snp.makeConstraints { make in
make.width.height.equalTo(28)
make.centerY.equalToSuperview()
make.right.equalToSuperview().inset(8)
}
tempLabel.snp.makeConstraints { make in
make.right.equalTo(forecastImage.snp.left).offset(-2)
make.centerY.equalToSuperview()
}
}
func prepareTriangle() {
triangleView.layer.addSublayer(triangle)
addSubview(triangleView)
triangleView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview()
make.width.equalTo(12)
make.height.equalTo(10)
}
}
}
......@@ -21,6 +21,10 @@ private enum ForecastCellType {
case precipitation
case wind
//Minutely
case minutely
case precipitationAdviced
// Shared
case adBanner
case adMREC
......@@ -156,7 +160,9 @@ class ForecastCellFactory: CellFactory {
let cell = dequeueReusableCell(type: PrecipitationCell.self, tableView: tableView, indexPath: indexPath)
if let hourly = forecastViewModel.location?.hourly {
if cellsToUpdate.contains(.precipitation) {
cell.configure(with: hourly)
if let location = forecastViewModel.location {
cell.configure(with: hourly, location: location)
}
cellsToUpdate.remove(.precipitation)
}
}
......@@ -170,6 +176,10 @@ class ForecastCellFactory: CellFactory {
}
}
return cell
case .minutely:
return UITableViewCell()
case .precipitationAdviced:
return UITableViewCell()
}
}
......@@ -179,6 +189,8 @@ class ForecastCellFactory: CellFactory {
return self.dailySection.rows[indexPath.row]
case .hourly:
return self.hourlySection.rows[indexPath.row]
default:
return .minutely
}
}
......@@ -256,6 +268,8 @@ class ForecastCellFactory: CellFactory {
adTypeString = "MREC"
adLoggingEmoji = "✅"
}
default:
break
}
adView.set(placementName: placementName, adType: adType)
......
......@@ -70,6 +70,8 @@ class ForecastViewController: UIViewController {
analytics(log: .ANALYTICS_VIEW_FORECAST_EXTENDED)
case .hourly:
analytics(log: .ANALYTICS_VIEW_FORECAST_HOURLY)
default:
break
}
}
......
......@@ -161,7 +161,9 @@ class TodayCellFactory: CellFactory {
case .precipitation:
let cell = dequeueReusableCell(type: PrecipitationCell.self, tableView: tableView, indexPath: indexPath)
if cellsToUpdate.contains(.precipitation) {
cell.configure(with: loc.daily)
if let location = todayViewModel.location {
cell.configure(with: loc.daily, location: location)
}
cellsToUpdate.remove(.precipitation)
}
return cell
......
......@@ -10,14 +10,18 @@ import OneWeatherCore
class TodayForecastTimePeriodCell: UITableViewCell {
//Private
private let periodSegmentedControl = ForecastTimePeriodControl(items: ["forecast.timePeriod.daily".localized(),
"forecast.timePeriod.hourly".localized()])
// private let periodSegmentedControl = ForecastTimePeriodControl(items: ["forecast.timePeriod.daily".localized(),
// "forecast.timePeriod.hourly".localized(),
// "forecast.timePeriod.minutely".localized()])
private let periodSegmentedControl = ForecastTimePeriodControl(items: nil)
private let forecastTimePeriodView = ForecastTimePeriodView()
private let minutelyForecastView = MinutelyForecastView()
private let descriptionView = ForecastDescriptionView(lightStyleBackgroundColor: UIColor(hex: 0xfaedda).withAlphaComponent(0.5),
gradientColors: [UIColor(hex: 0xe81e15).withAlphaComponent(0.33).cgColor,
UIColor(hex: 0xf71d11).withAlphaComponent(0).cgColor])
private var location:Location?
private var graphIsDrawn = false
private let featureAvailability = FeatureAvailabilityManager.shared
//MARK:- Cell life cycle
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
......@@ -26,6 +30,7 @@ class TodayForecastTimePeriodCell: UITableViewCell {
prepareCell()
prepareSegmentedControl()
prepareForecastTimePeriodView()
prepareMinutelyForecastView()
prepareDescriptionView()
}
......@@ -34,9 +39,22 @@ class TodayForecastTimePeriodCell: UITableViewCell {
}
//Public
public func configure(with location:Location) {
public func configure(with location: Location) {
self.location = location
//Update segment control
if featureAvailability?.isAvailable(feature: .minutelyForecast) == true {
self.periodSegmentedControl.set(items: ["forecast.timePeriod.daily".localized(),
"forecast.timePeriod.hourly".localized(),
"forecast.timePeriod.minutely".localized()])
}
else {
self.periodSegmentedControl.set(items: ["forecast.timePeriod.daily".localized(),
"forecast.timePeriod.hourly".localized()])
}
self.forecastTimePeriodView.set(daily: location.daily, hourly: location.hourly)
self.minutelyForecastView.configure(with: location, forecastType: .temperature)
self.handleSegmentDidChange()
}
......@@ -45,8 +63,16 @@ class TodayForecastTimePeriodCell: UITableViewCell {
return
}
let forecastType = timePeriod == .daily ? ForecastType.daily : ForecastType.hourly
self.forecastTimePeriodView.set(forecastType: forecastType, buttonType: ForecastPeriodButton.self)
switch timePeriod {
case .daily, .hourly:
forecastTimePeriodView.isHidden = false
minutelyForecastView.isHidden = true
let forecastType = timePeriod == .daily ? ForecastType.daily : ForecastType.hourly
self.forecastTimePeriodView.set(forecastType: forecastType, buttonType: ForecastPeriodButton.self)
case .minutely:
forecastTimePeriodView.isHidden = true
minutelyForecastView.isHidden = false
}
}
}
......@@ -78,6 +104,18 @@ private extension TodayForecastTimePeriodCell {
}
}
func prepareMinutelyForecastView() {
minutelyForecastView.isHidden = true
contentView.addSubview(minutelyForecastView)
minutelyForecastView.snp.makeConstraints { (make) in
make.left.equalToSuperview()
make.right.equalToSuperview()
make.top.equalTo(periodSegmentedControl.snp.bottom).offset(40).priority(.medium)
make.height.equalTo(240)
}
}
func prepareDescriptionView() {
//TODO: Hide the description for now
descriptionView.isHidden = true
......
//
// BlendMinutelySource.h
// BlendMinutelySource
//
// Created by Dmitry Stepanets on 24.08.2021.
//
#import <Foundation/Foundation.h>
//! Project version number for BlendMinutelySource.
FOUNDATION_EXPORT double BlendMinutelySourceVersionNumber;
//! Project version string for BlendMinutelySource.
FOUNDATION_EXPORT const unsigned char BlendMinutelySourceVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <BlendMinutelySource/PublicHeader.h>
//
// BlendMinutelySource.swift
// BlendMinutelySource
//
// Created by Dmitry Stepanets on 24.08.2021.
//
import Foundation
import OneWeatherCore
public enum BlendMinutelySourceError: Error {
case badUrl
case networkError(Error?)
case badServerResponse(Error?)
case dataEncodingError(String)
case alreadyBeingUpdated
case invalidParameters
}
public class BlendMinutelySource: MinutelyForecastSource {
//Private
private let kBlendApiKey = "0imfnc8mVLWwsAawjYr4Rx-Af50DDqtlx"
private let kEndpoitURL = "https://pro-1w-dataaggregator.onelouder.com/1weather/api/v1/weather/nowcast"
private let kCountry = "US"
private let kWindUnit = "mph"
private let kPressureUnit = "inHg"
private let kPrecipitationUnit = "inhr"
private let kTempUnit = "F"
private lazy var dateFormatter: DateFormatter = {
let fmt = DateFormatter()
fmt.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
return fmt
}()
/// This queue is needed to synchronize access to locationsBeingUpdated. Also, to make logging more clear.
private let internalQueue: OperationQueue = {
let queue = OperationQueue()
queue.name = "BlendMinutelySource Queue"
queue.maxConcurrentOperationCount = 1
return queue
}()
private var locationsBeingUpdated = Set<Location>()
//Public
#if DEBUG
public let minutelyUpdateInterval = TimeInterval(2 * 60) // 2 minutes
#else
public let minutelyUpdateInterval = TimeInterval(15 * 60) // 15 minutes
#endif
public init() {}
public func getMinutelyForecast(forLocation location: Location, completion: @escaping MinutelyForecastCompletion) {
internalQueue.addOperation { [weak self] in
let extendedCompletion: MinutelyForecastCompletion = { [weak self] result in
self?.internalQueue.addOperation {
completion(result)
self?.locationsBeingUpdated.remove(location)
}
}
self?.getMinutelyForecastInternal(forLocation: location, completion: extendedCompletion)
}
}
private func getMinutelyForecastInternal(forLocation location: Location, completion: @escaping MinutelyForecastCompletion) {
guard !locationsBeingUpdated.contains(location) else {
completion(.failure(BlendMinutelySourceError.alreadyBeingUpdated))
return
}
locationsBeingUpdated.insert(location)
guard location.region != nil, location.cityName != nil else {
completion(.failure(BlendMinutelySourceError.invalidParameters))
return
}
let endpointURL = URL(string: kEndpoitURL)!
let queryItems = [URLQueryItem(name: "lat", value: location.lat),
URLQueryItem(name: "lon", value: location.lon),
URLQueryItem(name: "state", value: location.region),
URLQueryItem(name: "country", value: kCountry),
URLQueryItem(name: "city", value: location.cityName),
URLQueryItem(name: "wind_unit", value: kWindUnit),
URLQueryItem(name: "pressure_unit", value: kPressureUnit),
URLQueryItem(name: "prec_unit", value: kPrecipitationUnit),
URLQueryItem(name: "temp_unit", value: kTempUnit)]
guard var components = URLComponents(url: endpointURL, resolvingAgainstBaseURL: true) else {
completion(.failure(BlendMinutelySourceError.badUrl))
return
}
components.queryItems = queryItems
guard let requestURL = components.url else {
completion(.failure(BlendMinutelySourceError.badUrl))
return
}
var request = URLRequest(url: requestURL)
request.addValue(kBlendApiKey, forHTTPHeaderField: "blend-api-key")
URLSession.shared.dataTask(with: request) {[weak self] data, response, error in
guard let self = self else {
completion(.failure(BlendMinutelySourceError.dataEncodingError("Missing self")))
return
}
if let networkError = error {
completion(.failure(BlendMinutelySourceError.networkError(networkError)))
return
}
guard let forecastData = data else {
completion(.failure(BlendMinutelySourceError.dataEncodingError("Incoming data is invalid")))
return
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .formatted(self.dateFormatter)
do {
let blendForecast = try decoder.decode(BlendMinutelyForecast.self, from: forecastData)
let forecast = self.covnertToAppModel(itemToConvert: blendForecast)
completion(.success(forecast))
}
catch {
guard let bodyData = data else {
completion(.failure(BlendMinutelySourceError.dataEncodingError(error.localizedDescription)))
return
}
let body = String(data: bodyData, encoding: .utf8) ?? "N/A"
completion(.failure(BlendMinutelySourceError.dataEncodingError(body)))
}
}
.resume()
}
private func covnertToAppModel(itemToConvert: BlendMinutelyForecast) -> MinutelyForecast {
let items = itemToConvert.forecast.map { MinutelyItem(time: $0.time,
temp: .init(value: Double($0.temp), unit: .fahrenheit),
precipitation: $0.precipitation,
windSpeed: .init(value: Double($0.windSpeed), unit: .milesPerHour),
pressure: .init(value: Double($0.pressure), unit: .inchesOfMercury)) }
let minutelyForecast = MinutelyForecast(lastUpdateTime: Date(),
forecast: items)
return minutelyForecast
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>
//
// BlendMinutelyForecast.swift
// BlendMinutelySource
//
// Created by Dmitry Stepanets on 24.08.2021.
//
import Foundation
import OneWeatherCore
struct BlendMinutelyForecast: Codable {
public let forecastInterval: Int
public let tempUnit: String
public let windUnit: String
public let pressureUnit: String
public let forecast: [BlendMinutelyItem]
}
//
// BlendMinutelyItem.swift
// BlendMinutelySource
//
// Created by Dmitry Stepanets on 24.08.2021.
//
import Foundation
public struct BlendMinutelyItem: Codable {
let time: Date
let temp: Int
let precipitation: Double
let windSpeed: Int
let pressure: Int
}
//
// BlendMinutelySourceTests.swift
// BlendMinutelySourceTests
//
// Created by Dmitry Stepanets on 24.08.2021.
//
import XCTest
@testable import BlendMinutelySource
class BlendMinutelySourceTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() throws {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
......@@ -39,6 +39,10 @@
CD8B861826F9C5EA00E3A9CD /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD8B861726F9C5EA00E3A9CD /* CoreLocation.framework */; };
CD8EA914265D42E2000D3D63 /* 1WModel.xcdatamodeld in Resources */ = {isa = PBXBuildFile; fileRef = CD3884372657BA420070FD6F /* 1WModel.xcdatamodeld */; };
CE0E006C26C739680060CBB6 /* CoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0E006B26C739680060CBB6 /* CoreDataStack.swift */; };
CEC40AF42705A2BF00C98305 /* _CoreMinutelyForecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC40AF22705A2BE00C98305 /* _CoreMinutelyForecast.swift */; };
CEC40AF52705A2BF00C98305 /* _CoreMinutelyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC40AF32705A2BE00C98305 /* _CoreMinutelyItem.swift */; };
CEC40AF82705A2C900C98305 /* CoreMinutelyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC40AF62705A2C900C98305 /* CoreMinutelyItem.swift */; };
CEC40AF92705A2C900C98305 /* CoreMinutelyForecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC40AF72705A2C900C98305 /* CoreMinutelyForecast.swift */; };
CEEF40F9265E2EE600425D8F /* OneWeatherCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEEF40F8265E2EE600425D8F /* OneWeatherCore.framework */; };
/* End PBXBuildFile section */
......@@ -90,6 +94,11 @@
CD69DBC12666381500FD2A7C /* OneWeatherAnalytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneWeatherAnalytics.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CD8B861726F9C5EA00E3A9CD /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; };
CE0E006B26C739680060CBB6 /* CoreDataStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStack.swift; sourceTree = "<group>"; };
CEC40AF1270587F600C98305 /* 1.2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = 1.2.xcdatamodel; sourceTree = "<group>"; };
CEC40AF22705A2BE00C98305 /* _CoreMinutelyForecast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _CoreMinutelyForecast.swift; sourceTree = "<group>"; };
CEC40AF32705A2BE00C98305 /* _CoreMinutelyItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _CoreMinutelyItem.swift; sourceTree = "<group>"; };
CEC40AF62705A2C900C98305 /* CoreMinutelyItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreMinutelyItem.swift; sourceTree = "<group>"; };
CEC40AF72705A2C900C98305 /* CoreMinutelyForecast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreMinutelyForecast.swift; sourceTree = "<group>"; };
CEEF40F8265E2EE600425D8F /* OneWeatherCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneWeatherCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E5F0E97C8CB8930C9E20B7FD /* Pods-CoreDataStorage.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreDataStorage.debug.xcconfig"; path = "Target Support Files/Pods-CoreDataStorage/Pods-CoreDataStorage.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
......@@ -176,6 +185,8 @@
CD3884262657BA410070FD6F /* CoreHourlyWeather.swift */,
CD3884272657BA410070FD6F /* CoreNotifications.swift */,
CD3884282657BA410070FD6F /* CoreNWSAlert.swift */,
CEC40AF72705A2C900C98305 /* CoreMinutelyForecast.swift */,
CEC40AF62705A2C900C98305 /* CoreMinutelyItem.swift */,
);
path = Human;
sourceTree = "<group>";
......@@ -193,6 +204,8 @@
CD3884312657BA410070FD6F /* _CoreDailyWeather.swift */,
CD3884322657BA410070FD6F /* _CoreNotifications.swift */,
CD3884332657BA410070FD6F /* _CoreNWSAlert.swift */,
CEC40AF22705A2BE00C98305 /* _CoreMinutelyForecast.swift */,
CEC40AF32705A2BE00C98305 /* _CoreMinutelyItem.swift */,
);
path = Machine;
sourceTree = "<group>";
......@@ -369,10 +382,12 @@
CD3884442657BA550070FD6F /* CoreHealth.swift in Sources */,
CD3884452657BA550070FD6F /* CoreLocation.swift in Sources */,
CD3884462657BA550070FD6F /* CoreDailyWeather.swift in Sources */,
CEC40AF52705A2BF00C98305 /* _CoreMinutelyItem.swift in Sources */,
CD3884472657BA550070FD6F /* CoreHourlyWeather.swift in Sources */,
CD3884482657BA550070FD6F /* CoreNotifications.swift in Sources */,
CD3884492657BA550070FD6F /* CoreNWSAlert.swift in Sources */,
CD38844A2657BA550070FD6F /* _CoreAppData.swift in Sources */,
CEC40AF82705A2C900C98305 /* CoreMinutelyItem.swift in Sources */,
CD38844B2657BA550070FD6F /* _CoreAirQuality.swift in Sources */,
CD38844C2657BA550070FD6F /* _CorePollutant.swift in Sources */,
CD38844D2657BA550070FD6F /* _CoreHourlyWeather.swift in Sources */,
......@@ -380,11 +395,13 @@
CD38844F2657BA550070FD6F /* _CoreLocation.swift in Sources */,
CD3884502657BA550070FD6F /* _CoreCurrentWeather.swift in Sources */,
CD3884512657BA550070FD6F /* _CoreDailyWeather.swift in Sources */,
CEC40AF42705A2BF00C98305 /* _CoreMinutelyForecast.swift in Sources */,
CD3884522657BA550070FD6F /* _CoreNotifications.swift in Sources */,
CD3884532657BA550070FD6F /* _CoreNWSAlert.swift in Sources */,
CE0E006C26C739680060CBB6 /* CoreDataStack.swift in Sources */,
CD38843F2657BA430070FD6F /* CoreDataStorage.swift in Sources */,
CD38843B2657BA430070FD6F /* CoreDataAppModelConvertable.swift in Sources */,
CEC40AF92705A2C900C98305 /* CoreMinutelyForecast.swift in Sources */,
CD38843D2657BA430070FD6F /* CoreDataError.swift in Sources */,
CD38843E2657BA430070FD6F /* CoreDataUtils.swift in Sources */,
);
......@@ -659,9 +676,10 @@
CD3884372657BA420070FD6F /* 1WModel.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
CEC40AF1270587F600C98305 /* 1.2.xcdatamodel */,
CD3884362657BA420070FD6F /* Model.xcdatamodel */,
);
currentVersion = CD3884362657BA420070FD6F /* Model.xcdatamodel */;
currentVersion = CEC40AF1270587F600C98305 /* 1.2.xcdatamodel */;
path = 1WModel.xcdatamodeld;
sourceTree = "<group>";
};
......
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>1.2.xcdatamodel</string>
</dict>
</plist>
......@@ -37,6 +37,7 @@ open class CoreLocation: _CoreLocation, CoreDataAppModelConvertable {
location.health = try self.health?.toAppModel()
location.notifications = try self.notifications?.toAppModel()
location.minutely = try self.minutely?.toAppModel()
return location
}
......@@ -86,6 +87,9 @@ open class CoreLocation: _CoreLocation, CoreDataAppModelConvertable {
self.notifications = skipIfError(attribute: "notifications", action: {
try CoreNotifications(context: context, appModel: appModel.notifications)
})
self.minutely = skipIfError(attribute: "minutely", action: {
try CoreMinutelyForecast(context: context, appModel: appModel.minutely)
})
}
private func skipIfError<T>(attribute: String, action: () throws -> T?) -> T? {
......
import Foundation
import CoreData
import OneWeatherCore
import OneWeatherAnalytics
@objc(CoreMinutelyForecast)
open class CoreMinutelyForecast: _CoreMinutelyForecast, CoreDataAppModelConvertable {
func toAppModel() throws -> MinutelyForecast {
let lastUpdateTime: Date = self.lastUpdateTime
var minutely = [MinutelyItem]()
try CoreDataUtils.foreach(in: items, of: self, attributeName: "minutely") { (coreMinutelyItem: CoreMinutelyItem) in
minutely.append(try coreMinutelyItem.toAppModel())
}
return MinutelyForecast(lastUpdateTime: lastUpdateTime, forecast: minutely)
}
/// This is here just so that we could inherit the generated init(managedObjectContext) convenience initializer.
public override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
required public init?(context: NSManagedObjectContext, appModel: MinutelyForecast?) throws {
guard let appModel = appModel else {
return nil
}
self.init(managedObjectContext: context)
self.lastUpdateTime = appModel.lastUpdateTime
self.items = NSOrderedSet(array: appModel.forecast.compactMap { elem in
skipIfError(attribute: "minutely") {
try CoreMinutelyItem(context: context, appModel: elem)
}
})
}
private func skipIfError<T>(attribute: String, action: () throws -> T?) -> T? {
do {
let result = try action()
return result
}
catch {
Logger(componentName: "CoreLocation").error("Couldn't parse \(attribute) in CoreLocation due to error: \(error)")
return nil
}
}
typealias AppModel = MinutelyForecast
}
import Foundation
import CoreData
import OneWeatherCore
@objc(CoreMinutelyItem)
open class CoreMinutelyItem: _CoreMinutelyItem, CoreDataAppModelConvertable {
func toAppModel() throws -> MinutelyItem {
guard let temp: Temperature = try CoreDataUtils.measurement(from: self.temp, in: self, attributeName: "temp") else {
throw CoreDataError.LoadAttributeError(entity: self, attributeName: "temp", value: self.temp, nestedError: nil)
}
guard let windSpeed: WindSpeed = try CoreDataUtils.measurement(from: self.windSpeed, in: self, attributeName: "windSpeed") else {
throw CoreDataError.LoadAttributeError(entity: self, attributeName: "windSpeed", value: self.windSpeed, nestedError: nil)
}
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)
}
return MinutelyItem(time: time, temp: temp, precipitation: precipitation, windSpeed: windSpeed, pressure: pressure)
}
/// This is here just so that we could inherit the generated init(managedObjectContext) convenience initializer.
public override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
required public init?(context: NSManagedObjectContext, appModel: MinutelyItem?) throws {
guard let appModel = appModel else {
return nil
}
self.init(managedObjectContext: context)
self.time = appModel.time
self.precipitation = appModel.precipitation
self.temp = try CoreDataUtils.measurementToData(appModel.temp, in: self, attributeName: "temp")
self.windSpeed = try CoreDataUtils.measurementToData(appModel.windSpeed, in: self, attributeName: "windSpeed")
self.pressure = try CoreDataUtils.measurementToData(appModel.pressure, in: self, attributeName: "pressure")
}
typealias AppModel = MinutelyItem
}
......@@ -25,6 +25,7 @@ public enum CoreLocationRelationships: String {
case daily = "daily"
case health = "health"
case hourly = "hourly"
case minutely = "minutely"
case notifications = "notifications"
case today = "today"
}
......@@ -121,6 +122,9 @@ open class _CoreLocation: NSManagedObject {
}
@NSManaged open
var minutely: CoreMinutelyForecast?
@NSManaged open
var notifications: CoreNotifications?
@NSManaged open
......
// DO NOT EDIT. This file is machine-generated and constantly overwritten.
// Make changes to CoreMinutelyForecast.swift instead.
import Foundation
import CoreData
public enum CoreMinutelyForecastAttributes: String {
case lastUpdateTime = "lastUpdateTime"
}
public enum CoreMinutelyForecastRelationships: String {
case items = "items"
case location = "location"
}
open class _CoreMinutelyForecast: NSManagedObject {
// MARK: - Class methods
open class func entityName () -> String {
return "CoreMinutelyForecast"
}
open class func entity(managedObjectContext: NSManagedObjectContext) -> NSEntityDescription? {
return NSEntityDescription.entity(forEntityName: self.entityName(), in: managedObjectContext)
}
@nonobjc
open class func fetchRequest() -> NSFetchRequest<CoreMinutelyForecast> {
return NSFetchRequest(entityName: self.entityName())
}
// MARK: - Life cycle methods
public override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
public convenience init?(managedObjectContext: NSManagedObjectContext) {
guard let entity = _CoreMinutelyForecast.entity(managedObjectContext: managedObjectContext) else { return nil }
self.init(entity: entity, insertInto: managedObjectContext)
}
// MARK: - Properties
@NSManaged open
var lastUpdateTime: Date!
// MARK: - Relationships
@NSManaged open
var items: NSOrderedSet
open func itemsSet() -> NSMutableOrderedSet {
return self.items.mutableCopy() as! NSMutableOrderedSet
}
@NSManaged open
var location: CoreLocation
}
extension _CoreMinutelyForecast {
open func addItems(_ objects: NSOrderedSet) {
let mutable = self.items.mutableCopy() as! NSMutableOrderedSet
mutable.union(objects)
self.items = mutable.copy() as! NSOrderedSet
}
open func removeItems(_ objects: NSOrderedSet) {
let mutable = self.items.mutableCopy() as! NSMutableOrderedSet
mutable.minus(objects)
self.items = mutable.copy() as! NSOrderedSet
}
open func addItemsObject(_ value: CoreMinutelyItem) {
let mutable = self.items.mutableCopy() as! NSMutableOrderedSet
mutable.add(value)
self.items = mutable.copy() as! NSOrderedSet
}
open func removeItemsObject(_ value: CoreMinutelyItem) {
let mutable = self.items.mutableCopy() as! NSMutableOrderedSet
mutable.remove(value)
self.items = mutable.copy() as! NSOrderedSet
}
}
// DO NOT EDIT. This file is machine-generated and constantly overwritten.
// Make changes to CoreMinutelyItem.swift instead.
import Foundation
import CoreData
public enum CoreMinutelyItemAttributes: String {
case precipitation = "precipitation"
case pressure = "pressure"
case temp = "temp"
case time = "time"
case windSpeed = "windSpeed"
}
public enum CoreMinutelyItemRelationships: String {
case forecast = "forecast"
}
open class _CoreMinutelyItem: NSManagedObject {
// MARK: - Class methods
open class func entityName () -> String {
return "CoreMinutelyItem"
}
open class func entity(managedObjectContext: NSManagedObjectContext) -> NSEntityDescription? {
return NSEntityDescription.entity(forEntityName: self.entityName(), in: managedObjectContext)
}
@nonobjc
open class func fetchRequest() -> NSFetchRequest<CoreMinutelyItem> {
return NSFetchRequest(entityName: self.entityName())
}
// MARK: - Life cycle methods
public override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
public convenience init?(managedObjectContext: NSManagedObjectContext) {
guard let entity = _CoreMinutelyItem.entity(managedObjectContext: managedObjectContext) else { return nil }
self.init(entity: entity, insertInto: managedObjectContext)
}
// MARK: - Properties
@NSManaged open
var precipitation: Double
@NSManaged open
var pressure: Data!
@NSManaged open
var temp: Data!
@NSManaged open
var time: Date!
@NSManaged open
var windSpeed: Data!
// MARK: - Relationships
@NSManaged open
var forecast: CoreMinutelyForecast
}
......@@ -73,6 +73,9 @@
CDD2F8EF2665102B00B48322 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD2F8EE2665102B00B48322 /* LocationManager.swift */; };
CDD2F8F12665112900B48322 /* DeviceLocationMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD2F8F02665112800B48322 /* DeviceLocationMonitor.swift */; };
CDD2F8F62665117400B48322 /* NWSAlertsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD2F8F42665117400B48322 /* NWSAlertsManager.swift */; };
CDF07A0126D5027300E797D9 /* MinutelyForecastSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF07A0026D5027300E797D9 /* MinutelyForecastSource.swift */; };
CDF07A0526D5032800E797D9 /* MinutelyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF07A0326D5032800E797D9 /* MinutelyItem.swift */; };
CDF07A0626D5032800E797D9 /* MinutelyForecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF07A0426D5032800E797D9 /* MinutelyForecast.swift */; };
CDFE458D26566BD50021A29F /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDFE458C26566BD50021A29F /* Storage.swift */; };
CDFE459426566D7B0021A29F /* HealthSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDFE459326566D7B0021A29F /* HealthSource.swift */; };
CDFE459626566D860021A29F /* FIPSSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDFE459526566D860021A29F /* FIPSSource.swift */; };
......@@ -185,6 +188,9 @@
CDD2F8EE2665102B00B48322 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; };
CDD2F8F02665112800B48322 /* DeviceLocationMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceLocationMonitor.swift; sourceTree = "<group>"; };
CDD2F8F42665117400B48322 /* NWSAlertsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NWSAlertsManager.swift; sourceTree = "<group>"; };
CDF07A0026D5027300E797D9 /* MinutelyForecastSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinutelyForecastSource.swift; sourceTree = "<group>"; };
CDF07A0326D5032800E797D9 /* MinutelyItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MinutelyItem.swift; sourceTree = "<group>"; };
CDF07A0426D5032800E797D9 /* MinutelyForecast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MinutelyForecast.swift; sourceTree = "<group>"; };
CDFE458C26566BD50021A29F /* Storage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
CDFE459326566D7B0021A29F /* HealthSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthSource.swift; sourceTree = "<group>"; };
CDFE459526566D860021A29F /* FIPSSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FIPSSource.swift; sourceTree = "<group>"; };
......@@ -388,6 +394,7 @@
CD615F912655269200B717DB /* Health */,
CD615F932655269200B717DB /* Notifications */,
CD3883EA2657B82A0070FD6F /* FIPS */,
CDF07A0226D502F500E797D9 /* Minutely */,
CD427D1A266F5F0500B4350A /* Shorts */,
);
path = ModelObjects;
......@@ -464,6 +471,15 @@
path = Managers;
sourceTree = "<group>";
};
CDF07A0226D502F500E797D9 /* Minutely */ = {
isa = PBXGroup;
children = (
CDF07A0426D5032800E797D9 /* MinutelyForecast.swift */,
CDF07A0326D5032800E797D9 /* MinutelyItem.swift */,
);
path = Minutely;
sourceTree = "<group>";
};
CDFE458B26566BC20021A29F /* Storage */ = {
isa = PBXGroup;
children = (
......@@ -482,6 +498,7 @@
CDFE459526566D860021A29F /* FIPSSource.swift */,
CDFE459326566D7B0021A29F /* HealthSource.swift */,
CD427D18266F5DCE00B4350A /* ShortsSource.swift */,
CDF07A0026D5027300E797D9 /* MinutelyForecastSource.swift */,
);
path = Sources;
sourceTree = "<group>";
......@@ -723,6 +740,7 @@
CDD2F8F12665112900B48322 /* DeviceLocationMonitor.swift in Sources */,
CD2D55D626553384007B70F4 /* UserDefaultsValue.swift in Sources */,
CD550FBA265531A100257FB5 /* RadarLayer.swift in Sources */,
CDF07A0626D5032800E797D9 /* MinutelyForecast.swift in Sources */,
CD550FBB265531A100257FB5 /* RadarLayerType.swift in Sources */,
CE3D393A26E7A66200E7E738 /* SubscriptionConfig.swift in Sources */,
CDBC243F2656740E00F9F4E2 /* AppData.swift in Sources */,
......@@ -745,6 +763,7 @@
CD91685726552FAE00EC04EF /* MulticastDelegate.swift in Sources */,
CDFE458D26566BD50021A29F /* Storage.swift in Sources */,
CD615FBD2655295C00B717DB /* Measurement+String.swift in Sources */,
CDF07A0526D5032800E797D9 /* MinutelyItem.swift in Sources */,
CD615FBE2655295C00B717DB /* Calendar+TimeZone.swift in Sources */,
CD71B9C6265E629D00803DBB /* String+NewLine.swift in Sources */,
CD91685F26552FEC00EC04EF /* Global.swift in Sources */,
......@@ -759,6 +778,7 @@
CD427D19266F5DCE00B4350A /* ShortsSource.swift in Sources */,
CD2D55D8265533F4007B70F4 /* UserDefaultsWrapper.swift in Sources */,
CDD2F8F62665117400B48322 /* NWSAlertsManager.swift in Sources */,
CDF07A0126D5027300E797D9 /* MinutelyForecastSource.swift in Sources */,
CE3A112726CD3CDE00D925C7 /* UserDefaults+OneWeather.swift in Sources */,
CD11AFE726651BF900EC4BA0 /* LegacyWdtLocation.swift in Sources */,
CD11AFE326651B6300EC4BA0 /* LegacyMigrationManager.swift in Sources */,
......
......@@ -128,11 +128,15 @@ public class WidgetManager {
}
let name = WidgetOptions.name(for: option)
var launchParams: [AnalyticsParameter : Any] = [.ANALYTICS_KEY_WIDGET_NAME : name]
if let location = LocationManager.shared.selectedLocation {
let smartText = smartTextProvider.smartText(for: location)
launchParams[.ANALYTICS_KEY_WIDGET_SMART_TEXT] = smartText
}
AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_CARD_CLICK)
AppAnalytics.shared.log(event: .ANALYTICS_LAUNCH_FROM_WIDGET,
params: [.ANALYTICS_KEY_WIDGET_NAME : name])
AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_LAUNCH_FROM,
params: [.ANALYTICS_KEY_WIDGET_NAME : name])
AppAnalytics.shared.log(event: .ANALYTICS_LAUNCH_FROM_WIDGET, params: launchParams)
}
public func logUpdate(forLocation location: Location?, kind: String, family: WidgetFamily) {
......
......@@ -23,6 +23,7 @@ public class LocationManager {
private let weatherUpdateSource: WeatherSource
private let healthSource: HealthSource
private let minutelyForecastSource: MinutelyForecastSource
private let fipsSource: FIPSSource
public let nwsAlertsManager: NWSAlertsManager
private var pushNotificationsManager: PushNotificationsManagerProtocol?
......@@ -30,6 +31,8 @@ public class LocationManager {
private let legacyMigrator = LegacyMigrationManager()
private var defaultLocation = Location(deviceLocation: false,
coordinates: .init(latitude: 37.3230, longitude: -122.0322), // Cupertino
region: "US",
cityName: "Cupertino",
timeZone: TimeZone(abbreviation: "PST")!) {
didSet {
if locations.count == 0 {
......@@ -209,9 +212,10 @@ public class LocationManager {
!locations.isEmpty || deviceLocationMonitor.hasLocationPermissions
}
public init(weatherUpdateSource: WeatherSource, healthSource: HealthSource, nwsAlertsManager: NWSAlertsManager, fipsSource: FIPSSource, pushNotificationsManager: PushNotificationsManagerProtocol?, storage: Storage) {
public init(weatherUpdateSource: WeatherSource, healthSource: HealthSource, minutelyForecastSource: MinutelyForecastSource, nwsAlertsManager: NWSAlertsManager, fipsSource: FIPSSource, pushNotificationsManager: PushNotificationsManagerProtocol?, storage: Storage) {
self.weatherUpdateSource = weatherUpdateSource
self.healthSource = healthSource
self.minutelyForecastSource = minutelyForecastSource
self.deviceLocationMonitor = DeviceLocationMonitor()
self.nwsAlertsManager = nwsAlertsManager
self.fipsSource = fipsSource
......@@ -301,6 +305,7 @@ public class LocationManager {
log.info("Update all: update default location if needed.")
updateWeather(for: defaultLocation, updateType: .full)
updateHealth(for: defaultLocation)
updateMinutelyForecast(for: defaultLocation)
return
}
log.info("Update all \(locations.count) locations if needed...")
......@@ -312,6 +317,7 @@ public class LocationManager {
updateWeather(for: location, updateType: .preferIncremental)
}
updateHealth(for: location)
updateMinutelyForecast(for: location)
updateNotifications(for: location)
getFipsIfNeeded(for: location)
}
......@@ -369,6 +375,30 @@ public class LocationManager {
}
}
public func updateMinutelyForecast(for location: Location) {
if let lastTimeUpdated = location.minutely?.lastUpdateTime {
guard Date().timeIntervalSince(lastTimeUpdated) >= minutelyForecastSource.minutelyUpdateInterval else {
log.verbose("Update minutely forecast (\(location)): fresh enough (last updated at \(lastTimeUpdated)), skip update.")
return
}
}
log.info("Update minutely forecast for: \(location)")
minutelyForecastSource.getMinutelyForecast(forLocation: location) {[weak self] result in
guard let self = self else { return }
switch result {
case .success(let minutelyForecast):
self.makeChanges(to: location, in: "minutely") { (location) -> Location in
var updatedLocation = location
updatedLocation.minutely = minutelyForecast
return updatedLocation
}
case .failure(let error):
self.log.error("Update minutely (\(location) error: \(error)")
}
}
}
public func updateNotifications(for location: Location) {
if let lastTimeUpdated = location.notifications?.updatedAt {
guard Date().timeIntervalSince(lastTimeUpdated) >= nwsAlertsManager.updateInterval else {
......
......@@ -47,6 +47,7 @@ public struct Location {
public var today: CurrentWeather?
public var daily = [DailyWeather]()
public var minutely: MinutelyForecast?
public var hourly = [HourlyWeather]() {
didSet {
let calendar = Calendar.timeZoneCalendar(timeZone: self.timeZone)
......
//
// MinutelyForecast.swift
// OneWeatherCore
//
// Created by Dmitry Stepanets on 24.08.2021.
//
import Foundation
public struct MinutelyForecast {
public let lastUpdateTime: Date
public let forecast: [MinutelyItem]
public init(lastUpdateTime: Date, forecast: [MinutelyItem]) {
self.lastUpdateTime = lastUpdateTime
self.forecast = forecast
}
}
//
// MinutelyItem.swift
// OneWeatherCore
//
// Created by Dmitry Stepanets on 24.08.2021.
//
import Foundation
import UIKit
public struct MinutelyItem {
public let time: Date
public let temp: Temperature
public let precipitation: Double
public let windSpeed: WindSpeed
public let pressure: Pressure
public var weatherTypeImage: UIImage?
public init(time: Date, temp: Temperature, precipitation: Double, windSpeed: WindSpeed, pressure: Pressure) {
self.time = time
self.temp = temp
self.precipitation = precipitation
self.windSpeed = windSpeed
self.pressure = pressure
}
}
......@@ -97,7 +97,7 @@ public class Settings {
@UserDefaultsBasicValue(key: "initial_onboarding_showed", userDefaults: UserDefaults.appDefaults)
public var initialOnboardingShowed = false
@UserDefaultsBasicValue(key: "shorts_showed_swipeUp_count", userDefaults: UserDefaults.appDefaults)
public var shortsSwipeUpNudgeShowedCount = 0
......
//
// MinutelyForecastSource.swift
// OneWeatherCore
//
// Created by Dmitry Stepanets on 24.08.2021.
//
import Foundation
public typealias MinutelyForecastCompletion = (_ result: Result<MinutelyForecast, Error>) -> ()
public protocol MinutelyForecastSource {
var minutelyUpdateInterval: TimeInterval { get }
func getMinutelyForecast(forLocation location: Location, completion: @escaping MinutelyForecastCompletion)
}
......@@ -31,7 +31,7 @@ public class WdtWeatherSource: WeatherSource {
#else
public let weatherUpdateInterval = TimeInterval(15 * 60) // 15 minutes
#endif
public init() {}
public func updateWeather(for location: Location, type: WeatherUpdateType, completion: @escaping WeatherSourceCompletion) {
......
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