Commit db349ab7 by Demid Merzlyakov

Search: added an ability to track user's location.

parent ab074700
...@@ -107,6 +107,8 @@ ...@@ -107,6 +107,8 @@
CEF959902600C5A800975FAA /* MoEngageAnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF9598F2600C5A800975FAA /* MoEngageAnalyticsService.swift */; }; CEF959902600C5A800975FAA /* MoEngageAnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF9598F2600C5A800975FAA /* MoEngageAnalyticsService.swift */; };
CEF959932600C63500975FAA /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF959922600C63500975FAA /* Analytics.swift */; }; CEF959932600C63500975FAA /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF959922600C63500975FAA /* Analytics.swift */; };
CEF959982600C88100975FAA /* AnalyticsParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF959972600C88100975FAA /* AnalyticsParameter.swift */; }; CEF959982600C88100975FAA /* AnalyticsParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF959972600C88100975FAA /* AnalyticsParameter.swift */; };
CEF9599F2601DF3300975FAA /* AdLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF9599E2601DF3300975FAA /* AdLogger.swift */; };
CEF959A626035A2600975FAA /* DeviceLocationMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF959A526035A2600975FAA /* DeviceLocationMonitor.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
...@@ -214,6 +216,8 @@ ...@@ -214,6 +216,8 @@
CEF9598F2600C5A800975FAA /* MoEngageAnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoEngageAnalyticsService.swift; sourceTree = "<group>"; }; CEF9598F2600C5A800975FAA /* MoEngageAnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoEngageAnalyticsService.swift; sourceTree = "<group>"; };
CEF959922600C63500975FAA /* Analytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Analytics.swift; sourceTree = "<group>"; }; CEF959922600C63500975FAA /* Analytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Analytics.swift; sourceTree = "<group>"; };
CEF959972600C88100975FAA /* AnalyticsParameter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsParameter.swift; sourceTree = "<group>"; }; CEF959972600C88100975FAA /* AnalyticsParameter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsParameter.swift; sourceTree = "<group>"; };
CEF9599E2601DF3300975FAA /* AdLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdLogger.swift; sourceTree = "<group>"; };
CEF959A526035A2600975FAA /* DeviceLocationMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceLocationMonitor.swift; sourceTree = "<group>"; };
DF826CF4702D9DCCB9A9DD71 /* Pods-1Weather.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-1Weather.release.xcconfig"; path = "Target Support Files/Pods-1Weather/Pods-1Weather.release.xcconfig"; sourceTree = "<group>"; }; DF826CF4702D9DCCB9A9DD71 /* Pods-1Weather.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-1Weather.release.xcconfig"; path = "Target Support Files/Pods-1Weather/Pods-1Weather.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
...@@ -294,6 +298,7 @@ ...@@ -294,6 +298,7 @@
CD1237C1255D5C5900C98139 /* 1Weather */ = { CD1237C1255D5C5900C98139 /* 1Weather */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
CEF9599C2601DF1A00975FAA /* Ads */,
CEF959632600C2E300975FAA /* Analytics */, CEF959632600C2E300975FAA /* Analytics */,
CD1237DA255D5DFA00C98139 /* PG.playground */, CD1237DA255D5DFA00C98139 /* PG.playground */,
87C171E725FF79CC00DA3464 /* Configuration */, 87C171E725FF79CC00DA3464 /* Configuration */,
...@@ -561,6 +566,7 @@ ...@@ -561,6 +566,7 @@
CEDE4F0925EFA376007457E9 /* Protocols */, CEDE4F0925EFA376007457E9 /* Protocols */,
CEAFF0A025E0FEF100DF4EBF /* ModelObjects */, CEAFF0A025E0FEF100DF4EBF /* ModelObjects */,
CEAFF0A225E0FF0800DF4EBF /* LocationManager.swift */, CEAFF0A225E0FF0800DF4EBF /* LocationManager.swift */,
CEF959A526035A2600975FAA /* DeviceLocationMonitor.swift */,
); );
path = Model; path = Model;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -644,6 +650,22 @@ ...@@ -644,6 +650,22 @@
path = Services; path = Services;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
CEF9599C2601DF1A00975FAA /* Ads */ = {
isa = PBXGroup;
children = (
CEF9599D2601DF1F00975FAA /* Helpers */,
);
path = Ads;
sourceTree = "<group>";
};
CEF9599D2601DF1F00975FAA /* Helpers */ = {
isa = PBXGroup;
children = (
CEF9599E2601DF3300975FAA /* AdLogger.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
DBFD169AA2AA6A3CB5B68BB5 /* Frameworks */ = { DBFD169AA2AA6A3CB5B68BB5 /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
...@@ -787,6 +809,7 @@ ...@@ -787,6 +809,7 @@
87C171EE25FF79CC00DA3464 /* AdConfigManager.swift in Sources */, 87C171EE25FF79CC00DA3464 /* AdConfigManager.swift in Sources */,
CDD0F1E82572429E00CF5017 /* AppFont.swift in Sources */, CDD0F1E82572429E00CF5017 /* AppFont.swift in Sources */,
CDC6124F25E7964700188DA7 /* CityDayTimesCell.swift in Sources */, CDC6124F25E7964700188DA7 /* CityDayTimesCell.swift in Sources */,
CEF9599F2601DF3300975FAA /* AdLogger.swift in Sources */,
CD17C5FB25D15B6B00EE884E /* AppCoordinator.swift in Sources */, CD17C5FB25D15B6B00EE884E /* AppCoordinator.swift in Sources */,
CD15DB3D25DA6C5100024727 /* ForecastTimePeriodControl.swift in Sources */, CD15DB3D25DA6C5100024727 /* ForecastTimePeriodControl.swift in Sources */,
CD1237F1255D83C500C98139 /* UIColor+Hex.swift in Sources */, CD1237F1255D83C500C98139 /* UIColor+Hex.swift in Sources */,
...@@ -838,6 +861,7 @@ ...@@ -838,6 +861,7 @@
CD86246125E662BC0097F3FB /* SunUvLineView.swift in Sources */, CD86246125E662BC0097F3FB /* SunUvLineView.swift in Sources */,
CEC526FA25E7959A00DA58A5 /* WeatherSource.swift in Sources */, CEC526FA25E7959A00DA58A5 /* WeatherSource.swift in Sources */,
CD822FFE25D6976F00A05501 /* TodayAdCell.swift in Sources */, CD822FFE25D6976F00A05501 /* TodayAdCell.swift in Sources */,
CEF959A626035A2600975FAA /* DeviceLocationMonitor.swift in Sources */,
CEC5275D25E8E50B00DA58A5 /* WdtDailySummary.swift in Sources */, CEC5275D25E8E50B00DA58A5 /* WdtDailySummary.swift in Sources */,
CDE18DCD25D1666700C80ED9 /* ForecastCoordinator.swift in Sources */, CDE18DCD25D1666700C80ED9 /* ForecastCoordinator.swift in Sources */,
CDC6126625E9085600188DA7 /* GraphLine.swift in Sources */, CDC6126625E9085600188DA7 /* GraphLine.swift in Sources */,
......
//
// AdLogger.swift
// BaconReader
//
// Created by Demid Merzlyakov on 03.04.2019.
// Copyright © 2019 OneLouder Apps. All rights reserved.
//
import Foundation
import UIKit
//import MoPub
//import FBAudienceNetwork
//import DTBiOSSDK
class AdLogger: NSObject {
enum LogLevel {
case debug
case info
case warning
case error
}
static var debugMode: Bool = false
static func setDebugMode(_ on: Bool) {
debugMode = on
#warning("Not implemented!")
//TODO: implement
// if on {
// MPLogManager.sharedInstance().consoleLogLevel = .debug
// DTBLog.setLogLevel(DTBLogLevelInfo)
// FBAdSettings.setLogLevel(.log)
// } else {
// MPLogManager.sharedInstance().consoleLogLevel = .none
// DTBLog.setLogLevel(DTBLogLevelOff)
// FBAdSettings.setLogLevel(.none)
// }
}
var componentName: String
init(componentName: String) {
self.componentName = componentName
}
func log(level: LogLevel, message: String) {
if AdLogger.debugMode {
NSLog("psm_ad \(componentName) [\(logLevelString(level: level))]: \(message)")
}
}
func debug(_ message: String) {
log(level: .debug, message: message)
}
func info(_ message: String) {
log(level: .info, message: message)
}
func warning(_ message: String) {
log(level: .warning, message: message)
}
func error(_ message: String) {
log(level: .error, message: message)
}
private func logLevelString(level: LogLevel) -> String {
switch level {
case .debug:
return "DEBUG"
case .info:
return "INFO"
case .warning:
return "WARN"
case .error:
return "ERROR"
}
}
}
//
// DeviceLocationMonitor.swift
// OneWeather
//
// Created by Demid Merzlyakov on 18.03.2021.
import Foundation
import CoreLocation
import UIKit
internal protocol DeviceLocationMonitorDelegate: class {
func deviceLocationMonitor(_ monitor: DeviceLocationMonitor, didUpdateLocation newLocation: PartialLocation)
func deviceLocationMonitor(_ monitor: DeviceLocationMonitor, authorizationStatusChanged locationIsAllowed: Bool)
}
internal class DeviceLocationMonitor: NSObject {
// MARK: - Private properties
private let log = Logger(componentName: "DeviceLocationMonitor")
private lazy var locationManager: CLLocationManager = { () -> CLLocationManager in
assert(Thread.isMainThread)
let result = CLLocationManager()
result.delegate = self
if #available(iOS 14, *) {
result.desiredAccuracy = kCLLocationAccuracyReduced
}
else {
result.desiredAccuracy = kCLLocationAccuracyKilometer
}
result.distanceFilter = 500.0 // only notify for 1 km
return result
}()
private var authorizationStatusChangeHandler: ((CLAuthorizationStatus) -> ())?
private var _lastKnownLocation: CLLocation? {
didSet {
if oldValue != _lastKnownLocation {
if let coordinates = _lastKnownLocation?.coordinate {
log.info(String(format:"lastKnownLocation changed: (%.5f, %5f)", coordinates.latitude, coordinates.longitude))
}
else {
log.info("lastKnownLocation changed: no location")
}
}
}
}
// MARK: - Public
public weak var delegate: DeviceLocationMonitorDelegate?
public var lastKnownLocation: DeviceLocation? {
get {
guard hasLocationPermissions else {
return nil
}
return DeviceLocation(location: _lastKnownLocation)
}
}
public enum LocationRequestResult {
/// User provided us with location permissions, we can use the current location.
case success(PartialLocation)
/// User opted out of providing us location access and chose to use Search instead. Show the Search screen if needed.
case useSearch
/// User denied location permissions and didn't choose Search. Do nothing.
case denied
}
public struct DeviceLocation: PartialLocation {
private static let coordinatesStringAccuracy = 6 // 6 digits after the dot
private let location: CLLocation?
fileprivate init(location: CLLocation?) {
self.location = location
}
public var deviceLocation: Bool {
return true
}
public var lat: String? {
guard let coordinate = location?.coordinate else {
return nil
}
return String(format: "%.\(DeviceLocation.coordinatesStringAccuracy)f", coordinate.latitude)
}
public var lon: String? {
guard let coordinate = location?.coordinate else {
return nil
}
return String(format: "%.\(DeviceLocation.coordinatesStringAccuracy)f", coordinate.longitude)
}
public var countryName: String? {
nil
}
public var region: String? {
nil
}
public var cityName: String? {
nil
}
public var nameForDisplay: String {
// this should never be called.
return ""
}
}
public typealias CurrentLocationCompletion = (LocationRequestResult) -> ()
public func useCurrentLocation(requestedFrom viewController: UIViewController, completion: @escaping CurrentLocationCompletion) {
guard CLLocationManager.locationServicesEnabled() else {
showGoToSettingsDialog(text: "location.servicesAreTurnedOff".localized(),
in: viewController,
completion: completion)
return
}
switch CLLocationManager.authorizationStatus() {
case .denied, .restricted:
showGoToSettingsDialog(text: "location.blockedByPermission".localized(), in: viewController, completion: completion)
case .notDetermined:
authorizationStatusChangeHandler = { [weak self] (_ status: CLAuthorizationStatus) in
DispatchQueue.main.async {
guard let self = self else { return }
if status == .denied {
self.delegate?.deviceLocationMonitor(self, authorizationStatusChanged: false)
completion(.useSearch)
}
else {
self.delegate?.deviceLocationMonitor(self, authorizationStatusChanged: true)
completion(.success(self.lastKnownLocation ?? DeviceLocation(location: self._lastKnownLocation)))
}
}
}
locationManager.requestWhenInUseAuthorization()
analytics(log: .ANALYTICS_LOC_PERM_DISPLAYED)
locationManager.startUpdatingLocation()
case .authorizedAlways, .authorizedWhenInUse:
completion(.success(lastKnownLocation ?? DeviceLocation(location: _lastKnownLocation)))
locationManager.startUpdatingLocation()
@unknown default:
assert(false, "Got to handle this new case: \(CLLocationManager.authorizationStatus())")
completion(.denied)
}
}
public var hasLocationPermissions: Bool {
guard CLLocationManager.locationServicesEnabled() else {
return false
}
return CLLocationManager.authorizationStatus() == .authorizedAlways || CLLocationManager.authorizationStatus() == .authorizedWhenInUse
}
// MARK: - Private methods
private func openAppSettings() {
guard let appSettingsUrl = URL(string: UIApplication.openSettingsURLString) else {
assert(false, "Failed to create settings URL from: \(UIApplication.openSettingsURLString)")
return
}
UIApplication.shared.open(appSettingsUrl)
}
private func showGoToSettingsDialog(text: String, in viewController: UIViewController, completion: @escaping CurrentLocationCompletion) {
let alertGoToSettings = UIAlertController(title: "location.goToSettings.title".localized(), message: text, preferredStyle: .alert)
let actionGoToSettings = UIAlertAction(title: "location.goToSettings.goToSettingsAction".localized(), style: .default) { [weak self] (_) in
self?.openAppSettings()
completion(.denied)
}
let actionUseSearch = UIAlertAction(title: "location.goToSettings.useSearchInsteadAction".localized(), style: .cancel) { (_) in
completion(.useSearch)
}
alertGoToSettings.addAction(actionGoToSettings)
alertGoToSettings.addAction(actionUseSearch)
viewController.present(alertGoToSettings, animated: true, completion: nil)
}
}
// MARK: - CLLocationManagerDelegate
extension DeviceLocationMonitor: CLLocationManagerDelegate {
internal func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .denied:
analytics(log: .ANALYTICS_LOC_PERM_NO)
case .authorizedAlways:
analytics(log: .ANALYTICS_LOC_PERM_YES)
case .authorizedWhenInUse:
analytics(log: .ANALYTICS_LOC_PERM_WHILEUSINGAPP)
case .notDetermined, .restricted:
// do nothing
break
@unknown default:
assertionFailure("Unknown case is not handled!")
}
log.info("didChangeAuthorizationStatus: \(status)")
/// Location authorization request doesn't have callbacks, and we want a callback, so that's a hack to be notified of the option selected in the permissions dialog.
if let handler = self.authorizationStatusChangeHandler {
handler(status)
self.authorizationStatusChangeHandler = nil
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let newLocation = (locations.min { $0.horizontalAccuracy < $1.horizontalAccuracy }) else {
log.info("updated location: no location")
return
}
_lastKnownLocation = newLocation
DispatchQueue.main.async {
self.delegate?.deviceLocationMonitor(self, didUpdateLocation: DeviceLocation(location: self._lastKnownLocation))
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
guard let clError = error as? CLError else { return }
switch clError {
case CLError.denied:
log.error("error: user denied access")
DispatchQueue.main.async {
self.delegate?.deviceLocationMonitor(self, authorizationStatusChanged: false)
}
default:
log.error("error: \(error)")
}
}
}
...@@ -7,19 +7,35 @@ ...@@ -7,19 +7,35 @@
import Foundation import Foundation
import CoreLocation import CoreLocation
import UIKit
public protocol LocationManagerDelegate: class { public protocol LocationManagerDelegate: class {
func locationManager(_ locationManager: LocationManager, changedCurrentLocation newLocation: Location?) func locationManager(_ locationManager: LocationManager, changedSelectedLocation newLocation: Location?)
func locationManager(_ locationManager: LocationManager, updatedLocationsList newList: [Location]) func locationManager(_ locationManager: LocationManager, updatedLocationsList newList: [Location])
} }
public class LocationManager { public class LocationManager {
private let log = Logger(componentName: "LocationManager") private let log = Logger(componentName: "LocationManager")
private let delegates = MulticastDelegate<LocationManagerDelegate>() private let delegates = MulticastDelegate<LocationManagerDelegate>()
private let deviceLocationMonitor: DeviceLocationMonitor
private let weatherUpdateSource: WeatherSource private let weatherUpdateSource: WeatherSource
private let defaultLocation = Location(lastTimeUpdated: Date(), private let defaultLocation = Location(deviceLocation: false,
lastTimeUpdated: Date(),
coordinates: .init(latitude: 37.3230, longitude: -122.0322), // Cupertino coordinates: .init(latitude: 37.3230, longitude: -122.0322), // Cupertino
timeZone: TimeZone(abbreviation: "PST")!) timeZone: TimeZone(abbreviation: "PST")!)
public enum LocationRequestResult {
/// User provided us with location permissions, we can use the current location.
case success
/// User opted out of providing us location access and chose to use Search instead. Show the Search screen if needed.
case useSearch
/// User denied location permissions and didn't choose Search. Do nothing.
case denied
}
public typealias CurrentLocationCompletion = (LocationRequestResult) -> ()
public private(set) var locations = [Location]() { public private(set) var locations = [Location]() {
didSet { didSet {
log.info("Locations list updated: \(locations.map { $0.description }.joined(separator: ", "))") log.info("Locations list updated: \(locations.map { $0.description }.joined(separator: ", "))")
...@@ -32,16 +48,16 @@ public class LocationManager { ...@@ -32,16 +48,16 @@ public class LocationManager {
} }
} }
private var _currentLocation: Location? { private var _selectedLocation: Location? {
didSet { didSet {
if oldValue?.description != currentLocation?.description { if oldValue?.description != selectedLocation?.description {
log.info("Current location changed to: \(currentLocation?.description ?? "nil")") log.info("Current location changed to: \(selectedLocation?.description ?? "nil")")
} }
log.info("Location updated.") log.info("Location updated.")
DispatchQueue.main.async { DispatchQueue.main.async {
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, changedCurrentLocation: self.currentLocation) delegate.locationManager(self, changedSelectedLocation: self.selectedLocation)
} }
} }
} }
...@@ -53,11 +69,13 @@ public class LocationManager { ...@@ -53,11 +69,13 @@ public class LocationManager {
public init(weatherUpdateSource: WeatherSource) { public init(weatherUpdateSource: WeatherSource) {
self.weatherUpdateSource = weatherUpdateSource self.weatherUpdateSource = weatherUpdateSource
self.deviceLocationMonitor = DeviceLocationMonitor()
self.deviceLocationMonitor.delegate = self
} }
public var currentLocation: Location? { public var selectedLocation: Location? {
get { get {
guard let location = _currentLocation else { guard let location = _selectedLocation else {
// TODO: don't do it this way! We won't be able to show search if there's no location! // TODO: don't do it this way! We won't be able to show search if there's no location!
return defaultLocation return defaultLocation
} }
...@@ -66,12 +84,12 @@ public class LocationManager { ...@@ -66,12 +84,12 @@ public class LocationManager {
} }
set { set {
_currentLocation = newValue _selectedLocation = newValue
} }
} }
public func updateWeather() { public func updateWeather() {
guard let location = currentLocation else { guard let location = selectedLocation else {
log.warning("Update weather: no location.") log.warning("Update weather: no location.")
return return
} }
...@@ -94,38 +112,53 @@ public class LocationManager { ...@@ -94,38 +112,53 @@ public class LocationManager {
} }
DispatchQueue.main.async { DispatchQueue.main.async {
self.currentLocation = updatedLocation self.selectedLocation = updatedLocation
} }
} }
} }
// TODO: update weather for all locations // TODO: update weather for all locations
public func addOrSelect(location: Location) { public func addIfNeeded(location: Location, selectLocation: Bool) {
// 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 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.
} }
if let existingLocation = locations.first(where: { $0 == location }) { if location.deviceLocation {
currentLocation = existingLocation if locations.first?.deviceLocation == true {
locations[0] = location
}
else {
locations = [location] + locations
}
if selectLocation {
selectedLocation = location
}
}
else if let existingLocation = locations.first(where: { $0 == location }) {
if selectLocation {
selectedLocation = existingLocation
}
} }
else { else {
locations.append(location) locations.append(location)
currentLocation = location if selectLocation {
selectedLocation = location
}
} }
// TODO: we need to update weather for new locations, probably. // TODO: we need to update weather for new locations, probably.
// Or not? Should ViewModels handle it? // Or not? Should ViewModels handle it?
} }
public func addOrSelect(partialLocation: PartialLocation) { public func addIfNeeded(partialLocation: PartialLocation, selectLocation: Bool) {
if let location = partialLocation as? Location { if let location = partialLocation as? Location {
addOrSelect(location: location) addIfNeeded(location: location, selectLocation: selectLocation)
} }
else { else {
makeLocation(from: partialLocation) { (location) in makeLocation(from: partialLocation) { (location) in
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
if let location = location { if let location = location {
self?.addOrSelect(location: location) self?.addIfNeeded(location: location, selectLocation: selectLocation)
} }
} }
} }
...@@ -141,8 +174,8 @@ public class LocationManager { ...@@ -141,8 +174,8 @@ public class LocationManager {
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.")
} }
if currentLocation == location { if selectedLocation == location {
currentLocation = locations.first selectedLocation = locations.first
} }
return result return result
} }
...@@ -155,10 +188,28 @@ public class LocationManager { ...@@ -155,10 +188,28 @@ public class LocationManager {
delegates.remove(delegate: delegate) delegates.remove(delegate: delegate)
} }
public func useCurrentLocation(presentDialogsIn viewController: UIViewController, completion: @escaping CurrentLocationCompletion) {
deviceLocationMonitor.useCurrentLocation(requestedFrom: viewController) { [weak self] (result) in
switch result {
case .success(let location):
self?.addIfNeeded(partialLocation: location, selectLocation: true)
completion(.success)
case .denied:
completion(.denied)
case .useSearch:
completion(.useSearch)
}
}
}
private func makeLocation(from partialLocation: PartialLocation, completion: @escaping (Location?) -> ()) { 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 { 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)") log.error("Geo lookup: no coordinates present: \(partialLocation)")
completion(nil) var location: Location? = nil
if partialLocation.deviceLocation {
location = Location(deviceLocation: true, lastTimeUpdated: Date(timeIntervalSince1970: 0), timeZone: TimeZone.current)
}
completion(location)
return return
} }
let location = CLLocation(latitude: lat, longitude: lon) let location = CLLocation(latitude: lat, longitude: lon)
...@@ -185,8 +236,10 @@ public class LocationManager { ...@@ -185,8 +236,10 @@ public class LocationManager {
return return
} }
//TODO: come up with something for the date //TODO: come up with something for the date, or just make it optional (probably makes the most sense)
result = Location(lastTimeUpdated: Date(timeIntervalSince1970: 0), timeZone: timeZone) result = Location(deviceLocation: partialLocation.deviceLocation,
lastTimeUpdated: Date(timeIntervalSince1970: 0),
timeZone: timeZone)
result?.coordinates = CLLocationCoordinate2D(latitude: lat, longitude: lon) result?.coordinates = CLLocationCoordinate2D(latitude: lat, longitude: lon)
result?.cityName = partialLocation.cityName ?? placemark.locality result?.cityName = partialLocation.cityName ?? placemark.locality
result?.countryName = partialLocation.countryName ?? placemark.country result?.countryName = partialLocation.countryName ?? placemark.country
...@@ -198,10 +251,24 @@ public class LocationManager { ...@@ -198,10 +251,24 @@ public class LocationManager {
let geocoder = CLGeocoder() let geocoder = CLGeocoder()
if #available(iOS 11, *) { if #available(iOS 11, *) {
geocoder.reverseGeocodeLocation(location, completionHandler: geocodeCompletion) let lookupLocale = Locale(identifier: "en-US")
geocoder.reverseGeocodeLocation(location, preferredLocale: lookupLocale, completionHandler: geocodeCompletion)
} }
else { else {
geocoder.reverseGeocodeLocation(location, completionHandler: geocodeCompletion) geocoder.reverseGeocodeLocation(location, completionHandler: geocodeCompletion)
} }
} }
} }
// MARK: - DeviceLocationMonitorDelegate implementation
extension LocationManager: DeviceLocationMonitorDelegate {
func deviceLocationMonitor(_ monitor: DeviceLocationMonitor, didUpdateLocation newLocation: PartialLocation) {
addIfNeeded(partialLocation: newLocation, selectLocation: false)
}
func deviceLocationMonitor(_ monitor: DeviceLocationMonitor, authorizationStatusChanged locationIsAllowed: Bool) {
if !locationIsAllowed && locations.first?.deviceLocation == true {
locations = [Location](locations.suffix(from: 1))
}
}
}
...@@ -92,6 +92,48 @@ extension GeoNamesPlace: Codable { ...@@ -92,6 +92,48 @@ extension GeoNamesPlace: Codable {
} }
extension GeoNamesPlace: PartialLocation { extension GeoNamesPlace: PartialLocation {
var deviceLocation: Bool {
return false
}
var nameForDisplay: String {
//TODO: refactor this
var sb = String()
let hasCity = cityName?.count ?? 0 > 0
if hasCity {
sb.append(cityName!)
}
let hasRegion = region?.count ?? 0 > 0
var countryString = ""
if let countryCode = countryCode, countryCode.count > 0 {
countryString = countryCode
}
else if let countryName = countryName, countryName.count > 0 {
countryString = countryName
}
let hasCountry = countryString.count > 0
if hasRegion || hasCountry {
if hasCity {
sb.append(" (")
}
if hasRegion && hasCountry {
sb.append("\(region!), \(countryString)")
}
else if hasRegion {
sb.append(region!)
}
else {
sb.append(countryString)
}
if hasCity {
sb.append(")")
}
}
return sb
}
var lat: String? { var lat: String? {
return latitude return latitude
} }
......
...@@ -8,8 +8,10 @@ ...@@ -8,8 +8,10 @@
import Foundation import Foundation
import CoreLocation import CoreLocation
public struct Location: Equatable, Hashable { public struct Location {
// MARK: - Data fields // MARK: - Data fields
/// True if this location came from the Device GPS (user's current location). False if it was added in some other way (search, push notification, popular cities, etc.)
public let deviceLocation: Bool
public var lastTimeUpdated: Date public var lastTimeUpdated: Date
public var coordinates: CLLocationCoordinate2D? public var coordinates: CLLocationCoordinate2D?
public var imageName: String? = "ny_bridge" //we'll possibly need to switch to URL public var imageName: String? = "ny_bridge" //we'll possibly need to switch to URL
...@@ -65,6 +67,30 @@ public struct Location: Equatable, Hashable { ...@@ -65,6 +67,30 @@ public struct Location: Equatable, Hashable {
} }
} }
extension Location: Equatable, Hashable {
public static func == (lhs: Self, rhs: Self) -> Bool {
if lhs.deviceLocation && rhs.deviceLocation {
return true
}
if let lCoordinates = lhs.coordinates, let rCoordinates = rhs.coordinates {
return lCoordinates == rCoordinates
}
return lhs.cityId == rhs.cityId
}
public func hash(into hasher: inout Hasher) {
if deviceLocation {
hasher.combine(deviceLocation)
}
else if let coordinates = self.coordinates {
hasher.combine(coordinates)
}
else {
hasher.combine(cityId)
}
}
}
// MARK: - CustomStringConvertible implementation // MARK: - CustomStringConvertible implementation
extension Location: CustomStringConvertible { extension Location: CustomStringConvertible {
public var description: String { public var description: String {
...@@ -100,16 +126,19 @@ extension Location: UpdatableModelObject { ...@@ -100,16 +126,19 @@ extension Location: UpdatableModelObject {
} }
extension CLLocationCoordinate2D: Hashable, Equatable { extension CLLocationCoordinate2D: Hashable, Equatable {
private static let comparisonAccuracyLimit: CLLocationDegrees = 0.00001 // approximately 1 meter – more than enough for our purposes. https://en.wikipedia.org/wiki/Decimal_degrees private static let comparisonAccuracyDigits = 4 // 4 digits after the dot, that's approximately 10 meters – should be enough for our purposes. https://en.wikipedia.org/wiki/Decimal_degrees
private var descriptionForComparison: String {
String(format: "%.\(CLLocationCoordinate2D.comparisonAccuracyDigits)f; %.\(CLLocationCoordinate2D.comparisonAccuracyDigits)f", latitude, longitude)
}
public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool {
// no point in comparing hashes first, I think, since making the hash might be as computationally expensive as this whole thing (I didn't profile it though). // not particularly fast, but it's not a too often used operation in the app, and it's the easiest way to make a comparison with given accuracy, while calculating the hash in a way such that if A == B, then A.hash == B.hash (with our "equal-to-X-digits-after-the-dot" equality implementation).
return fabs(lhs.latitude - rhs.latitude) + fabs(lhs.longitude - rhs.longitude) < CLLocationCoordinate2D.comparisonAccuracyLimit lhs.descriptionForComparison == rhs.descriptionForComparison
} }
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
hasher.combine(self.latitude) hasher.combine(descriptionForComparison)
hasher.combine(self.longitude)
} }
} }
...@@ -127,4 +156,44 @@ extension Location: PartialLocation { ...@@ -127,4 +156,44 @@ extension Location: PartialLocation {
} }
return String(format: "%.6f", value) return String(format: "%.6f", value)
} }
public var nameForDisplay: String {
get {
//TODO: refactor this
if let nickname = self.nickname, nickname.count > 0 {
return nickname
}
var sb = String()
let hasCity = cityName?.count ?? 0 > 0
if hasCity {
sb.append(cityName!)
}
let hasRegion = region?.count ?? 0 > 0
var hasCountry = countryCode?.count ?? 0 > 0
if deviceLocation && countryCode == "US" {
hasCountry = false
}
if hasRegion || hasCountry {
if hasCity {
sb.append(" (")
}
if hasRegion && hasCountry {
sb.append("\(region!), \(countryCode!)")
}
else if hasRegion {
sb.append(region!)
}
else {
sb.append(countryCode!)
}
if hasCity {
sb.append(")")
}
}
return sb
}
}
} }
...@@ -8,9 +8,13 @@ ...@@ -8,9 +8,13 @@
import Foundation import Foundation
public protocol PartialLocation { public protocol PartialLocation {
/// True if this location came from the Device GPS (user's current location). False if it was added in some other way (search, push notification, popular cities, etc.)
var deviceLocation: Bool { get }
var lat: String? { get} var lat: String? { get}
var lon: String? { get } var lon: String? { get }
var countryName: String? { get } var countryName: String? { get }
var region: String? { get } var region: String? { get }
var cityName: String? { get } var cityName: String? { get }
var nameForDisplay: String { get }
} }
...@@ -50,7 +50,8 @@ struct WdtLocation: Codable { ...@@ -50,7 +50,8 @@ struct WdtLocation: Codable {
today?.moonPhase = firstDay.moonPhase today?.moonPhase = firstDay.moonPhase
} }
return Location(lastTimeUpdated: updatedAt, return Location(deviceLocation: false,
lastTimeUpdated: updatedAt,
coordinates: coordinates, coordinates: coordinates,
imageName: nil, imageName: nil,
countryCode: nil, countryCode: nil,
......
...@@ -90,3 +90,11 @@ ...@@ -90,3 +90,11 @@
// Search // Search
"search.error.maxLocationWarning" = "To keep your 1Weather running in tip-top shape, please limit the number of locations to %@"; "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."; "search.error.deleteError" = "Failed to delete location.";
"search.accessibility.tapToSelectLocation" = "Tap to select this location";
// Location
"location.servicesAreTurnedOff" = "System location services are turned off.";
"location.blockedByPermission" = "Location access is blocked by permission settings.";
"location.goToSettings.title" = "Location Services";
"location.goToSettings.goToSettingsAction" = "Go to Settings";
"location.goToSettings.useSearchInsteadAction" = "Use Search instead";
...@@ -8,20 +8,23 @@ ...@@ -8,20 +8,23 @@
import UIKit import UIKit
public struct ThemeManager { public struct ThemeManager {
struct Colors { public struct Colors {
static let locationBlue = UIColor(hex: 0x1071F0) static let locationBlue = UIColor(hex: 0x1071F0)
static let temperatureLabelBG = UIColor(hex: 0x5F5F5F) static let temperatureLabelBG = UIColor(hex: 0x5F5F5F)
static let citySelected = UIColor(hex: 0x599A0E) static let citySelected = UIColor(hex: 0x599A0E)
static let cityNoSelected = UIColor(hex: 0xC5C5C5).withAlphaComponent(0.5) static let cityNoSelected = UIColor(hex: 0xC5C5C5).withAlphaComponent(0.5)
static let cityAddButtonBG = UIColor(hex: 0x131315) static let cityAddButtonBG = UIColor(hex: 0x131315)
static let searchBarTint = UIColor(hex: 0xEBEBF5).withAlphaComponent(0.6) static let searchBarTint = UIColor(hex: 0xEBEBF5).withAlphaComponent(0.6)
static let primaryBackground = UIColor(hex: 0x17181A)
static let primaryTextColor = UIColor.white
} }
static var currentTheme:ThemeProtocol { public static var currentTheme:ThemeProtocol {
return DefaultTheme() return DefaultTheme()
} }
static func refreshAppearance() { public static func refreshAppearance() {
//Navigation bar //Navigation bar
UINavigationBar.appearance().barTintColor = currentTheme.navigationBarBackgroundColor UINavigationBar.appearance().barTintColor = currentTheme.navigationBarBackgroundColor
UINavigationBar.appearance().isTranslucent = false UINavigationBar.appearance().isTranslucent = false
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
import UIKit import UIKit
protocol ThemeProtocol { public protocol ThemeProtocol {
//Base //Base
var name:String { get } var name:String { get }
var baseBackgroundColor:UIColor { get } var baseBackgroundColor:UIColor { get }
......
...@@ -10,7 +10,6 @@ import SnapKit ...@@ -10,7 +10,6 @@ import SnapKit
class CityCell: UITableViewCell { class CityCell: UITableViewCell {
//Public //Public
static let kIdentifier = "cityCell"
var onSelect:(() -> Void)? var onSelect:(() -> Void)?
var onAdd:(() -> Void)? var onAdd:(() -> Void)?
...@@ -31,20 +30,25 @@ class CityCell: UITableViewCell { ...@@ -31,20 +30,25 @@ class CityCell: UITableViewCell {
prepareTemperatureLabel() prepareTemperatureLabel()
} }
func configure(wdtLocation:WdtLocation, mode:LocationsViewModelDisplayMode) { func configure(location: PartialLocation, isSelectedLocation: Bool, mode:LocationsViewModelDisplayMode) {
cityLabel.text = wdtLocation.cityStateCountryName cityLabel.text = location.nameForDisplay
temperatureLabel.text = wdtLocation.currentConditions?.temp ?? "--" if let temp = (location as? Location)?.today?.temp?.shortString {
temperatureLabel.text = temp
self.contentView.accessibilityLabel = "\(location.nameForDisplay), \(temp)"
}
else {
temperatureLabel.text = "--"
self.contentView.accessibilityLabel = "\(location.nameForDisplay)"
}
self.contentView.accessibilityHint = "search.accessibility.tapToSelectLocation".localized()
addButton.isHidden = mode == .savedCities addButton.isHidden = mode == .savedCities
selectedButton.isHidden = mode != .savedCities selectedButton.isHidden = mode != .savedCities
temperatureContainer.isHidden = mode != .savedCities temperatureContainer.isHidden = mode != .savedCities
self.contentView.accessibilityLabel = "\(wdtLocation.cityStateName), \(wdtLocation.currentConditions?.temp ?? "")"
self.contentView.accessibilityHint = "Tap to select this location".localized
if mode == .savedCities { if mode == .savedCities {
UIView.performWithoutAnimation { UIView.performWithoutAnimation {
self.selectedButton.tintColor = wdtLocation.selectedLocation ? ThemeManager.Colors.citySelected : ThemeManager.Colors.cityNoSelected self.selectedButton.tintColor = isSelectedLocation ? ThemeManager.Colors.citySelected : ThemeManager.Colors.cityNoSelected
} }
} }
} }
......
...@@ -8,12 +8,14 @@ ...@@ -8,12 +8,14 @@
import UIKit import UIKit
//MARK:- Location Navigation View Controller //MARK:- Location Navigation View Controller
class LocationViewController: NavigationController { class LocationViewController: UINavigationController {
init(closeButtonIsHidden:Bool = false, openedFromOnboarding: Bool = false) { init(closeButtonIsHidden:Bool = false, openedFromOnboarding: Bool = false) {
let savedCitiesViewController = CitiesViewController(closeButtonHidden: closeButtonIsHidden, openedFromOnboarding: openedFromOnboarding) let savedCitiesViewController = CitiesViewController(closeButtonHidden: closeButtonIsHidden, openedFromOnboarding: openedFromOnboarding)
super.init(rootViewController: savedCitiesViewController) super.init(rootViewController: savedCitiesViewController)
self.modalPresentationStyle = .fullScreen self.modalPresentationStyle = .fullScreen
self.navigationBar.barTintColor = ThemeManager.Colors.primaryBackground //TODO: Dark mode
self.navigationBar.barTintColor = UIColor(hex: 0x17181A)// ThemeManager.Colors.primaryBackground
} }
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
...@@ -23,6 +25,11 @@ class LocationViewController: NavigationController { ...@@ -23,6 +25,11 @@ class LocationViewController: NavigationController {
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override var preferredStatusBarStyle: UIStatusBarStyle {
//TODO: Dark mode
return .lightContent
}
} }
//MARK:- Cities View Controller //MARK:- Cities View Controller
...@@ -81,7 +88,7 @@ private class CitiesViewController:UIViewController { ...@@ -81,7 +88,7 @@ private class CitiesViewController:UIViewController {
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
if openedFromOnboarding { if openedFromOnboarding {
analyticsLogEvent(ANALYTICS_FTUE_SEARCH_SEEN) analytics(log: .ANALYTICS_FTUE_SEARCH_SEEN)
} }
} }
...@@ -123,21 +130,9 @@ private class CitiesViewController:UIViewController { ...@@ -123,21 +130,9 @@ private class CitiesViewController:UIViewController {
@objc private func handleLocationButton() { @objc private func handleLocationButton() {
if openedFromOnboarding { if openedFromOnboarding {
analyticsLogEvent(ANALYTICS_FTUE_SEARCH_GPS) analytics(log: .ANALYTICS_FTUE_SEARCH_GPS)
}
UserLocationManager.shared.useCurrentLocation(requestedFrom: self) { [weak self] (_ result: UserLocationManager.LocationRequestResult) in
switch result {
case .success:
self?.close()
break
case .useSearch:
// do nothing, we're already in Search
break
case .denied:
// do nothing
break
}
} }
locationsViewModel.useCurrentLocation(requestedBy: self)
} }
@objc private func handleEditButton(button:UIButton) { @objc private func handleEditButton(button:UIButton) {
...@@ -272,7 +267,7 @@ extension CitiesViewController: LocationsViewModelDelegate { ...@@ -272,7 +267,7 @@ extension CitiesViewController: LocationsViewModelDelegate {
self.showAlert(withTitle: title, message: message) self.showAlert(withTitle: title, message: message)
} }
func viewModelDidSelectCity(model: LocationsViewModel, city: WdtLocation) { func viewModelDidSelectCity(model: LocationsViewModel, city: Location) {
close() close()
} }
} }
...@@ -285,7 +280,12 @@ extension CitiesViewController: UITableViewDataSource { ...@@ -285,7 +280,12 @@ extension CitiesViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CityCell.kIdentifier, for: indexPath) as! CityCell let cell = tableView.dequeueReusableCell(withIdentifier: CityCell.kIdentifier, for: indexPath) as! CityCell
cell.configure(wdtLocation: locationsViewModel.cities[indexPath.row],
let location = locationsViewModel.cities[indexPath.row]
let isSelectedLocation = locationsViewModel.isSelected(location)
cell.configure(location: location,
isSelectedLocation: isSelectedLocation,
mode: locationsViewModel.displayMode) mode: locationsViewModel.displayMode)
let displayModeOnCreation = self.locationsViewModel.displayMode let displayModeOnCreation = self.locationsViewModel.displayMode
...@@ -294,10 +294,11 @@ extension CitiesViewController: UITableViewDataSource { ...@@ -294,10 +294,11 @@ extension CitiesViewController: UITableViewDataSource {
strongSelf.locationsViewModel.add(city: strongSelf.locationsViewModel.cities[indexPath.row]) strongSelf.locationsViewModel.add(city: strongSelf.locationsViewModel.cities[indexPath.row])
if strongSelf.openedFromOnboarding { if strongSelf.openedFromOnboarding {
if displayModeOnCreation == .popularCities { if displayModeOnCreation == .popularCities {
analyticsLogEvent(ANALYTICS_FTUE_SEARCH_POPULAR) analytics(log: .ANALYTICS_FTUE_SEARCH_POPULAR)
} }
else { else {
analyticsLogEvent(ANALYTICS_FTUE_SEARCH_ADD)
analytics(log: .ANALYTICS_FTUE_SEARCH_ADD)
} }
} }
} }
...@@ -347,7 +348,7 @@ extension CitiesViewController: UITableViewDelegate { ...@@ -347,7 +348,7 @@ extension CitiesViewController: UITableViewDelegate {
} }
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
if indexPath.row == 0 && (locationsViewModel.cities.first?.myLocation ?? false) { if indexPath.row == 0 && (locationsViewModel.cities.first?.deviceLocation ?? false) {
return false return false
} }
return true return true
...@@ -373,17 +374,25 @@ extension CitiesViewController: UITableViewDelegate { ...@@ -373,17 +374,25 @@ extension CitiesViewController: UITableViewDelegate {
return .delete return .delete
} }
private func showEditLocationViewController(for location: WdtLocation) { private func showEditLocationViewController(for location: Location) {
let storyboard = UIStoryboard(name: "Extra", bundle: Bundle.main) #warning("Not implemented!")
let vc = storyboard.instantiateViewController(withIdentifier: "EditLocationViewController") as! EditLocationViewController // let storyboard = UIStoryboard(name: "Extra", bundle: Bundle.main)
vc.location = location //TODO: implement
navigationController?.pushViewController(vc, animated: true) // let vc = storyboard.instantiateViewController(withIdentifier: "EditLocationViewController") as! EditLocationViewController
// vc.location = location
// navigationController?.pushViewController(vc, animated: true)
} }
@available(iOS 11.0, *) @available(iOS 11.0, *)
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let city = locationsViewModel.cities[indexPath.row] guard locationsViewModel.displayMode == .savedCities else {
guard !city.myLocation else { return nil
}
guard let city = locationsViewModel.cities[indexPath.row] as? Location else {
return nil
}
guard !city.deviceLocation else {
return nil return nil
} }
let delete = UIContextualAction(style: .destructive, title: "Delete".localized) { [weak self] (_, _, completion) in let delete = UIContextualAction(style: .destructive, title: "Delete".localized) { [weak self] (_, _, completion) in
...@@ -399,8 +408,14 @@ extension CitiesViewController: UITableViewDelegate { ...@@ -399,8 +408,14 @@ extension CitiesViewController: UITableViewDelegate {
} }
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let city = locationsViewModel.cities[indexPath.row] guard locationsViewModel.displayMode == .savedCities else {
guard !city.myLocation else { return nil
}
guard let city = locationsViewModel.cities[indexPath.row] as? Location else {
return nil
}
guard !city.deviceLocation else {
return [] return []
} }
let delete = UITableViewRowAction(style: .destructive, title: "Delete".localized) { [weak self] (_, _) in let delete = UITableViewRowAction(style: .destructive, title: "Delete".localized) { [weak self] (_, _) in
...@@ -419,12 +434,12 @@ extension CitiesViewController: UITableViewDelegate { ...@@ -419,12 +434,12 @@ extension CitiesViewController: UITableViewDelegate {
case .searchResults: case .searchResults:
locationsViewModel.add(city: locationsViewModel.cities[indexPath.row]) locationsViewModel.add(city: locationsViewModel.cities[indexPath.row])
if openedFromOnboarding { if openedFromOnboarding {
analyticsLogEvent(ANALYTICS_FTUE_SEARCH_ADD) analytics(log: .ANALYTICS_FTUE_SEARCH_ADD)
} }
case .popularCities: case .popularCities:
locationsViewModel.add(city: locationsViewModel.cities[indexPath.row]) locationsViewModel.add(city: locationsViewModel.cities[indexPath.row])
if openedFromOnboarding { if openedFromOnboarding {
analyticsLogEvent(ANALYTICS_FTUE_SEARCH_POPULAR) analytics(log: .ANALYTICS_FTUE_SEARCH_POPULAR)
} }
} }
} }
...@@ -437,7 +452,7 @@ extension CitiesViewController: UISearchBarDelegate { ...@@ -437,7 +452,7 @@ extension CitiesViewController: UISearchBarDelegate {
self.locationsViewModel.displayMode = .popularCities self.locationsViewModel.displayMode = .popularCities
self.locationsViewModel.fetchPopularCities() self.locationsViewModel.fetchPopularCities()
if openedFromOnboarding { if openedFromOnboarding {
analyticsLogEvent(ANALYTICS_FTUE_SEARCH_TAP) analytics(log: .ANALYTICS_FTUE_SEARCH_TAP)
} }
} }
......
...@@ -44,6 +44,13 @@ public class LocationsViewModel { ...@@ -44,6 +44,13 @@ public class LocationsViewModel {
} }
} }
public func isSelected(_ location: PartialLocation?) -> Bool {
guard let fullLocation = location as? Location else {
return false
}
return fullLocation == locationManager.selectedLocation
}
public init() { public init() {
locationManager.add(delegate: self) locationManager.add(delegate: self)
} }
...@@ -183,12 +190,13 @@ public class LocationsViewModel { ...@@ -183,12 +190,13 @@ public class LocationsViewModel {
self.delegate?.viewModelError(model: self, title: nil, message: "search.error.maxLocationWarning".localized()) self.delegate?.viewModelError(model: self, title: nil, message: "search.error.maxLocationWarning".localized())
return return
} }
locationManager.addOrSelect(partialLocation: city) locationManager.addIfNeeded(partialLocation: city, selectLocation: true)
} }
func delete(city: PartialLocation) { func delete(city: PartialLocation) {
guard let location = city as? Location else { guard let location = city as? Location else {
log.warning("Attempted to delete partial location that can't be casted to Location. Ignoring: \(city)") log.warning("Attempted to delete partial location that can't be casted to Location. Ignoring: \(city)")
return
} }
if !locationManager.remove(location: location) { if !locationManager.remove(location: location) {
self.delegate?.viewModelError(model: self, title: "Error", message: "search.error.deleteError".localized()) self.delegate?.viewModelError(model: self, title: "Error", message: "search.error.deleteError".localized())
...@@ -200,16 +208,24 @@ public class LocationsViewModel { ...@@ -200,16 +208,24 @@ public class LocationsViewModel {
} }
func useCurrentLocation(requestedBy viewController:UIViewController) { func useCurrentLocation(requestedBy viewController:UIViewController) {
#warning("Not implemented!") locationManager.useCurrentLocation(presentDialogsIn: viewController) { [weak self] (result: LocationManager.LocationRequestResult) in
//TODO: implement switch result {
// UserLocationManager.shared.useCurrentLocation(requestedFrom: viewController) { (_ result: UserLocationManager.LocationRequestResult) in case .success:
// // do nothing // we'll get a location via a different callback, viewModelDidSelectCity will be called, and the ViewController will close.
// } break
case .useSearch:
// do nothing, we're already in Search
break
case .denied:
// do nothing
break
}
}
} }
} }
extension LocationsViewModel: LocationManagerDelegate { extension LocationsViewModel: LocationManagerDelegate {
public func locationManager(_ locationManager: LocationManager, changedCurrentLocation newLocation: Location?) { public func locationManager(_ locationManager: LocationManager, changedSelectedLocation newLocation: Location?) {
DispatchQueue.main.async { DispatchQueue.main.async {
self.delegate?.viewModelDidChange(model: self) self.delegate?.viewModelDidChange(model: self)
if let location = newLocation { if let location = newLocation {
......
...@@ -37,11 +37,15 @@ class ForecastViewModel: ViewModelProtocol { ...@@ -37,11 +37,15 @@ class ForecastViewModel: ViewModelProtocol {
//MARK:- LocationManager Delegate //MARK:- LocationManager Delegate
extension ForecastViewModel: LocationManagerDelegate { extension ForecastViewModel: LocationManagerDelegate {
func locationManager(_ locationManager: LocationManager, changedCurrentLocation newLocation: Location?) { func locationManager(_ locationManager: LocationManager, changedSelectedLocation newLocation: Location?) {
DispatchQueue.main.async { DispatchQueue.main.async {
print("TVM-Forecast") print("TVM-Forecast")
self.location = newLocation self.location = newLocation
self.delegate?.viewModelDidChange(model: self) self.delegate?.viewModelDidChange(model: self)
} }
} }
func locationManager(_ locationManager: LocationManager, updatedLocationsList newList: [Location]) {
// do nothing
}
} }
...@@ -37,7 +37,7 @@ class TodayViewModel: ViewModelProtocol { ...@@ -37,7 +37,7 @@ class TodayViewModel: ViewModelProtocol {
//MARK:- LocationManager Delegate //MARK:- LocationManager Delegate
extension TodayViewModel: LocationManagerDelegate { extension TodayViewModel: LocationManagerDelegate {
func locationManager(_ locationManager: LocationManager, changedCurrentLocation newLocation: Location?) { func locationManager(_ locationManager: LocationManager, changedSelectedLocation newLocation: Location?) {
DispatchQueue.main.async { DispatchQueue.main.async {
print("TVM") print("TVM")
self.location = newLocation self.location = newLocation
......
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