Commit 8e13f07c by Demid Merzlyakov

Storage: optimized saving.

parent 2cfcb142
......@@ -45,49 +45,70 @@ public class LocationManager {
public typealias CurrentLocationCompletion = (LocationRequestResult) -> ()
public private(set) var locations = [Location]() {
didSet {
private var _locations = [Location]()
private var _selectedLocationIndex: Int?
private func set(locations: [Location], selectedIndex: Int?) {
DispatchQueue.main.async { [weak self] in
self?._locations = locations
self?._selectedLocationIndex = selectedIndex
self?.handleLocationsChange(locationsChanged: true, selectedLocationChanged: true)
}
}
private func handleLocationsChange(locationsChanged: Bool, selectedLocationChanged: Bool) {
if locationsChanged {
log.info("Locations list updated: \(locations.map { $0.description }.joined(separator: ", "))")
let newValue = locations
if let selectedIndex = self.selectedLocationIndex, selectedIndex >= newValue.count {
self.selectedLocationIndex = newValue.count > 0 ? 0 : nil
}
else {
updateAllWeatherIfNeeded() //TODO: the whole flow is not optimal.
self.delegates.invoke { (delegate) in
delegate.locationManager(self, changedSelectedLocation: self.selectedLocation)
}
}
self.delegates.invoke { [weak self] (delegate) in
guard let self = self else { return }
delegate.locationManager(self, updatedLocationsList: self.locations)
}
storage.save(locations: locations, selectedIndex: selectedLocationIndex)
}
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)
updateAllWeatherIfNeeded()
}
private var selectedLocationIndex: Int? {
didSet {
var oldLocation: Location?
if let oldValue = oldValue, oldValue < locations.count {
oldLocation = locations[oldValue]
}
var newLocation: Location?
if let newValue = selectedLocationIndex, newValue < locations.count {
newLocation = locations[newValue]
public private(set) var locations: [Location] {
get {
return _locations
}
set {
let oldSelectedLocation = self.selectedLocation
if let selectedLocation = self.selectedLocation {
if let newSelectedIndex = newValue.firstIndex(where: { $0 == selectedLocation }) {
_selectedLocationIndex = newSelectedIndex
}
else {
_selectedLocationIndex = nil
}
}
if oldLocation?.description != newLocation?.description {
log.info("Current location changed to: \(selectedLocation?.description ?? "nil")")
if _selectedLocationIndex == nil && !newValue.isEmpty {
_selectedLocationIndex = 0
}
log.info("Location updated.")
DispatchQueue.main.async {
self.delegates.invoke { [weak self] (delegate) in
guard let self = self else { return }
delegate.locationManager(self, changedSelectedLocation: newLocation)
}
_locations = newValue
let selectedLocationChanged = (oldSelectedLocation != self.selectedLocation)
handleLocationsChange(locationsChanged: true, selectedLocationChanged: selectedLocationChanged)
}
}
private var selectedLocationIndex: Int? {
get {
return _selectedLocationIndex
}
set {
_selectedLocationIndex = newValue
if newValue == nil && !locations.isEmpty {
_selectedLocationIndex = 0
}
updateAllWeatherIfNeeded()
handleLocationsChange(locationsChanged: false, selectedLocationChanged: true)
}
}
......@@ -95,7 +116,12 @@ public class LocationManager {
get {
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!
return defaultLocation
if locations.count > 0 {
return locations.first
}
else {
return defaultLocation
}
}
guard index < locations.count else {
assertionFailure("This shouldn't happen. Got to investigate.")
......@@ -137,17 +163,18 @@ public class LocationManager {
self.deviceLocationMonitor.delegate = self
storage.load { [weak self] (locations, selectedIndex, error) in
guard let self = self else { return }
guard error == nil else {
self.log.error("Error while loading locations: \(error!)")
return
}
guard let locations = locations else {
assertionFailure("Either error or locations have to be nil, not both")
return
DispatchQueue.main.async {
guard let self = self else { return }
guard error == nil else {
self.log.error("Error while loading locations: \(error!)")
return
}
guard let locations = locations else {
assertionFailure("Either error or locations have to be nil, not both")
return
}
self.set(locations: locations, selectedIndex: selectedIndex)
}
self.locations = locations
self.selectedLocationIndex = selectedIndex
}
}
......
......@@ -65,6 +65,16 @@ public struct Location {
public var cityId: String {
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 {
......
......@@ -9,6 +9,7 @@ import Foundation
import CoreData
public class CoreDataStorage: Storage {
private var lastSavedAppData: AppData? = nil
private let log = Logger(componentName: "CoreDataStorage 💾")
private lazy var managedContext: NSManagedObjectContext? = {
persistentContainer.newBackgroundContext()
......@@ -27,15 +28,20 @@ public class CoreDataStorage: Storage {
log.info("Save: start")
managedContext?.perform { [weak self] in
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 {
return
}
do {
try self.deleteAll(in: context)
let appData = AppData(selectedIndex: selectedIndex, locations: locations)
if let coreAppData = try CoreAppData(context: context, appModel: appData) {
context.insert(coreAppData)
try self.save(context: context)
self.lastSavedAppData = appData
}
self.log.info("Save: success")
}
......@@ -52,31 +58,30 @@ public class CoreDataStorage: Storage {
guard let context = self.managedContext else {
return
}
let completionOnMain: StorageCompletion = { [weak self] (locations, selectedIndex, error) in
let completionWithErrorHandling: StorageCompletion = { [weak self] (locations, selectedIndex, error) in
if error != nil {
self?.log.error("Load: error.")
}
else {
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 {
let fetchRequest: NSFetchRequest<CoreAppData> = CoreAppData.fetchRequest()
fetchRequest.fetchLimit = 1
let results = try context.fetch(fetchRequest)
guard let coreAppData = results.first else {
completionOnMain([], nil, nil)
completionWithErrorHandling([], nil, nil)
return
}
let appData: AppData = try coreAppData.toAppModel()
completionOnMain(appData.locations, appData.selectedIndex, nil)
self.lastSavedAppData = appData
completionWithErrorHandling(appData.locations, appData.selectedIndex, nil)
}
catch {
self.log.error("Error during load: \(error)")
completionOnMain(nil, nil, error)
completionWithErrorHandling(nil, nil, error)
}
}
}
......
......@@ -9,7 +9,19 @@ import Foundation
/// 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 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