Commit ab074700 by Demid Merzlyakov

Search: LocationsViewModel updated.

parent c49077b0
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import CoreLocation
public protocol LocationManagerDelegate: class { public protocol LocationManagerDelegate: class {
func locationManager(_ locationManager: LocationManager, changedCurrentLocation newLocation: Location?) func locationManager(_ locationManager: LocationManager, changedCurrentLocation newLocation: Location?)
...@@ -99,7 +100,8 @@ public class LocationManager { ...@@ -99,7 +100,8 @@ public class LocationManager {
} }
// TODO: update weather for all locations // TODO: update weather for all locations
public func add(location: Location) { public func addOrSelect(location: Location) {
// If the location is partially incomplete (e.g. coordinates are missing, or country / city / region), then some of this information will be returned by the weather update, but we may need to consider reverse geocoding here as well, if needed.
if locations.count >= maxLocationsCount { if locations.count >= maxLocationsCount {
log.warning("Adding new location, although the location limit is exceeded. New total: \(locations.count + 1)") log.warning("Adding new location, although the location limit is exceeded. New total: \(locations.count + 1)")
// This may happen if a location is added from a push notification. // This may happen if a location is added from a push notification.
...@@ -115,9 +117,26 @@ public class LocationManager { ...@@ -115,9 +117,26 @@ public class LocationManager {
// Or not? Should ViewModels handle it? // Or not? Should ViewModels handle it?
} }
public func remove(location: Location) { public func addOrSelect(partialLocation: PartialLocation) {
if let location = partialLocation as? Location {
addOrSelect(location: location)
}
else {
makeLocation(from: partialLocation) { (location) in
DispatchQueue.main.async { [weak self] in
if let location = location {
self?.addOrSelect(location: location)
}
}
}
}
}
public func remove(location: Location) -> Bool {
var result = false
if let index = locations.firstIndex(of: location) { if let index = locations.firstIndex(of: location) {
locations.remove(at: index) locations.remove(at: index)
result = true
} }
else { else {
log.warning("Couldn't remove \(location), because we couldn't find index.") log.warning("Couldn't remove \(location), because we couldn't find index.")
...@@ -125,10 +144,9 @@ public class LocationManager { ...@@ -125,10 +144,9 @@ public class LocationManager {
if currentLocation == location { if currentLocation == location {
currentLocation = locations.first currentLocation = locations.first
} }
return result
} }
// MARK: Delegates management
public func add(delegate: LocationManagerDelegate) { public func add(delegate: LocationManagerDelegate) {
delegates.add(delegate: delegate) delegates.add(delegate: delegate)
} }
...@@ -136,4 +154,54 @@ public class LocationManager { ...@@ -136,4 +154,54 @@ public class LocationManager {
public func remove(delegate: LocationManagerDelegate) { public func remove(delegate: LocationManagerDelegate) {
delegates.remove(delegate: delegate) delegates.remove(delegate: delegate)
} }
private func makeLocation(from partialLocation: PartialLocation, completion: @escaping (Location?) -> ()) {
guard let latStr = partialLocation.lat, let lonStr = partialLocation.lon, let lat = CLLocationDegrees(latStr), let lon = CLLocationDegrees(lonStr) else {
log.error("Geo lookup: no coordinates present: \(partialLocation)")
completion(nil)
return
}
let location = CLLocation(latitude: lat, longitude: lon)
let geocodeCompletion: CLGeocodeCompletionHandler = { [weak self] (placemarks, error) in
guard let self = self else { return }
var result: Location? = nil
defer {
completion(result)
}
guard error == nil else {
self.log.error("Geo lookup failed: \(error!)")
return
}
guard let placemark: CLPlacemark = placemarks?.first else {
self.log.warning("Geo lookup didn't get a result.")
return
}
guard let timeZone = placemark.timeZone else {
self.log.error("Geo lookup couldn't determine time zone for (\(latStr); \(lonStr)).")
return
}
//TODO: come up with something for the date
result = Location(lastTimeUpdated: Date(timeIntervalSince1970: 0), timeZone: timeZone)
result?.coordinates = CLLocationCoordinate2D(latitude: lat, longitude: lon)
result?.cityName = partialLocation.cityName ?? placemark.locality
result?.countryName = partialLocation.countryName ?? placemark.country
result?.countryCode = placemark.isoCountryCode
result?.region = partialLocation.region ?? placemark.administrativeArea
result?.zip = placemark.postalCode
self.log.info("Geo lookup got location: \(result?.description ?? "-")")
}
let geocoder = CLGeocoder()
if #available(iOS 11, *) {
geocoder.reverseGeocodeLocation(location, completionHandler: geocodeCompletion)
}
else {
geocoder.reverseGeocodeLocation(location, completionHandler: geocodeCompletion)
}
}
} }
...@@ -87,3 +87,6 @@ ...@@ -87,3 +87,6 @@
"tabBar.radar" = "radar"; "tabBar.radar" = "radar";
"tabBar.menu" = "menu"; "tabBar.menu" = "menu";
// Search
"search.error.maxLocationWarning" = "To keep your 1Weather running in tip-top shape, please limit the number of locations to %@";
"search.error.deleteError" = "Failed to delete location.";
...@@ -8,33 +8,33 @@ ...@@ -8,33 +8,33 @@
import UIKit import UIKit
import AlgoliaSearchClient import AlgoliaSearchClient
protocol LocationsViewModelDelegate:class { public protocol LocationsViewModelDelegate:class {
func viewModelDidChange(model:LocationsViewModel) func viewModelDidChange(model:LocationsViewModel)
func viewModelDisplayModeDidChange(model: LocationsViewModel) func viewModelDisplayModeDidChange(model: LocationsViewModel)
func viewModelError(model:LocationsViewModel, title:String?, message:String?) func viewModelError(model:LocationsViewModel, title:String?, message:String?)
func viewModelDidSelectCity(model:LocationsViewModel, city: WdtLocation) func viewModelDidSelectCity(model:LocationsViewModel, city: Location)
} }
enum LocationsViewModelDisplayMode { public enum LocationsViewModelDisplayMode {
case savedCities case savedCities
case popularCities case popularCities
case searchResults case searchResults
} }
class LocationsViewModel { public class LocationsViewModel {
//Public //Public
weak var delegate:LocationsViewModelDelegate? public weak var delegate: LocationsViewModelDelegate?
var cities:[WdtLocation] { public var cities: [PartialLocation] {
switch self.displayMode { switch self.displayMode {
case .savedCities: case .savedCities:
return WeatherUpdateManager.shared.allWdtLocations ?? [WdtLocation]() return locationManager.locations
case .popularCities: case .popularCities:
return self.popularCities return self.popularCities
case .searchResults: case .searchResults:
return self.fetchedCities return self.fetchedCities
} }
} }
var displayMode:LocationsViewModelDisplayMode = .savedCities { public var displayMode:LocationsViewModelDisplayMode = .savedCities {
didSet { didSet {
if oldValue == .searchResults { if oldValue == .searchResults {
clean() clean()
...@@ -44,15 +44,22 @@ class LocationsViewModel { ...@@ -44,15 +44,22 @@ class LocationsViewModel {
} }
} }
public init() {
locationManager.add(delegate: self)
}
//Private //Private
private static let maxLocationsCount = 12
private let log = Logger(componentName: "LocationsViewModel")
private let locationManager = LocationManager.shared
private let popularCitiesManager = PopularCitiesManager.shared private let popularCitiesManager = PopularCitiesManager.shared
private var popularCities = [WdtLocation]() { private var popularCities = [GeoNamesPlace]() {
didSet { didSet {
assert(Thread.isMainThread) assert(Thread.isMainThread)
self.delegate?.viewModelDidChange(model: self) self.delegate?.viewModelDidChange(model: self)
} }
} }
private var fetchedCities = [WdtLocation]() { private var fetchedCities = [GeoNamesPlace]() {
didSet { didSet {
assert(Thread.isMainThread) assert(Thread.isMainThread)
self.delegate?.viewModelDidChange(model: self) self.delegate?.viewModelDidChange(model: self)
...@@ -66,16 +73,17 @@ class LocationsViewModel { ...@@ -66,16 +73,17 @@ class LocationsViewModel {
func fetchPopularCities() { func fetchPopularCities() {
self.popularCitiesManager.fetchPopularCities {[weak self] result in self.popularCitiesManager.fetchPopularCities {[weak self] result in
guard let strongSelf = self else { return } guard let self = self else { return }
switch result { switch result {
case .success(let popularCities): case .success(let popularCities):
DispatchQueue.main.async { DispatchQueue.main.async {
strongSelf.popularCities = popularCities.map{return WdtLocation(place: $0)} self.popularCities = popularCities
} }
case .failure(let error): case .failure(let error):
self.log.error("Couldn't get popular cities: \(error)")
DispatchQueue.main.async { DispatchQueue.main.async {
strongSelf.popularCities = [] self.popularCities = []
} }
} }
} }
...@@ -94,10 +102,9 @@ class LocationsViewModel { ...@@ -94,10 +102,9 @@ class LocationsViewModel {
fakePlace.countryCode = "US" fakePlace.countryCode = "US"
fakePlace.fcodeName = "populated place" fakePlace.fcodeName = "populated place"
fakePlace.toponymName = "1WVille" fakePlace.toponymName = "1WVille"
let fakeWdtLocation = WdtLocation(place: fakePlace)
DispatchQueue.main.async { DispatchQueue.main.async {
self.fetchedCities = [fakeWdtLocation] self.fetchedCities = [fakePlace]
} }
return return
} }
...@@ -158,7 +165,7 @@ class LocationsViewModel { ...@@ -158,7 +165,7 @@ class LocationsViewModel {
filteredPlaces.append(place) filteredPlaces.append(place)
} }
DispatchQueue.main.async { DispatchQueue.main.async {
strongSelf.fetchedCities = filteredPlaces.map{return WdtLocation(place: $0)} strongSelf.fetchedCities = filteredPlaces
} }
break break
case .failure(let error): case .failure(let error):
...@@ -171,40 +178,50 @@ class LocationsViewModel { ...@@ -171,40 +178,50 @@ class LocationsViewModel {
} }
//MARK:- City CUD methods //MARK:- City CUD methods
func add(city:WdtLocation) { func add(city: PartialLocation) {
if WeatherUpdateManager.shared.allLocationsCount >= 12 { guard locationManager.locations.count < locationManager.maxLocationsCount else {
self.delegate?.viewModelError(model: self, title: nil, message: "To keep your 1Weather running in tip-top shape, please limit the number of locations to 12.".localized) self.delegate?.viewModelError(model: self, title: nil, message: "search.error.maxLocationWarning".localized())
return return
} }
locationManager.addOrSelect(partialLocation: city)
if WeatherUpdateManager.shared.addLocation(city) {
WeatherUpdateManager.shared.selectedWdtLocation = city
self.displayMode = .savedCities
}
else {
self.delegate?.viewModelDidChange(model: self)
}
self.delegate?.viewModelDidSelectCity(model: self, city: city)
} }
func delete(city:WdtLocation) { func delete(city: PartialLocation) {
if WeatherUpdateManager.shared.deleteLocation(city) { guard let location = city as? Location else {
self.delegate?.viewModelDidChange(model: self) log.warning("Attempted to delete partial location that can't be casted to Location. Ignoring: \(city)")
} }
else { if !locationManager.remove(location: location) {
self.delegate?.viewModelError(model: self, title: "Error", message: "Failed to delete location") self.delegate?.viewModelError(model: self, title: "Error", message: "search.error.deleteError".localized())
} }
} }
func select(city:WdtLocation) { func select(city: PartialLocation) {
WeatherUpdateManager.shared.selectedWdtLocation = city add(city: city)
self.delegate?.viewModelDidChange(model: self)
self.delegate?.viewModelDidSelectCity(model: self, city: city)
} }
func useCurrentLocation(requestedBy viewController:UIViewController) { func useCurrentLocation(requestedBy viewController:UIViewController) {
UserLocationManager.shared.useCurrentLocation(requestedFrom: viewController) { (_ result: UserLocationManager.LocationRequestResult) in #warning("Not implemented!")
// do nothing //TODO: implement
// UserLocationManager.shared.useCurrentLocation(requestedFrom: viewController) { (_ result: UserLocationManager.LocationRequestResult) in
// // do nothing
// }
}
}
extension LocationsViewModel: LocationManagerDelegate {
public func locationManager(_ locationManager: LocationManager, changedCurrentLocation newLocation: Location?) {
DispatchQueue.main.async {
self.delegate?.viewModelDidChange(model: self)
if let location = newLocation {
self.delegate?.viewModelDidSelectCity(model: self, city: location)
}
}
}
public func locationManager(_ locationManager: LocationManager, updatedLocationsList newList: [Location]) {
self.delegate?.viewModelDidChange(model: self)
DispatchQueue.main.async {
self.delegate?.viewModelDidChange(model: self)
} }
} }
} }
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