Commit ab074700 by Demid Merzlyakov

Search: LocationsViewModel updated.

parent c49077b0
......@@ -6,6 +6,7 @@
//
import Foundation
import CoreLocation
public protocol LocationManagerDelegate: class {
func locationManager(_ locationManager: LocationManager, changedCurrentLocation newLocation: Location?)
......@@ -99,7 +100,8 @@ public class LocationManager {
}
// 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 {
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.
......@@ -115,9 +117,26 @@ public class LocationManager {
// 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) {
locations.remove(at: index)
result = true
}
else {
log.warning("Couldn't remove \(location), because we couldn't find index.")
......@@ -125,10 +144,9 @@ public class LocationManager {
if currentLocation == location {
currentLocation = locations.first
}
return result
}
// MARK: Delegates management
public func add(delegate: LocationManagerDelegate) {
delegates.add(delegate: delegate)
}
......@@ -136,4 +154,54 @@ public class LocationManager {
public func remove(delegate: LocationManagerDelegate) {
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 @@
"tabBar.radar" = "radar";
"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 @@
import UIKit
import AlgoliaSearchClient
protocol LocationsViewModelDelegate:class {
public protocol LocationsViewModelDelegate:class {
func viewModelDidChange(model:LocationsViewModel)
func viewModelDisplayModeDidChange(model: LocationsViewModel)
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 popularCities
case searchResults
}
class LocationsViewModel {
public class LocationsViewModel {
//Public
weak var delegate:LocationsViewModelDelegate?
var cities:[WdtLocation] {
public weak var delegate: LocationsViewModelDelegate?
public var cities: [PartialLocation] {
switch self.displayMode {
case .savedCities:
return WeatherUpdateManager.shared.allWdtLocations ?? [WdtLocation]()
return locationManager.locations
case .popularCities:
return self.popularCities
case .searchResults:
return self.fetchedCities
}
}
var displayMode:LocationsViewModelDisplayMode = .savedCities {
public var displayMode:LocationsViewModelDisplayMode = .savedCities {
didSet {
if oldValue == .searchResults {
clean()
......@@ -44,15 +44,22 @@ class LocationsViewModel {
}
}
public init() {
locationManager.add(delegate: self)
}
//Private
private static let maxLocationsCount = 12
private let log = Logger(componentName: "LocationsViewModel")
private let locationManager = LocationManager.shared
private let popularCitiesManager = PopularCitiesManager.shared
private var popularCities = [WdtLocation]() {
private var popularCities = [GeoNamesPlace]() {
didSet {
assert(Thread.isMainThread)
self.delegate?.viewModelDidChange(model: self)
}
}
private var fetchedCities = [WdtLocation]() {
private var fetchedCities = [GeoNamesPlace]() {
didSet {
assert(Thread.isMainThread)
self.delegate?.viewModelDidChange(model: self)
......@@ -66,16 +73,17 @@ class LocationsViewModel {
func fetchPopularCities() {
self.popularCitiesManager.fetchPopularCities {[weak self] result in
guard let strongSelf = self else { return }
guard let self = self else { return }
switch result {
case .success(let popularCities):
DispatchQueue.main.async {
strongSelf.popularCities = popularCities.map{return WdtLocation(place: $0)}
self.popularCities = popularCities
}
case .failure(let error):
self.log.error("Couldn't get popular cities: \(error)")
DispatchQueue.main.async {
strongSelf.popularCities = []
self.popularCities = []
}
}
}
......@@ -94,10 +102,9 @@ class LocationsViewModel {
fakePlace.countryCode = "US"
fakePlace.fcodeName = "populated place"
fakePlace.toponymName = "1WVille"
let fakeWdtLocation = WdtLocation(place: fakePlace)
DispatchQueue.main.async {
self.fetchedCities = [fakeWdtLocation]
self.fetchedCities = [fakePlace]
}
return
}
......@@ -158,7 +165,7 @@ class LocationsViewModel {
filteredPlaces.append(place)
}
DispatchQueue.main.async {
strongSelf.fetchedCities = filteredPlaces.map{return WdtLocation(place: $0)}
strongSelf.fetchedCities = filteredPlaces
}
break
case .failure(let error):
......@@ -171,40 +178,50 @@ class LocationsViewModel {
}
//MARK:- City CUD methods
func add(city:WdtLocation) {
if WeatherUpdateManager.shared.allLocationsCount >= 12 {
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)
func add(city: PartialLocation) {
guard locationManager.locations.count < locationManager.maxLocationsCount else {
self.delegate?.viewModelError(model: self, title: nil, message: "search.error.maxLocationWarning".localized())
return
}
locationManager.addOrSelect(partialLocation: city)
}
if WeatherUpdateManager.shared.addLocation(city) {
WeatherUpdateManager.shared.selectedWdtLocation = city
self.displayMode = .savedCities
func delete(city: PartialLocation) {
guard let location = city as? Location else {
log.warning("Attempted to delete partial location that can't be casted to Location. Ignoring: \(city)")
}
else {
self.delegate?.viewModelDidChange(model: self)
if !locationManager.remove(location: location) {
self.delegate?.viewModelError(model: self, title: "Error", message: "search.error.deleteError".localized())
}
self.delegate?.viewModelDidSelectCity(model: self, city: city)
}
func delete(city:WdtLocation) {
if WeatherUpdateManager.shared.deleteLocation(city) {
self.delegate?.viewModelDidChange(model: self)
}
else {
self.delegate?.viewModelError(model: self, title: "Error", message: "Failed to delete location")
func select(city: PartialLocation) {
add(city: city)
}
func useCurrentLocation(requestedBy viewController:UIViewController) {
#warning("Not implemented!")
//TODO: implement
// UserLocationManager.shared.useCurrentLocation(requestedFrom: viewController) { (_ result: UserLocationManager.LocationRequestResult) in
// // do nothing
// }
}
}
func select(city:WdtLocation) {
WeatherUpdateManager.shared.selectedWdtLocation = city
extension LocationsViewModel: LocationManagerDelegate {
public func locationManager(_ locationManager: LocationManager, changedCurrentLocation newLocation: Location?) {
DispatchQueue.main.async {
self.delegate?.viewModelDidChange(model: self)
self.delegate?.viewModelDidSelectCity(model: self, city: city)
if let location = newLocation {
self.delegate?.viewModelDidSelectCity(model: self, city: location)
}
}
}
func useCurrentLocation(requestedBy viewController:UIViewController) {
UserLocationManager.shared.useCurrentLocation(requestedFrom: viewController) { (_ result: UserLocationManager.LocationRequestResult) in
// do nothing
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