Commit 699a1ed0 by Demid Merzlyakov

IOS-241: widgets now use a shared data source and don't send separate network…

IOS-241: widgets now use a shared data source and don't send separate network requests each. Plus, widgets don't generate a Radar snapshot when it's not needed.
parent 3b733415
...@@ -250,6 +250,7 @@ ...@@ -250,6 +250,7 @@
CEC8FBAF2639756A0001A6BF /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC8FBAE2639756A0001A6BF /* OnboardingViewController.swift */; }; CEC8FBAF2639756A0001A6BF /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC8FBAE2639756A0001A6BF /* OnboardingViewController.swift */; };
CEC8FBB2263976240001A6BF /* OnboardingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC8FBB1263976240001A6BF /* OnboardingCoordinator.swift */; }; CEC8FBB2263976240001A6BF /* OnboardingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC8FBB1263976240001A6BF /* OnboardingCoordinator.swift */; };
CEC8FBB5263976400001A6BF /* OnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC8FBB4263976400001A6BF /* OnboardingViewModel.swift */; }; CEC8FBB5263976400001A6BF /* OnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC8FBB4263976400001A6BF /* OnboardingViewModel.swift */; };
CEE1150626D987C5008FE415 /* WidgetLocationSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE1150526D987C5008FE415 /* WidgetLocationSource.swift */; };
CEE8869526C30F680000161B /* OneWeatherUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD5909CF26A59AAA00448579 /* OneWeatherUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; CEE8869526C30F680000161B /* OneWeatherUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD5909CF26A59AAA00448579 /* OneWeatherUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
CEEB3547266F5D9900E16F90 /* BannerAdCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEB3546266F5D9900E16F90 /* BannerAdCell.swift */; }; CEEB3547266F5D9900E16F90 /* BannerAdCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEB3546266F5D9900E16F90 /* BannerAdCell.swift */; };
CEEB3549266F5DA900E16F90 /* MRECAdCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEB3548266F5DA900E16F90 /* MRECAdCell.swift */; }; CEEB3549266F5DA900E16F90 /* MRECAdCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEB3548266F5DA900E16F90 /* MRECAdCell.swift */; };
...@@ -544,6 +545,7 @@ ...@@ -544,6 +545,7 @@
CEC8FBAE2639756A0001A6BF /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; }; CEC8FBAE2639756A0001A6BF /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
CEC8FBB1263976240001A6BF /* OnboardingCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingCoordinator.swift; sourceTree = "<group>"; }; CEC8FBB1263976240001A6BF /* OnboardingCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingCoordinator.swift; sourceTree = "<group>"; };
CEC8FBB4263976400001A6BF /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = "<group>"; }; CEC8FBB4263976400001A6BF /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = "<group>"; };
CEE1150526D987C5008FE415 /* WidgetLocationSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetLocationSource.swift; sourceTree = "<group>"; };
CEEB3546266F5D9900E16F90 /* BannerAdCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerAdCell.swift; sourceTree = "<group>"; }; CEEB3546266F5D9900E16F90 /* BannerAdCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerAdCell.swift; sourceTree = "<group>"; };
CEEB3548266F5DA900E16F90 /* MRECAdCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MRECAdCell.swift; sourceTree = "<group>"; }; CEEB3548266F5DA900E16F90 /* MRECAdCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MRECAdCell.swift; sourceTree = "<group>"; };
CEEF40FF265E47FF00425D8F /* BlendFIPSSource.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BlendFIPSSource.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CEEF40FF265E47FF00425D8F /* BlendFIPSSource.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BlendFIPSSource.framework; sourceTree = BUILT_PRODUCTS_DIR; };
...@@ -847,6 +849,7 @@ ...@@ -847,6 +849,7 @@
CD415DA22668FFF300177515 /* WeatherProvider.swift */, CD415DA22668FFF300177515 /* WeatherProvider.swift */,
CD5293D92669094E009547C8 /* WeatherEntry.swift */, CD5293D92669094E009547C8 /* WeatherEntry.swift */,
CDA02A1826A6F92F00A8F2F6 /* WeatherLocationMock.swift */, CDA02A1826A6F92F00A8F2F6 /* WeatherLocationMock.swift */,
CEE1150526D987C5008FE415 /* WidgetLocationSource.swift */,
); );
path = Data; path = Data;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -2017,6 +2020,7 @@ ...@@ -2017,6 +2020,7 @@
CD5293E1266A4258009547C8 /* AppFont.swift in Sources */, CD5293E1266A4258009547C8 /* AppFont.swift in Sources */,
CDA02A1926A6F92F00A8F2F6 /* WeatherLocationMock.swift in Sources */, CDA02A1926A6F92F00A8F2F6 /* WeatherLocationMock.swift in Sources */,
CD5293DF266A235F009547C8 /* ForecastWidgetViewModel.swift in Sources */, CD5293DF266A235F009547C8 /* ForecastWidgetViewModel.swift in Sources */,
CEE1150626D987C5008FE415 /* WidgetLocationSource.swift in Sources */,
CDF6E87726A8329D004A9DBD /* WindWidget.swift in Sources */, CDF6E87726A8329D004A9DBD /* WindWidget.swift in Sources */,
CD5293D8266908DB009547C8 /* WidgetPlaceholderView.swift in Sources */, CD5293D8266908DB009547C8 /* WidgetPlaceholderView.swift in Sources */,
CD5293EA266A564E009547C8 /* ThemeProtocol.swift in Sources */, CD5293EA266A564E009547C8 /* ThemeProtocol.swift in Sources */,
......
...@@ -18,9 +18,17 @@ import OneWeatherUI ...@@ -18,9 +18,17 @@ import OneWeatherUI
class WeatherProvider: TimelineProvider { class WeatherProvider: TimelineProvider {
typealias Entry = WeatherEntry typealias Entry = WeatherEntry
var storage: Storage = CoreDataStorage() private let needsRadar: Bool
var weatherSource: WeatherSource = WdtWeatherSource() private var weatherSource: WeatherSource
private let logger = Logger(componentName: "WeatherProvider") private let locationSource: WidgetLocationSource
private let widgetType: String
public init(widgetType: String, locationSource: WidgetLocationSource = WidgetLocationSource.shared, weatherSource: WeatherSource = WdtWeatherSource(), needsRadar: Bool) {
self.widgetType = widgetType
self.locationSource = locationSource
self.weatherSource = weatherSource
self.needsRadar = needsRadar
}
func placeholder(in context: Context) -> WeatherEntry { func placeholder(in context: Context) -> WeatherEntry {
return WeatherEntry() return WeatherEntry()
...@@ -31,46 +39,14 @@ class WeatherProvider: TimelineProvider { ...@@ -31,46 +39,14 @@ class WeatherProvider: TimelineProvider {
completion(entry) completion(entry)
} }
func isFreshEnough(_ location: Location) -> Bool {
guard let lastTimeUpdated = location.lastWeatherUpdateDate else {
return false
}
return Date().timeIntervalSince(lastTimeUpdated) < self.weatherSource.weatherUpdateInterval
}
func getUpToDateLocation(_ completion: @escaping (Location?) -> () ) {
storage.load { [weak self] (locations, selectedIndex, error) in
guard let self = self else {
completion(nil)
return
}
guard let locations = locations, let selectedIndex = selectedIndex, selectedIndex < locations.count else {
completion(nil)
return
}
let selectedLocation = locations[selectedIndex]
guard !self.isFreshEnough(selectedLocation) else {
completion(selectedLocation)
return
}
self.weatherSource.updateWeather(for: selectedLocation, type: .preferIncremental) { updatedLocation, error in
guard let updatedLocation = updatedLocation else {
completion(selectedLocation)
return
}
completion(updatedLocation)
}
}
}
func getTimeline(in context: Context, completion: @escaping (Timeline<WeatherEntry>) -> Void) { func getTimeline(in context: Context, completion: @escaping (Timeline<WeatherEntry>) -> Void) {
getUpToDateLocation { location in locationSource.getUpToDateLocation { [weak self] location in
guard let self = self else { return }
if if
let fetchedLocation = location, let fetchedLocation = location,
let coordinates = fetchedLocation.coordinates let coordinates = fetchedLocation.coordinates
{ {
if self.needsRadar {
SnapshotLoader.load(at: coordinates, SnapshotLoader.load(at: coordinates,
size: .init(width: 340, height: 280) size: .init(width: 340, height: 280)
) { mapImage in ) { mapImage in
...@@ -83,6 +59,14 @@ class WeatherProvider: TimelineProvider { ...@@ -83,6 +59,14 @@ class WeatherProvider: TimelineProvider {
} }
else { else {
let nextRefresh = Calendar.current.date(byAdding: .minute, value: 30, to: Date())! let nextRefresh = Calendar.current.date(byAdding: .minute, value: 30, to: Date())!
let entry = WeatherEntry(location: fetchedLocation, date: nextRefresh, radarMapImage: nil)
let timeline = Timeline(entries: [entry], policy: .atEnd)
WidgetManager.shared.logUpdate(forLocation: fetchedLocation)
completion(timeline)
}
}
else {
let nextRefresh = Calendar.current.date(byAdding: .minute, value: 30, to: Date())!
let entry = WeatherEntry(location: location, date: nextRefresh) let entry = WeatherEntry(location: location, date: nextRefresh)
let timeline = Timeline(entries: [entry], policy: .atEnd) let timeline = Timeline(entries: [entry], policy: .atEnd)
WidgetManager.shared.logUpdate(forLocation: location) WidgetManager.shared.logUpdate(forLocation: location)
......
//
// WidgetLocationSource.swift
// OneWeatherWidgetExtension
//
// Created by Demid Merzlyakov on 28.08.2021.
//
import Foundation
import OneWeatherCore
import WDTWeatherSource
import CoreDataStorage
private class WeatherRequester {
public typealias Completion = (Result<Location, Error>) -> ()
private let requestedLocation: Location
private let weatherSource: WeatherSource
private var result: Result<Location, Error>?
private let syncQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
private var awaitingCompletions = [Completion]()
public init(for location: Location, with weatherSource: WeatherSource) {
self.requestedLocation = location
self.weatherSource = weatherSource
request()
}
public func await(completion: @escaping Completion) {
syncQueue.addOperation { [weak self] in
guard let self = self else { return }
if let result = self.result {
completion(result)
return
}
let extendedCompletion: Completion = { result in
completion(result)
}
self.awaitingCompletions.append(extendedCompletion)
}
}
private func request() {
weatherSource.updateWeather(for: requestedLocation, type: .preferIncremental) { [weak self] location, error in
guard let self = self else { return }
let result: Result<Location, Error>
if let location = location {
result = .success(location)
}
else if let error = error {
result = .failure(error)
}
else {
result = .failure(NSError())
}
self.syncQueue.addOperation { [weak self] in
guard let self = self else { return }
for completion in self.awaitingCompletions {
completion(result)
}
}
}
}
}
class WidgetLocationSource {
public static let shared = WidgetLocationSource()
private let weatherSource: WeatherSource = WdtWeatherSource()
private let storage: Storage = CoreDataStorage()
private var locationsCache = Set<Location>()
private var activeRequests = [Location: WeatherRequester]()
private let syncQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
private func pickMoreRecent(storageLocation: Location, cacheLocation: Location) -> Location {
var moreRecentLocation = storageLocation
if let cacheUpdateTime = cacheLocation.lastWeatherUpdateDate {
if let storageUpdateTime = storageLocation.lastWeatherUpdateDate {
if cacheUpdateTime > storageUpdateTime {
moreRecentLocation = cacheLocation
}
}
else {
moreRecentLocation = cacheLocation
}
self.locationsCache.remove(cacheLocation)
self.locationsCache.insert(moreRecentLocation)
}
return moreRecentLocation
}
public func getUpToDateLocation(_ completion: @escaping (Location?) -> () ) {
storage.load { [weak self] (locations, selectedIndex, error) in
guard let self = self else {
completion(nil)
return
}
guard let locations = locations, let selectedIndex = selectedIndex, selectedIndex < locations.count else {
completion(nil)
return
}
self.syncQueue.addOperation { [weak self] in
guard let self = self else { return }
var location: Location = locations[selectedIndex]
if let cacheLocation = self.locationsCache.first(where: { $0 == location}) {
location = self.pickMoreRecent(storageLocation: locations[selectedIndex], cacheLocation: cacheLocation)
}
else {
location = locations[selectedIndex]
}
guard !self.isFreshEnough(location) else {
completion(location)
return
}
let requester = self.activeRequests[location] ?? WeatherRequester(for: location, with: self.weatherSource)
self.activeRequests[location] = requester
requester.await { [weak self] result in
self?.syncQueue.addOperation {
switch result {
case .success(let updatedLocation):
completion(updatedLocation)
self?.locationsCache.remove(location)
self?.locationsCache.insert(updatedLocation)
case .failure(_):
completion(location)
}
self?.activeRequests[location] = nil
}
}
}
}
}
private func isFreshEnough(_ location: Location) -> Bool {
guard let lastTimeUpdated = location.lastWeatherUpdateDate else {
return false
}
return Date().timeIntervalSince(lastTimeUpdated) < self.weatherSource.weatherUpdateInterval
}
}
...@@ -17,7 +17,7 @@ struct PrecipitationWidget: Widget { ...@@ -17,7 +17,7 @@ struct PrecipitationWidget: Widget {
// We currently display selectedLocation, so it's not really configurable, // We currently display selectedLocation, so it's not really configurable,
// but we'll probably need to switch to an IntentConfiguration at some point. // but we'll probably need to switch to an IntentConfiguration at some point.
StaticConfiguration(kind: kind, StaticConfiguration(kind: kind,
provider: WeatherProvider() provider: WeatherProvider(widgetType: kind.components(separatedBy: ".").last ?? "-", needsRadar: false)
) { weatherEntry in ) { weatherEntry in
MediumPrecipitationWidgetView(widgetViewModel: ForecastWidgetViewModel(location: weatherEntry.location)) MediumPrecipitationWidgetView(widgetViewModel: ForecastWidgetViewModel(location: weatherEntry.location))
.widgetURL(URL(string: "ow-widget://precipitation-medium")) .widgetURL(URL(string: "ow-widget://precipitation-medium"))
......
...@@ -17,7 +17,7 @@ struct RadarWidget: Widget { ...@@ -17,7 +17,7 @@ struct RadarWidget: Widget {
// We currently display selectedLocation, so it's not really configurable, // We currently display selectedLocation, so it's not really configurable,
// but we'll probably need to switch to an IntentConfiguration at some point. // but we'll probably need to switch to an IntentConfiguration at some point.
StaticConfiguration(kind: kind, StaticConfiguration(kind: kind,
provider: WeatherProvider() provider: WeatherProvider(widgetType: kind.components(separatedBy: ".").last ?? "-", needsRadar: true)
) { weatherEntry in ) { weatherEntry in
LargeRadarWidgetView(widgetViewModel: ForecastWidgetViewModel(location: weatherEntry.location, LargeRadarWidgetView(widgetViewModel: ForecastWidgetViewModel(location: weatherEntry.location,
radarImage: weatherEntry.radarMapImage)) radarImage: weatherEntry.radarMapImage))
......
...@@ -17,7 +17,7 @@ struct TemperatureWidget: Widget { ...@@ -17,7 +17,7 @@ struct TemperatureWidget: Widget {
// We currently display selectedLocation, so it's not really configurable, // We currently display selectedLocation, so it's not really configurable,
// but we'll probably need to switch to an IntentConfiguration at some point. // but we'll probably need to switch to an IntentConfiguration at some point.
StaticConfiguration(kind: kind, StaticConfiguration(kind: kind,
provider: WeatherProvider() provider: WeatherProvider(widgetType: kind.components(separatedBy: ".").last ?? "-", needsRadar: false)
) { weatherEntry in ) { weatherEntry in
WidgetView(entry: weatherEntry) WidgetView(entry: weatherEntry)
} }
......
...@@ -17,7 +17,7 @@ struct WindWidget: Widget { ...@@ -17,7 +17,7 @@ struct WindWidget: Widget {
// We currently display selectedLocation, so it's not really configurable, // We currently display selectedLocation, so it's not really configurable,
// but we'll probably need to switch to an IntentConfiguration at some point. // but we'll probably need to switch to an IntentConfiguration at some point.
StaticConfiguration(kind: kind, StaticConfiguration(kind: kind,
provider: WeatherProvider() provider: WeatherProvider(widgetType: kind.components(separatedBy: ".").last ?? "-", needsRadar: false)
) { weatherEntry in ) { weatherEntry in
WidgetView(entry: weatherEntry) WidgetView(entry: weatherEntry)
} }
......
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