Commit 8e13f07c by Demid Merzlyakov

Storage: optimized saving.

parent 2cfcb142
...@@ -45,49 +45,70 @@ public class LocationManager { ...@@ -45,49 +45,70 @@ public class LocationManager {
public typealias CurrentLocationCompletion = (LocationRequestResult) -> () public typealias CurrentLocationCompletion = (LocationRequestResult) -> ()
public private(set) var locations = [Location]() { private var _locations = [Location]()
didSet { private var _selectedLocationIndex: Int?
log.info("Locations list updated: \(locations.map { $0.description }.joined(separator: ", "))") private func set(locations: [Location], selectedIndex: Int?) {
let newValue = locations DispatchQueue.main.async { [weak self] in
if let selectedIndex = self.selectedLocationIndex, selectedIndex >= newValue.count { self?._locations = locations
self.selectedLocationIndex = newValue.count > 0 ? 0 : nil self?._selectedLocationIndex = selectedIndex
} self?.handleLocationsChange(locationsChanged: true, selectedLocationChanged: true)
else {
updateAllWeatherIfNeeded() //TODO: the whole flow is not optimal.
self.delegates.invoke { (delegate) in
delegate.locationManager(self, changedSelectedLocation: self.selectedLocation)
} }
} }
private func handleLocationsChange(locationsChanged: Bool, selectedLocationChanged: Bool) {
if locationsChanged {
log.info("Locations list updated: \(locations.map { $0.description }.joined(separator: ", "))")
self.delegates.invoke { [weak self] (delegate) in self.delegates.invoke { [weak self] (delegate) in
guard let self = self else { return } guard let self = self else { return }
delegate.locationManager(self, updatedLocationsList: self.locations) delegate.locationManager(self, updatedLocationsList: self.locations)
} }
}
if selectedLocationChanged {
let newLocation = self.selectedLocation
self.delegates.invoke { [weak self] (delegate) in
guard let self = self else { return }
delegate.locationManager(self, changedSelectedLocation: newLocation)
}
}
storage.save(locations: locations, selectedIndex: selectedLocationIndex) storage.save(locations: locations, selectedIndex: selectedLocationIndex)
updateAllWeatherIfNeeded()
} }
public private(set) var locations: [Location] {
get {
return _locations
} }
set {
let oldSelectedLocation = self.selectedLocation
private var selectedLocationIndex: Int? { if let selectedLocation = self.selectedLocation {
didSet { if let newSelectedIndex = newValue.firstIndex(where: { $0 == selectedLocation }) {
var oldLocation: Location? _selectedLocationIndex = newSelectedIndex
if let oldValue = oldValue, oldValue < locations.count { }
oldLocation = locations[oldValue] else {
_selectedLocationIndex = nil
} }
var newLocation: Location?
if let newValue = selectedLocationIndex, newValue < locations.count {
newLocation = locations[newValue]
} }
if oldLocation?.description != newLocation?.description { if _selectedLocationIndex == nil && !newValue.isEmpty {
log.info("Current location changed to: \(selectedLocation?.description ?? "nil")") _selectedLocationIndex = 0
} }
log.info("Location updated.") _locations = newValue
DispatchQueue.main.async { let selectedLocationChanged = (oldSelectedLocation != self.selectedLocation)
self.delegates.invoke { [weak self] (delegate) in handleLocationsChange(locationsChanged: true, selectedLocationChanged: selectedLocationChanged)
guard let self = self else { return }
delegate.locationManager(self, changedSelectedLocation: newLocation)
} }
} }
updateAllWeatherIfNeeded()
private var selectedLocationIndex: Int? {
get {
return _selectedLocationIndex
}
set {
_selectedLocationIndex = newValue
if newValue == nil && !locations.isEmpty {
_selectedLocationIndex = 0
}
handleLocationsChange(locationsChanged: false, selectedLocationChanged: true)
} }
} }
...@@ -95,8 +116,13 @@ public class LocationManager { ...@@ -95,8 +116,13 @@ public class LocationManager {
get { get {
guard let index = selectedLocationIndex else { guard let index = selectedLocationIndex else {
// TODO: don't do it this way, because we won't be able to tell that no location is currently selected! // TODO: don't do it this way, because we won't be able to tell that no location is currently selected!
if locations.count > 0 {
return locations.first
}
else {
return defaultLocation return defaultLocation
} }
}
guard index < locations.count else { guard index < locations.count else {
assertionFailure("This shouldn't happen. Got to investigate.") assertionFailure("This shouldn't happen. Got to investigate.")
// But in runtime we can handle it gracefully // But in runtime we can handle it gracefully
...@@ -137,6 +163,7 @@ public class LocationManager { ...@@ -137,6 +163,7 @@ public class LocationManager {
self.deviceLocationMonitor.delegate = self self.deviceLocationMonitor.delegate = self
storage.load { [weak self] (locations, selectedIndex, error) in storage.load { [weak self] (locations, selectedIndex, error) in
DispatchQueue.main.async {
guard let self = self else { return } guard let self = self else { return }
guard error == nil else { guard error == nil else {
self.log.error("Error while loading locations: \(error!)") self.log.error("Error while loading locations: \(error!)")
...@@ -146,8 +173,8 @@ public class LocationManager { ...@@ -146,8 +173,8 @@ public class LocationManager {
assertionFailure("Either error or locations have to be nil, not both") assertionFailure("Either error or locations have to be nil, not both")
return return
} }
self.locations = locations self.set(locations: locations, selectedIndex: selectedIndex)
self.selectedLocationIndex = selectedIndex }
} }
} }
......
...@@ -65,6 +65,16 @@ public struct Location { ...@@ -65,6 +65,16 @@ public struct Location {
public var cityId: String { public var cityId: String {
return "\(self.countryCode ?? ""):\(self.region ?? ""):\(self.cityName ?? "")" return "\(self.countryCode ?? ""):\(self.region ?? ""):\(self.cityName ?? "")"
} }
public func equals(to other: Location, onlyCompareLocationInfo: Bool) -> Bool {
guard !onlyCompareLocationInfo else {
return self == other
}
guard self == other else { return false }
guard self.lastWeatherUpdateDate == other.lastWeatherUpdateDate else { return false }
guard self.health?.lastUpdateTime == other.health?.lastUpdateTime else { return false }
return true
}
} }
extension Location: Equatable, Hashable { extension Location: Equatable, Hashable {
......
...@@ -9,6 +9,7 @@ import Foundation ...@@ -9,6 +9,7 @@ import Foundation
import CoreData import CoreData
public class CoreDataStorage: Storage { public class CoreDataStorage: Storage {
private var lastSavedAppData: AppData? = nil
private let log = Logger(componentName: "CoreDataStorage 💾") private let log = Logger(componentName: "CoreDataStorage 💾")
private lazy var managedContext: NSManagedObjectContext? = { private lazy var managedContext: NSManagedObjectContext? = {
persistentContainer.newBackgroundContext() persistentContainer.newBackgroundContext()
...@@ -27,15 +28,20 @@ public class CoreDataStorage: Storage { ...@@ -27,15 +28,20 @@ public class CoreDataStorage: Storage {
log.info("Save: start") log.info("Save: start")
managedContext?.perform { [weak self] in managedContext?.perform { [weak self] in
guard let self = self else { return } guard let self = self else { return }
let appData = AppData(selectedIndex: selectedIndex, locations: locations)
guard appData != self.lastSavedAppData else {
self.log.info("Save: no changes, skip")
return
}
guard let context = self.managedContext else { guard let context = self.managedContext else {
return return
} }
do { do {
try self.deleteAll(in: context) try self.deleteAll(in: context)
let appData = AppData(selectedIndex: selectedIndex, locations: locations)
if let coreAppData = try CoreAppData(context: context, appModel: appData) { if let coreAppData = try CoreAppData(context: context, appModel: appData) {
context.insert(coreAppData) context.insert(coreAppData)
try self.save(context: context) try self.save(context: context)
self.lastSavedAppData = appData
} }
self.log.info("Save: success") self.log.info("Save: success")
} }
...@@ -52,31 +58,30 @@ public class CoreDataStorage: Storage { ...@@ -52,31 +58,30 @@ public class CoreDataStorage: Storage {
guard let context = self.managedContext else { guard let context = self.managedContext else {
return return
} }
let completionOnMain: StorageCompletion = { [weak self] (locations, selectedIndex, error) in let completionWithErrorHandling: StorageCompletion = { [weak self] (locations, selectedIndex, error) in
if error != nil { if error != nil {
self?.log.error("Load: error.") self?.log.error("Load: error.")
} }
else { else {
self?.log.info("Load: success. \(String(describing: locations?.count)) locations, selected: \(String(describing: selectedIndex))") self?.log.info("Load: success. \(String(describing: locations?.count)) locations, selected: \(String(describing: selectedIndex))")
} }
DispatchQueue.main.async {
completion(locations, selectedIndex, error) completion(locations, selectedIndex, error)
} }
}
do { do {
let fetchRequest: NSFetchRequest<CoreAppData> = CoreAppData.fetchRequest() let fetchRequest: NSFetchRequest<CoreAppData> = CoreAppData.fetchRequest()
fetchRequest.fetchLimit = 1 fetchRequest.fetchLimit = 1
let results = try context.fetch(fetchRequest) let results = try context.fetch(fetchRequest)
guard let coreAppData = results.first else { guard let coreAppData = results.first else {
completionOnMain([], nil, nil) completionWithErrorHandling([], nil, nil)
return return
} }
let appData: AppData = try coreAppData.toAppModel() let appData: AppData = try coreAppData.toAppModel()
completionOnMain(appData.locations, appData.selectedIndex, nil) self.lastSavedAppData = appData
completionWithErrorHandling(appData.locations, appData.selectedIndex, nil)
} }
catch { catch {
self.log.error("Error during load: \(error)") self.log.error("Error during load: \(error)")
completionOnMain(nil, nil, error) completionWithErrorHandling(nil, nil, error)
} }
} }
} }
......
...@@ -9,7 +9,19 @@ import Foundation ...@@ -9,7 +9,19 @@ import Foundation
/// A helper structure, so that we could work with CoreAppData the same way we work with everything else. /// A helper structure, so that we could work with CoreAppData the same way we work with everything else.
public struct AppData { public struct AppData: Equatable {
public let selectedIndex: Int? public let selectedIndex: Int?
public let locations: [Location] public let locations: [Location]
public static func == (lhs: Self, rhs: Self) -> Bool {
guard lhs.selectedIndex == rhs.selectedIndex else { return false }
guard lhs.locations.count == rhs.locations.count else { return false }
for (i, location) in lhs.locations.enumerated() {
let otherLocation = rhs.locations[i]
guard location.equals(to: otherLocation, onlyCompareLocationInfo: false) else {
return false
}
}
return true
}
} }
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