Commit 571c527f by Demid Merzlyakov

Migrate cities.

parent 969312b3
......@@ -16,6 +16,8 @@
87D81582262EFC9B0015A6D1 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D81581262EFC9A0015A6D1 /* Notifications.swift */; };
87D815AA2636D5E60015A6D1 /* NWSAlertCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D815A92636D5E60015A6D1 /* NWSAlertCoordinator.swift */; };
87D815AC2636D61D0015A6D1 /* NWSAlertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D815AB2636D61D0015A6D1 /* NWSAlertViewModel.swift */; };
87DE8C81263BFBCE00E1C8D4 /* LegacyMigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87DE8C80263BFBCE00E1C8D4 /* LegacyMigrationManager.swift */; };
87DE8CB2263C09BA00E1C8D4 /* LegacyWdtLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87DE8CB1263C09BA00E1C8D4 /* LegacyWdtLocation.swift */; };
C27F92C189A9C9E637AF6C3A /* Pods_OneWeatherNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 871EA87D239E6F89F6F8818E /* Pods_OneWeatherNotificationServiceExtension.framework */; };
CD1237C3255D5C5900C98139 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1237C2255D5C5900C98139 /* AppDelegate.swift */; };
CD1237CC255D5C5C00C98139 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CD1237CB255D5C5C00C98139 /* Assets.xcassets */; };
......@@ -333,6 +335,8 @@
87D815AE2636E6BF0015A6D1 /* NWSForecastOfficeTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWSForecastOfficeTableViewCell.swift; sourceTree = "<group>"; };
87D815B02636ED850015A6D1 /* NWSAlertCellFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWSAlertCellFactory.swift; sourceTree = "<group>"; };
87D815B22636F2040015A6D1 /* NWSAlertInfoBlockTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWSAlertInfoBlockTableViewCell.swift; sourceTree = "<group>"; };
87DE8C80263BFBCE00E1C8D4 /* LegacyMigrationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyMigrationManager.swift; sourceTree = "<group>"; };
87DE8CB1263C09BA00E1C8D4 /* LegacyWdtLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyWdtLocation.swift; sourceTree = "<group>"; };
C8C576F6184B547435CFF0F3 /* Pods-1Weather.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-1Weather.debug.xcconfig"; path = "Target Support Files/Pods-1Weather/Pods-1Weather.debug.xcconfig"; sourceTree = "<group>"; };
CD1237BF255D5C5900C98139 /* 1Weather.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = 1Weather.app; sourceTree = BUILT_PRODUCTS_DIR; };
CD1237C2255D5C5900C98139 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
......@@ -1578,6 +1582,8 @@
CD16A179262D81880028E74A /* UserDefaultsWrapper.swift */,
CEFB8577261750DF00C5CDD2 /* CoreData */,
CEE0A178262FA9650044C257 /* DelayedSaveStorage.swift */,
87DE8C80263BFBCE00E1C8D4 /* LegacyMigrationManager.swift */,
87DE8CB1263C09BA00E1C8D4 /* LegacyWdtLocation.swift */,
);
path = Storage;
sourceTree = "<group>";
......@@ -1944,6 +1950,7 @@
CD32CE0E260C770E00235081 /* MenuHeaderView.swift in Sources */,
CD15DB3D25DA6C5100024727 /* ForecastTimePeriodControl.swift in Sources */,
CD67617726259DD70079D273 /* MapLayersPresentationAnimator.swift in Sources */,
87DE8CB2263C09BA00E1C8D4 /* LegacyWdtLocation.swift in Sources */,
CD1237F1255D83C500C98139 /* UIColor+Hex.swift in Sources */,
CD7BF1582620410800A30DF5 /* RadarCoordinator.swift in Sources */,
CDD0F1EE25725BCF00CF5017 /* ThemeManager.swift in Sources */,
......@@ -1962,6 +1969,7 @@
CD9B6B1425DBCDE2001D9B80 /* GraphView.swift in Sources */,
CD39F2EE25DE858D009FE398 /* NotificationName+Localization.swift in Sources */,
CEAFF08F25DFC6ED00DF4EBF /* HourlyWeather.swift in Sources */,
87DE8C81263BFBCE00E1C8D4 /* LegacyMigrationManager.swift in Sources */,
87C1721025FF874B00DA3464 /* PartialLocation.swift in Sources */,
CD866A6F260F67F200E96A5C /* SettingsDetailsViewModel.swift in Sources */,
CD71709025FA317700A63C27 /* ForecastTimePeriodView.swift in Sources */,
......
......@@ -63,6 +63,14 @@ internal class DeviceLocationMonitor: NSObject {
}
public struct DeviceLocation: PartialLocation {
public var nickname: String? {
return nil
}
public var selected: Bool? {
return nil
}
private static let coordinatesStringAccuracy = 6 // 6 digits after the dot
private let location: CLLocation?
fileprivate init(location: CLLocation?) {
......
......@@ -26,6 +26,7 @@ public class LocationManager {
public let nwsAlertsManager: NWSAlertsManager
private let pushNotificationsManager: PushNotificationsManager
private let storage: Storage
private let legacyMigrator = LegacyMigrationManager()
private var defaultLocation = Location(deviceLocation: false,
coordinates: .init(latitude: 37.3230, longitude: -122.0322), // Cupertino
timeZone: TimeZone(abbreviation: "PST")!) {
......@@ -204,7 +205,7 @@ public class LocationManager {
self.nwsAlertsManager.delegates.add(delegate: self)
storage.load { [weak self] (locations, selectedIndex, error) in
let storageLoadingCompletion: StorageCompletion = { [weak self] (locations, selectedIndex, error) in
onMain {
guard let self = self else { return }
defer {
......@@ -234,6 +235,34 @@ public class LocationManager {
self.deviceLocationMonitor.startLocationUpdatesIfPossible()
}
}
storage.load { (locations, selectedIndex, error) in
if locations?.count ?? 0 == 0 && self.legacyMigrator.migrationNeeded {
self.legacyMigrator.loadLegacyLocations { [weak self] (legacyLocations, migrationError) in
guard let self = self else { return }
guard migrationError == nil else {
storageLoadingCompletion(locations, selectedIndex, error)
return
}
guard let legacyLocations = legacyLocations else {
storageLoadingCompletion(locations, selectedIndex, error)
fatalError("Should never happen. Either legacyLocations or migrationError must be not nil")
return
}
for location: PartialLocation in legacyLocations {
self.addIfNeeded(partialLocation: location, selectLocation: location.selected ?? false)
}
self.loadedLocations = true
self.actionAfterLocationLoad?(self.locations)
self.deviceLocationMonitor.startLocationUpdatesIfPossible()
}
}
else {
storageLoadingCompletion(locations, selectedIndex, error)
}
}
}
public func updateEverythingIfNeeded() {
......
......@@ -21,6 +21,7 @@ final class GeoNamesPlace: NSObject {
var toponymName : String? // airport name if fcodeName is airport
var fipsCode: String?
var optionalCityId: String?
var nickname: String?
func detailName() -> String {
var sb = String()
......@@ -90,6 +91,7 @@ extension GeoNamesPlace: Codable {
case countryCode
case fcodeName
case toponymName
case nickname
}
}
......@@ -98,6 +100,11 @@ extension GeoNamesPlace: PartialLocation {
return false
}
var selected: Bool? {
return nil
}
var nameForDisplay: String {
//TODO: refactor this
var sb = String()
......
......@@ -186,6 +186,10 @@ extension Location: PartialLocation {
return String(format: "%.6f", value)
}
public var selected: Bool? {
return nil
}
public var nameForDisplay: String {
get {
//TODO: refactor this
......
......@@ -17,6 +17,8 @@ public protocol PartialLocation {
var cityName: String? { get }
var fipsCode: String? { get }
var optionalCityId: String? { get }
var nickname: String? { get }
var selected: Bool? { get }
var nameForDisplay: String { get }
}
//
// LegacyMigrationManager.swift
// 1Weather
//
// Created by Demid Merzlyakov on 30.04.2021.
//
import Foundation
class LegacyMigrationManager {
typealias Completion = ([PartialLocation]?, MigrationError?) -> ()
private enum MigrationResult: String {
case notNeeded = "not needed"
case success = "success"
case failure = "failure"
}
public enum MigrationError: Error {
case notNeeded
case dataLoadingError
}
private let lastMigrationResultKey = "LegacyMigrationManager.lastMigrationResult"
private let userLocationsKey = "user.locations"
private let log = Logger(componentName: "LegacyMigrationManager")
private let internalQueue: OperationQueue = {
let queue = OperationQueue()
queue.name = "Migration Queue"
queue.maxConcurrentOperationCount = 1
return queue
}()
private struct ConvertedLocation: PartialLocation {
var selected: Bool?
var deviceLocation: Bool
var lat: String?
var lon: String?
var countryName: String?
var region: String?
var cityName: String?
var fipsCode: String?
let optionalCityId: String? = nil
var nickname: String?
let nameForDisplay: String = ""
}
private var lastMigrationResult: MigrationResult? {
get {
if let saved = UserDefaults.standard.string(forKey: lastMigrationResultKey) {
return MigrationResult(rawValue: saved)
}
return nil
}
set {
UserDefaults.standard.set(newValue?.rawValue, forKey: lastMigrationResultKey)
}
}
private func oneWeatherUserDefaults() -> UserDefaults {
if let userDefault = UserDefaults(suiteName: "group.com.onelouder.oneweather") {
return userDefault
}
return UserDefaults.standard
}
private var legacyDataAvailable: Bool {
self.oneWeatherUserDefaults().data(forKey: userLocationsKey) != nil
}
/// Call after successful migration, and also after successfully loading existing data from storage
public func clearLegacyData() {
self.lastMigrationResult = .notNeeded
oneWeatherUserDefaults().setValue(nil, forKey: userLocationsKey)
}
public var migrationNeeded: Bool {
guard legacyDataAvailable else {
return false
}
if let lastMigrationResult = self.lastMigrationResult {
return lastMigrationResult == .failure
}
return true
}
func loadLegacyLocations(completion: @escaping Completion) {
internalQueue.addOperation { [weak self] in
guard let self = self else { return }
self.log.info("Start loading")
guard let pathsData = self.oneWeatherUserDefaults().data(forKey: self.userLocationsKey) else {
self.lastMigrationResult = .notNeeded
self.log.info("not needed")
completion(nil, .notNeeded)
return
}
NSKeyedUnarchiver.setClass(LegacyWdtLocation.self, forClassName: "OneWeather.WdtLocation")
guard let paths = NSKeyedUnarchiver.unarchiveObject(with: pathsData) as? [String] else {
self.log.error("Couldn't parse paths")
self.lastMigrationResult = .notNeeded
completion(nil, .dataLoadingError)
return
}
var legacyLocations = [LegacyWdtLocation]()
for path in paths {
if let loc = LegacyWdtLocation.load(path) {
legacyLocations.append(loc)
} else {
self.log.warning("Failed to parse location at \(path)")
}
}
if legacyLocations.isEmpty && !paths.isEmpty {
// failed to parse all the locations
self.lastMigrationResult = .failure
self.log.error("Couldn't parse any of the locations.")
completion(nil, .dataLoadingError)
return
}
var result = [ConvertedLocation]()
for legacyLocation in legacyLocations {
var converted = ConvertedLocation(deviceLocation: legacyLocation.myLocation)
converted.lat = String(format: "%.8f", legacyLocation.geoPointLat)
converted.lon = String(format: "%.8f", legacyLocation.geoPointLong)
converted.countryName = legacyLocation.countryName
converted.region = legacyLocation.region
converted.cityName = legacyLocation.city
converted.fipsCode = legacyLocation.fips
converted.selected = legacyLocation.selectedLocation
result.append(converted)
}
self.lastMigrationResult = .success
completion(result, nil)
}
}
}
//
// LegacyWdtLocation.swift
// 1Weather
//
// Created by Demid Merzlyakov on 30.04.2021.
//
import Foundation
//@objc(LegacyWdtLocation)
class LegacyWdtLocation: NSObject, NSCoding {
static let LOCATION = "location"
var city: String?
var region: String? // aka state
var country: String? // actually countryCode (i.e US)
var countryName: String? // actually country (i.e United States)
var zip: String?
fileprivate var _nickName: String?
var myLocation = false
var selectedLocation = false
var geoPointLat: Double = -1
var geoPointLong: Double = -1
var fips: String?
class var documentsFolder: String {
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) as [String]
return paths[0]
}
class func load(_ directory: String, path: String, key: String) -> AnyObject? {
var fullPath = path + "/" + key
if directory.hasSuffix("/") {
fullPath = directory.appending(fullPath)
} else {
fullPath = directory.appendingFormat("/%@", fullPath)
}
return NSKeyedUnarchiver.unarchiveObject(withFile: fullPath) as AnyObject?
}
// base weather class data is saved in Documents directory so it doesn't get deleted by the system
class func load(_ path: String) -> LegacyWdtLocation? {
return LegacyWdtLocation.load(LegacyWdtLocation.documentsFolder, path: path, key: LegacyWdtLocation.LOCATION) as? LegacyWdtLocation
}
required init(coder decoder: NSCoder) {
self.city = decoder.decodeObject(forKey: "city") as? String
self._nickName = decoder.decodeObject(forKey: "nickName") as? String
self.country = decoder.decodeObject(forKey: "country") as? String
self.countryName = decoder.decodeObject(forKey: "countryName") as? String
self.region = decoder.decodeObject(forKey: "region") as? String
self.geoPointLat = decoder.decodeDouble(forKey: "geoPointLat")
self.geoPointLong = decoder.decodeDouble(forKey: "geoPointLong")
self.myLocation = decoder.decodeBool(forKey: "myLocation")
self.selectedLocation = decoder.decodeBool(forKey: "selectedLocation")
self.fips = decoder.decodeObject(forKey: "fips") as? String
}
func encode(with coder: NSCoder) {
coder.encodeCInt(1, forKey: "version")
coder.encode(self.city, forKey: "city")
coder.encode(self._nickName, forKey: "nickName")
coder.encode(self.country, forKey: "country")
coder.encode(self.countryName, forKey: "countryName")
coder.encode(self.region, forKey: "region")
coder.encode(self.geoPointLat, forKey: "geoPointLat")
coder.encode(self.geoPointLong, forKey: "geoPointLong")
coder.encode(self.myLocation, forKey: "myLocation")
coder.encode(self.selectedLocation, forKey: "selectedLocation")
coder.encode(self.fips, forKey: "fips")
}
}
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