Commit db349ab7 by Demid Merzlyakov

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

parent ab074700
......@@ -107,6 +107,8 @@
CEF959902600C5A800975FAA /* MoEngageAnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF9598F2600C5A800975FAA /* MoEngageAnalyticsService.swift */; };
CEF959932600C63500975FAA /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF959922600C63500975FAA /* Analytics.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 */
/* Begin PBXFileReference section */
......@@ -214,6 +216,8 @@
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>"; };
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>"; };
/* End PBXFileReference section */
......@@ -294,6 +298,7 @@
CD1237C1255D5C5900C98139 /* 1Weather */ = {
isa = PBXGroup;
children = (
CEF9599C2601DF1A00975FAA /* Ads */,
CEF959632600C2E300975FAA /* Analytics */,
CD1237DA255D5DFA00C98139 /* PG.playground */,
87C171E725FF79CC00DA3464 /* Configuration */,
......@@ -561,6 +566,7 @@
CEDE4F0925EFA376007457E9 /* Protocols */,
CEAFF0A025E0FEF100DF4EBF /* ModelObjects */,
CEAFF0A225E0FF0800DF4EBF /* LocationManager.swift */,
CEF959A526035A2600975FAA /* DeviceLocationMonitor.swift */,
);
path = Model;
sourceTree = "<group>";
......@@ -644,6 +650,22 @@
path = Services;
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 */ = {
isa = PBXGroup;
children = (
......@@ -787,6 +809,7 @@
87C171EE25FF79CC00DA3464 /* AdConfigManager.swift in Sources */,
CDD0F1E82572429E00CF5017 /* AppFont.swift in Sources */,
CDC6124F25E7964700188DA7 /* CityDayTimesCell.swift in Sources */,
CEF9599F2601DF3300975FAA /* AdLogger.swift in Sources */,
CD17C5FB25D15B6B00EE884E /* AppCoordinator.swift in Sources */,
CD15DB3D25DA6C5100024727 /* ForecastTimePeriodControl.swift in Sources */,
CD1237F1255D83C500C98139 /* UIColor+Hex.swift in Sources */,
......@@ -838,6 +861,7 @@
CD86246125E662BC0097F3FB /* SunUvLineView.swift in Sources */,
CEC526FA25E7959A00DA58A5 /* WeatherSource.swift in Sources */,
CD822FFE25D6976F00A05501 /* TodayAdCell.swift in Sources */,
CEF959A626035A2600975FAA /* DeviceLocationMonitor.swift in Sources */,
CEC5275D25E8E50B00DA58A5 /* WdtDailySummary.swift in Sources */,
CDE18DCD25D1666700C80ED9 /* ForecastCoordinator.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)")
}
}
}
......@@ -92,6 +92,48 @@ extension GeoNamesPlace: Codable {
}
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? {
return latitude
}
......
......@@ -8,8 +8,10 @@
import Foundation
import CoreLocation
public struct Location: Equatable, Hashable {
public struct Location {
// 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 coordinates: CLLocationCoordinate2D?
public var imageName: String? = "ny_bridge" //we'll possibly need to switch to URL
......@@ -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
extension Location: CustomStringConvertible {
public var description: String {
......@@ -100,16 +126,19 @@ extension Location: UpdatableModelObject {
}
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 {
// 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).
return fabs(lhs.latitude - rhs.latitude) + fabs(lhs.longitude - rhs.longitude) < CLLocationCoordinate2D.comparisonAccuracyLimit
// 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).
lhs.descriptionForComparison == rhs.descriptionForComparison
}
public func hash(into hasher: inout Hasher) {
hasher.combine(self.latitude)
hasher.combine(self.longitude)
hasher.combine(descriptionForComparison)
}
}
......@@ -127,4 +156,44 @@ extension Location: PartialLocation {
}
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 @@
import Foundation
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 lon: String? { get }
var countryName: String? { get }
var region: String? { get }
var cityName: String? { get }
var nameForDisplay: String { get }
}
......@@ -50,7 +50,8 @@ struct WdtLocation: Codable {
today?.moonPhase = firstDay.moonPhase
}
return Location(lastTimeUpdated: updatedAt,
return Location(deviceLocation: false,
lastTimeUpdated: updatedAt,
coordinates: coordinates,
imageName: nil,
countryCode: nil,
......
......@@ -90,3 +90,11 @@
// 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.";
"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 @@
import UIKit
public struct ThemeManager {
struct Colors {
public struct Colors {
static let locationBlue = UIColor(hex: 0x1071F0)
static let temperatureLabelBG = UIColor(hex: 0x5F5F5F)
static let citySelected = UIColor(hex: 0x599A0E)
static let cityNoSelected = UIColor(hex: 0xC5C5C5).withAlphaComponent(0.5)
static let cityAddButtonBG = UIColor(hex: 0x131315)
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()
}
static func refreshAppearance() {
public static func refreshAppearance() {
//Navigation bar
UINavigationBar.appearance().barTintColor = currentTheme.navigationBarBackgroundColor
UINavigationBar.appearance().isTranslucent = false
......
......@@ -7,7 +7,7 @@
import UIKit
protocol ThemeProtocol {
public protocol ThemeProtocol {
//Base
var name:String { get }
var baseBackgroundColor:UIColor { get }
......
......@@ -10,7 +10,6 @@ import SnapKit
class CityCell: UITableViewCell {
//Public
static let kIdentifier = "cityCell"
var onSelect:(() -> Void)?
var onAdd:(() -> Void)?
......@@ -31,20 +30,25 @@ class CityCell: UITableViewCell {
prepareTemperatureLabel()
}
func configure(wdtLocation:WdtLocation, mode:LocationsViewModelDisplayMode) {
cityLabel.text = wdtLocation.cityStateCountryName
temperatureLabel.text = wdtLocation.currentConditions?.temp ?? "--"
func configure(location: PartialLocation, isSelectedLocation: Bool, mode:LocationsViewModelDisplayMode) {
cityLabel.text = location.nameForDisplay
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
selectedButton.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 {
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 @@
import UIKit
//MARK:- Location Navigation View Controller
class LocationViewController: NavigationController {
class LocationViewController: UINavigationController {
init(closeButtonIsHidden:Bool = false, openedFromOnboarding: Bool = false) {
let savedCitiesViewController = CitiesViewController(closeButtonHidden: closeButtonIsHidden, openedFromOnboarding: openedFromOnboarding)
super.init(rootViewController: savedCitiesViewController)
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?) {
......@@ -23,6 +25,11 @@ class LocationViewController: NavigationController {
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var preferredStatusBarStyle: UIStatusBarStyle {
//TODO: Dark mode
return .lightContent
}
}
//MARK:- Cities View Controller
......@@ -81,7 +88,7 @@ private class CitiesViewController:UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if openedFromOnboarding {
analyticsLogEvent(ANALYTICS_FTUE_SEARCH_SEEN)
analytics(log: .ANALYTICS_FTUE_SEARCH_SEEN)
}
}
......@@ -123,21 +130,9 @@ private class CitiesViewController:UIViewController {
@objc private func handleLocationButton() {
if openedFromOnboarding {
analyticsLogEvent(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
}
analytics(log: .ANALYTICS_FTUE_SEARCH_GPS)
}
locationsViewModel.useCurrentLocation(requestedBy: self)
}
@objc private func handleEditButton(button:UIButton) {
......@@ -272,7 +267,7 @@ extension CitiesViewController: LocationsViewModelDelegate {
self.showAlert(withTitle: title, message: message)
}
func viewModelDidSelectCity(model: LocationsViewModel, city: WdtLocation) {
func viewModelDidSelectCity(model: LocationsViewModel, city: Location) {
close()
}
}
......@@ -285,7 +280,12 @@ extension CitiesViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
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)
let displayModeOnCreation = self.locationsViewModel.displayMode
......@@ -294,10 +294,11 @@ extension CitiesViewController: UITableViewDataSource {
strongSelf.locationsViewModel.add(city: strongSelf.locationsViewModel.cities[indexPath.row])
if strongSelf.openedFromOnboarding {
if displayModeOnCreation == .popularCities {
analyticsLogEvent(ANALYTICS_FTUE_SEARCH_POPULAR)
analytics(log: .ANALYTICS_FTUE_SEARCH_POPULAR)
}
else {
analyticsLogEvent(ANALYTICS_FTUE_SEARCH_ADD)
analytics(log: .ANALYTICS_FTUE_SEARCH_ADD)
}
}
}
......@@ -347,7 +348,7 @@ extension CitiesViewController: UITableViewDelegate {
}
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 true
......@@ -373,17 +374,25 @@ extension CitiesViewController: UITableViewDelegate {
return .delete
}
private func showEditLocationViewController(for location: WdtLocation) {
let storyboard = UIStoryboard(name: "Extra", bundle: Bundle.main)
let vc = storyboard.instantiateViewController(withIdentifier: "EditLocationViewController") as! EditLocationViewController
vc.location = location
navigationController?.pushViewController(vc, animated: true)
private func showEditLocationViewController(for location: Location) {
#warning("Not implemented!")
// let storyboard = UIStoryboard(name: "Extra", bundle: Bundle.main)
//TODO: implement
// let vc = storyboard.instantiateViewController(withIdentifier: "EditLocationViewController") as! EditLocationViewController
// vc.location = location
// navigationController?.pushViewController(vc, animated: true)
}
@available(iOS 11.0, *)
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let city = locationsViewModel.cities[indexPath.row]
guard !city.myLocation else {
guard locationsViewModel.displayMode == .savedCities else {
return nil
}
guard let city = locationsViewModel.cities[indexPath.row] as? Location else {
return nil
}
guard !city.deviceLocation else {
return nil
}
let delete = UIContextualAction(style: .destructive, title: "Delete".localized) { [weak self] (_, _, completion) in
......@@ -399,8 +408,14 @@ extension CitiesViewController: UITableViewDelegate {
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let city = locationsViewModel.cities[indexPath.row]
guard !city.myLocation else {
guard locationsViewModel.displayMode == .savedCities else {
return nil
}
guard let city = locationsViewModel.cities[indexPath.row] as? Location else {
return nil
}
guard !city.deviceLocation else {
return []
}
let delete = UITableViewRowAction(style: .destructive, title: "Delete".localized) { [weak self] (_, _) in
......@@ -419,12 +434,12 @@ extension CitiesViewController: UITableViewDelegate {
case .searchResults:
locationsViewModel.add(city: locationsViewModel.cities[indexPath.row])
if openedFromOnboarding {
analyticsLogEvent(ANALYTICS_FTUE_SEARCH_ADD)
analytics(log: .ANALYTICS_FTUE_SEARCH_ADD)
}
case .popularCities:
locationsViewModel.add(city: locationsViewModel.cities[indexPath.row])
if openedFromOnboarding {
analyticsLogEvent(ANALYTICS_FTUE_SEARCH_POPULAR)
analytics(log: .ANALYTICS_FTUE_SEARCH_POPULAR)
}
}
}
......@@ -437,7 +452,7 @@ extension CitiesViewController: UISearchBarDelegate {
self.locationsViewModel.displayMode = .popularCities
self.locationsViewModel.fetchPopularCities()
if openedFromOnboarding {
analyticsLogEvent(ANALYTICS_FTUE_SEARCH_TAP)
analytics(log: .ANALYTICS_FTUE_SEARCH_TAP)
}
}
......
......@@ -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() {
locationManager.add(delegate: self)
}
......@@ -183,12 +190,13 @@ public class LocationsViewModel {
self.delegate?.viewModelError(model: self, title: nil, message: "search.error.maxLocationWarning".localized())
return
}
locationManager.addOrSelect(partialLocation: city)
locationManager.addIfNeeded(partialLocation: city, selectLocation: true)
}
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)")
return
}
if !locationManager.remove(location: location) {
self.delegate?.viewModelError(model: self, title: "Error", message: "search.error.deleteError".localized())
......@@ -200,16 +208,24 @@ public class LocationsViewModel {
}
func useCurrentLocation(requestedBy viewController:UIViewController) {
#warning("Not implemented!")
//TODO: implement
// UserLocationManager.shared.useCurrentLocation(requestedFrom: viewController) { (_ result: UserLocationManager.LocationRequestResult) in
// // do nothing
// }
locationManager.useCurrentLocation(presentDialogsIn: viewController) { [weak self] (result: LocationManager.LocationRequestResult) in
switch result {
case .success:
// 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 {
public func locationManager(_ locationManager: LocationManager, changedCurrentLocation newLocation: Location?) {
public func locationManager(_ locationManager: LocationManager, changedSelectedLocation newLocation: Location?) {
DispatchQueue.main.async {
self.delegate?.viewModelDidChange(model: self)
if let location = newLocation {
......
......@@ -37,11 +37,15 @@ class ForecastViewModel: ViewModelProtocol {
//MARK:- LocationManager Delegate
extension ForecastViewModel: LocationManagerDelegate {
func locationManager(_ locationManager: LocationManager, changedCurrentLocation newLocation: Location?) {
func locationManager(_ locationManager: LocationManager, changedSelectedLocation newLocation: Location?) {
DispatchQueue.main.async {
print("TVM-Forecast")
self.location = newLocation
self.delegate?.viewModelDidChange(model: self)
}
}
func locationManager(_ locationManager: LocationManager, updatedLocationsList newList: [Location]) {
// do nothing
}
}
......@@ -37,7 +37,7 @@ class TodayViewModel: ViewModelProtocol {
//MARK:- LocationManager Delegate
extension TodayViewModel: LocationManagerDelegate {
func locationManager(_ locationManager: LocationManager, changedCurrentLocation newLocation: Location?) {
func locationManager(_ locationManager: LocationManager, changedSelectedLocation newLocation: Location?) {
DispatchQueue.main.async {
print("TVM")
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