Commit aced138f by Demid Merzlyakov

Merge branch 'feature/push-notifications'

parents 8f30fea7 cf8f3803
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.onelouder.oneweather.MoEngage</string>
<string>group.com.onelouder.oneweather</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)psm.lsdk</string>
</array>
</dict>
</plist>
......@@ -18,4 +18,5 @@ public enum AnalyticsParameter: String {
case ANALYTICS_KEY_PLACEMENT_NAME = "placement_name"
case ANALYTICS_KEY_AD_UNIT_ID = "AD_PLACEMENT_ID"
case ANALYTICS_KEY_AD_ADAPTER = "AD_ADAPTER"
case ANALYTICS_KEY_PUSH_NOTIFICATION_SOURCE = "source"
}
......@@ -52,13 +52,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
.withCrashReporting(false)
.withAppVersion(appVersion))
// TODO: Radar Setup
// WDT radar setup
// SwarmManager.sharedManager.authentication = SkywiseAuthentication(
// app_id: WDT_APP_ID,
// app_key: WDT_APP_KEY
// )
//MoEngage setup
var moEngageConfig = MOSDKConfig(appID: kMoEngageAppId)
moEngageConfig.appGroupID = "group.com.onelouder.oneweather.MoEngage"
......@@ -70,11 +63,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
MoEngage.sharedInstance().initializeLive(with: moEngageConfig, andLaunchOptions: launchOptions)
#endif
PushNotificationsManager.shared.registerForRemoteNotifications()
return true
}
func applicationDidBecomeActive(_ application: UIApplication) {
LocationManager.shared.updateAllWeatherIfNeeded()
LocationManager.shared.updateEverythingIfNeeded()
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
PushNotificationsManager.shared.set(pushToken: deviceToken)
}
}
......@@ -12,12 +12,17 @@ class AppCoordinator: Coordinator {
private let tabBarController = AppTabBarController()
//Public
static var instance: AppCoordinator!
var parentCoordinator: Coordinator?
var childCoordinators = [Coordinator]()
init(window:UIWindow) {
window.rootViewController = tabBarController
window.makeKeyAndVisible()
//TODO: this is very bad, fix it
AppCoordinator.instance = self
}
func start() {
......@@ -40,6 +45,30 @@ class AppCoordinator: Coordinator {
tabBarController.setupTabBar()
}
public func openToday() {
tabBarController.selectedIndex = AppTabBarController.AppTab.today.rawValue
}
public func openForecast(timePeriod: TimePeriod?) {
let forecastIndex = AppTabBarController.AppTab.forecast.rawValue
tabBarController.selectedIndex = forecastIndex
if let timePeriod = timePeriod {
if let forecastCoordinator = childCoordinators[forecastIndex] as? ForecastCoordinator {
forecastCoordinator.open(timePeriod: timePeriod)
}
}
}
public func openRadar() {
tabBarController.selectedIndex = AppTabBarController.AppTab.radar.rawValue
}
public func openNotifications() {
let notificationsCoordinator = NotificationsCoordinator(parentViewController: tabBarController)
notificationsCoordinator.parentCoordinator = self
notificationsCoordinator.start()
}
func viewControllerDidEnd(controller: UIViewController) {
//
}
......
//
// DeeplinksRouter.swift
// OneWeather
//
// Created by Demid Merzlyakov on 28.01.2021.
// Copyright © 2021 OneLouder, Inc. All rights reserved.
//
import Foundation
class DeeplinksRouter {
static let urlScheme = "oneweather"
enum UrlPathComponent: String {
case home = "home"
case today = "today"
case forecast = "forecast"
case hourly = "hourly"
case daily = "daily"
case precipitation = "precipitation"
case radar = "radar"
case sunMoon = "sun-moon"
}
enum UrlQueryParam: String {
case cityId = "cityid"
case lat = "lat"
case lon = "lon"
case fipsCode = "fipsCode"
}
private var appCoordinator: AppCoordinator {
AppCoordinator.instance
}
private let log = Logger(componentName: "Router")
private var locationManager: LocationManager {
return LocationManager.shared
}
public init() {
}
private func is1WUrl(_ url: URL) -> Bool {
if let scheme = url.scheme {
if scheme.lowercased() == "oneweather" {
return true
}
}
if let host = url.host {
if host == "1weatherapp.com" || host == "oneweatherapp.com" {
return true
}
}
return false
}
public func parseLocation(from url: URL) -> PartialLocation? {
guard let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems else {
return nil
}
var queryItemsLookup = [UrlQueryParam: String](minimumCapacity: queryItems.count)
for item: URLQueryItem in queryItems {
if let knownParam = UrlQueryParam(rawValue: item.name.lowercased()) {
queryItemsLookup[knownParam] = item.value
}
}
guard let cityId = queryItemsLookup[.cityId] else {
return nil
}
let result = GeoNamesPlace()
result.latitude = queryItemsLookup[.lat]
result.longitude = queryItemsLookup[.lon]
result.fipsCode = queryItemsLookup[.fipsCode]
result.optionalCityId = cityId
return result
}
public func open(url: URL) {
guard self.is1WUrl(url) else {
log.info("not 1Weather URL, ignore: \(url)")
return
}
log.info("open URL: \(url)")
var pathComponents = url.pathComponents
while let currentComponent = pathComponents.popLast() {
if let parsedComponent = UrlPathComponent(rawValue: currentComponent.lowercased()) {
log.debug("Parsed path: \(parsedComponent.rawValue)")
if let location = parseLocation(from: url) {
log.debug("Location found: \(location)")
locationManager.addIfNeeded(partialLocation: location, selectLocation: true)
}
else {
log.debug("No location.")
}
switch parsedComponent {
case .home:
openToday()
case .today:
openToday()
case .forecast:
openForecast(timePeriod: nil)
case .hourly:
openForecast(timePeriod: .hourly)
case .daily:
openForecast(timePeriod: .daily)
case .precipitation:
openPrecipitation()
case .radar:
openRadar()
case .sunMoon:
openSunMoon()
}
break
}
else {
log.debug("Skip unknown path component: \(currentComponent)")
}
}
}
public func openToday() {
DispatchQueue.main.async {
self.log.info("open Today")
self.appCoordinator.openToday()
}
}
public func openForecast(timePeriod: TimePeriod?) {
DispatchQueue.main.async {
self.log.info("open Forecast")
self.appCoordinator.openForecast(timePeriod: timePeriod)
}
}
public func openAlerts() {
DispatchQueue.main.async {
self.log.info("open Alerts")
self.appCoordinator.openNotifications()
}
}
public func openPrecipitation() {
DispatchQueue.main.async {
self.log.info("open Precipitation")
//not implemented
self.openToday()
}
}
public func openRadar() {
DispatchQueue.main.async {
self.log.info("open Radar")
self.appCoordinator.openRadar()
}
}
public func openSunMoon() {
DispatchQueue.main.async {
self.log.info("open SunMoon")
//not implemented
self.openToday()
}
}
public func openVideo() {
DispatchQueue.main.async {
self.log.info("open Video")
//not implemented
self.openToday()
}
}
public func openWeatherDetail() {
DispatchQueue.main.async {
self.log.info("open WeatherDetail")
//not implemented
self.openToday()
}
}
}
......@@ -12,6 +12,7 @@ class ForecastCoordinator: Coordinator {
private let forecastViewModel = ForecastViewModel(locationManager: LocationManager.shared)
private let navigationController = UINavigationController(nibName: nil, bundle: nil)
private var tabBarController:UITabBarController?
private var forecastViewController: ForecastViewController?
//Public
var childCoordinators = [Coordinator]()
......@@ -25,6 +26,11 @@ class ForecastCoordinator: Coordinator {
let forecastViewController = ForecastViewController(viewModel: forecastViewModel)
navigationController.viewControllers = [forecastViewController]
tabBarController?.add(viewController: navigationController)
self.forecastViewController = forecastViewController
}
public func open(timePeriod: TimePeriod) {
self.forecastViewController?.switchTo(timePeriod: timePeriod)
}
func viewControllerDidEnd(controller: UIViewController) {
......
......@@ -39,6 +39,10 @@
</array>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
......
......@@ -103,6 +103,14 @@ internal class DeviceLocationMonitor: NSObject {
// this should never be called.
return ""
}
public var fipsCode: String? {
nil
}
public var optionalCityId: String? {
return nil
}
}
public typealias CurrentLocationCompletion = (LocationRequestResult) -> ()
......
......@@ -21,7 +21,9 @@ public class LocationManager {
private let weatherUpdateSource: WeatherSource
private let healthSource: HealthSource
private let fipsSource: FIPSSource
public let nwsAlertsManager: NWSAlertsManager
private let pushNotificationsManager: PushNotificationsManager
private let storage: Storage
private var defaultLocation = Location(deviceLocation: false,
coordinates: .init(latitude: 37.3230, longitude: -122.0322), // Cupertino
......@@ -53,7 +55,7 @@ public class LocationManager {
self?._locations = locations
self?._selectedLocationIndex = selectedIndex
self?.handleLocationsChange(locationsChanged: true, selectedLocationChanged: true)
self?.updateAllWeatherIfNeeded()
self?.updateEverythingIfNeeded()
}
}
......@@ -154,15 +156,19 @@ public class LocationManager {
weatherUpdateSource: WdtWeatherSource(),
healthSource: BlendHealthSource(),
nwsAlertsManager: NWSAlertsManager(),
fipsSource: BlendFIPSSource(),
pushNotificationsManager: PushNotificationsManager.shared,
storage: DelayedSaveStorage(storage: CoreDataStorage(), delay: 2))
public let maxLocationsCount = 12
public init(weatherUpdateSource: WeatherSource, healthSource: HealthSource, nwsAlertsManager: NWSAlertsManager, storage: Storage) {
public init(weatherUpdateSource: WeatherSource, healthSource: HealthSource, nwsAlertsManager: NWSAlertsManager, fipsSource: FIPSSource, pushNotificationsManager: PushNotificationsManager, storage: Storage) {
self.weatherUpdateSource = weatherUpdateSource
self.healthSource = healthSource
self.deviceLocationMonitor = DeviceLocationMonitor()
self.nwsAlertsManager = nwsAlertsManager
self.fipsSource = fipsSource
self.pushNotificationsManager = pushNotificationsManager
self.storage = storage
self.deviceLocationMonitor.delegate = self
......@@ -185,7 +191,7 @@ public class LocationManager {
}
}
public func updateAllWeatherIfNeeded() {
public func updateEverythingIfNeeded() {
let locations = self.locations
guard locations.count > 0 else {
log.info("Update all: update default location if needed.")
......@@ -204,6 +210,25 @@ public class LocationManager {
}
updateHealth(for: location)
updateNotifications(for: location)
getFipsIfNeeded(for: location)
}
pushNotificationsManager.updateNwsSubscriptions(for: locations)
}
public func getFipsIfNeeded(for location: Location) {
if location.fipsCode == nil {
fipsSource.getFipsCode(for: location) { [weak self] (fipsCode) in
if let fipsCode = fipsCode {
self?.makeChanges(to: location, in: "getFIPS", changes: { (location) -> Location in
var updatedLocation = location
updatedLocation.fipsCode = fipsCode
return updatedLocation
}, completion: { [weak self] in
guard let self = self else { return }
self.pushNotificationsManager.updateNwsSubscriptions(for: self.locations)
})
}
}
}
}
......@@ -321,8 +346,11 @@ public class LocationManager {
}
}
private func makeChanges(to location: Location, in operation: String, changes: @escaping (Location) -> Location) {
private func makeChanges(to location: Location, in operation: String, changes: @escaping (Location) -> Location, completion: (() -> ())? = nil) {
DispatchQueue.main.async {
defer {
completion?()
}
if let indexToUpdate = self.locations.firstIndex(where: { $0 == location }) {
self.locations[indexToUpdate] = changes(self.locations[indexToUpdate])
}
......@@ -365,7 +393,7 @@ public class LocationManager {
selectedLocationIndex = locations.count - 1
}
}
updateAllWeatherIfNeeded()
updateEverythingIfNeeded()
}
public func addIfNeeded(partialLocation: PartialLocation, selectLocation: Bool) {
......@@ -421,6 +449,20 @@ public class LocationManager {
}
private func makeLocation(from partialLocation: PartialLocation, completion: @escaping (Location?) -> ()) {
if let fipsCode = partialLocation.fipsCode {
if let existingLocation = locations.first(where: { $0.fipsCode == fipsCode }) {
log.info("Geo lookup: found location using FIPS code: \(existingLocation)")
completion(existingLocation)
return
}
}
if let cityId = partialLocation.optionalCityId {
if let existingLocation = locations.first(where: { $0.cityId == cityId }) {
log.info("Geo lookup: found location using city ID: \(existingLocation)")
completion(existingLocation)
return
}
}
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)")
var location: Location? = nil
......@@ -431,6 +473,7 @@ public class LocationManager {
completion(location)
return
}
let location = CLLocation(latitude: lat, longitude: lon)
let geocodeCompletion: CLGeocodeCompletionHandler = { [weak self] (placemarks, error) in
......
......@@ -19,6 +19,8 @@ final class GeoNamesPlace: NSObject {
var countryCode : String?
var fcodeName : String? // airport if airport
var toponymName : String? // airport name if fcodeName is airport
var fipsCode: String?
var optionalCityId: String?
func detailName() -> String {
var sb = String()
......
......@@ -225,4 +225,8 @@ extension Location: PartialLocation {
return sb
}
}
public var optionalCityId: String? {
cityId
}
}
......@@ -15,6 +15,8 @@ public protocol PartialLocation {
var countryName: String? { get }
var region: String? { get }
var cityName: String? { get }
var fipsCode: String? { get }
var optionalCityId: String? { get }
var nameForDisplay: String { get }
}
//
// BlendFIPSSource.swift
// 1Weather
//
// Created by Demid Merzlyakov on 28.04.2021.
//
import Foundation
class BlendFIPSSource: FIPSSource {
private let log = Logger(componentName: "BlendFIPSSource")
private let baseUrlProduction = "https://nwsalert.onelouder.com"
private let baseUrlStaging = "https://sta-nwsalert.onelouder.com"
private static let blendAPIKeyHeaderName = "blend-api-key"
private static let blendAPIKey = "0imfnc8mVLWwsAawjYr4Rx-Af50DDqtlx"
#if DEBUG
public var useStaging = true
#else
public var useStaging = false
#endif
private var baseUrl: String {
useStaging ? baseUrlStaging : baseUrlProduction
}
/// This queue is needed to synchronize access to locationsBeingUpdated. Also, to make logging more clear.
private let internalQueue: OperationQueue = {
let queue = OperationQueue()
queue.name = "BlendHealthSource Queue"
queue.maxConcurrentOperationCount = 1
return queue
}()
private var locationsBeingUpdated = Set<Location>()
public func getFipsCode(for location: Location, completion: @escaping FIPSSourceeCompletion) {
internalQueue.addOperation { [weak self] in
let extendedCompletion: FIPSSourceeCompletion = { [weak self] (fipsCode) in
self?.internalQueue.addOperation {
completion(fipsCode)
self?.locationsBeingUpdated.remove(location)
}
}
self?.getFipsCodeInternal(for: location, completion: extendedCompletion)
}
}
private func getFipsCodeInternal(for location: Location, completion: @escaping FIPSSourceeCompletion) {
guard !locationsBeingUpdated.contains(location) else {
completion(nil)
return
}
locationsBeingUpdated.insert(location)
guard let url = URL(string: self.baseUrl + "/1weather/api/v1/location") else {
assertionFailure("Should never happen. The URL should be correct.")
return
}
var queryParameters = [String: String]()
if let coordinates = location.coordinates {
queryParameters["lat"] = String(format: "%.5f", coordinates.latitude)
queryParameters["lon"] = String(format: "%.5f", coordinates.longitude)
}
queryParameters["zip"] = location.zip
queryParameters["city"] = location.cityName
queryParameters["state"] = location.region
queryParameters["country"] = location.countryCode
guard !queryParameters.isEmpty else {
completion(nil)
log.error("Not enough information about location.")
return
}
log.debug("Network request (\(location)): \(url)")
var request = URLRequest(url: url)
let encoder = JSONEncoder()
guard let requestBody = try? encoder.encode(queryParameters) else {
completion(nil)
log.error("Couldn't encode request body: \(queryParameters)")
return
}
request.httpBody = requestBody
var headers = request.allHTTPHeaderFields ?? [String: String]()
headers[BlendFIPSSource.blendAPIKeyHeaderName] = BlendFIPSSource.blendAPIKey
headers["Content-Type"] = "application/json"
request.allHTTPHeaderFields = headers
request.httpMethod = "POST"
let urlSession = URLSession.shared
let dataTask = urlSession.dataTask(with: request) { (data, reponse, error) in
// TODO: check response HTTP code
guard let data = data else {
self.log.debug("Network response (\(location)): error \(String(describing: error))")
completion(nil)
return
}
let responseBodyString = String(data: data, encoding: .utf8) ?? "<couldn't show as string"
self.log.debug("Network response (\(location)): \(responseBodyString)")
completion(FIPSResponse(data: data)?.fipsCode)
}
dataTask.resume()
}
}
//
// FIPSSource.swift
// 1Weather
//
// Created by Demid Merzlyakov on 27.04.2021.
//
import Foundation
public typealias FIPSSourceeCompletion = (String?) -> ()
public protocol FIPSSource {
func getFipsCode(for location: Location, completion: @escaping FIPSSourceeCompletion)
}
//
// FIPSResponse.swift
// 1Weather
//
// Created by Demid Merzlyakov on 28.04.2021.
//
import Foundation
struct FIPSResponse: Codable {
let fipsCode, s2CellID: String
enum CodingKeys: String, CodingKey {
case fipsCode = "fips_code"
case s2CellID = "s2_cell_id"
}
}
// MARK: Convenience initializers
extension FIPSResponse {
init?(data: Data) {
guard let me = try? JSONDecoder().decode(FIPSResponse.self, from: data) else { return nil }
self = me
}
init?(_ json: String, using encoding: String.Encoding = .utf8) {
guard let data = json.data(using: encoding) else { return nil }
self.init(data: data)
}
init?(fromURL url: String) {
guard let url = URL(string: url) else { return nil }
guard let data = try? Data(contentsOf: url) else { return nil }
self.init(data: data)
}
var jsonData: Data? {
return try? JSONEncoder().encode(self)
}
var json: String? {
guard let data = self.jsonData else { return nil }
return String(data: data, encoding: .utf8)
}
}
//
// PushNotificationsManager.swift
// OneWeather
//
// Created by Demid Merzlyakov on 18.01.2021.
// Copyright © 2021 OneLouder, Inc. All rights reserved.
//
import Foundation
import MoEngage
public class PushNotificationsManager: NSObject {
// MARK: - Private
private let log = Logger(componentName: "PushNotificationsManager")
// MARK: - Public
public override init() {
super.init()
UNUserNotificationCenter.current().delegate = self
}
public static let shared = PushNotificationsManager()
// TODO: forced re-register on timeout
public func registerForRemoteNotifications() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { [weak self]
(granted, error) in
guard let self = self else { return }
if let error = error {
self.log.error("Error: \(error)")
}
else {
self.log.info("Granted: \(granted)")
}
MoEngage.sharedInstance().registerForRemoteNotification(withCategories: nil, withUserNotificationCenterDelegate: self)
}
}
private var lastSetFIPSList = ""
public func updateNwsSubscriptions(for locations: [Location]) {
let fipsCodes = locations.compactMap { $0.fipsCode }
let newFipsList = fipsCodes.joined(separator: ",")
if newFipsList != lastSetFIPSList {
log.info("Set FIPS_LIST to '\(newFipsList)'")
lastSetFIPSList = newFipsList
}
MoEngage.sharedInstance().setUserAttribute(newFipsList, forKey: "FIPS_LIST")
}
public func set(pushToken: Data) {
let tokenString = pushToken.map { String(format: "%02.2hhx", $0) }.joined()
log.info("Got new APNS token: \(tokenString)")
MoEngage.sharedInstance().setPushToken(pushToken)
}
}
extension PushNotificationsManager: UNUserNotificationCenterDelegate {
private enum MoEngageScreenName: String {
case mainScreen = "com.handmark.expressweather.ui.activities.mainactivity"
case detailsScreen = "com.handmark.expressweather.ui.activities.weatherdetailsactivity"
case videosScreen = "com.handmark.expressweather.ui.activities.videodetailsactivity"
case alertsScreen = "com.handmark.expressweather.ui.activities.alertactivity"
}
private func makeMoEngageDeeplinkUrl(from response: UNNotificationResponse) -> URL? {
guard let appExtra = response.notification.request.content.userInfo["app_extra"] as? [String: Any] else {
return nil
}
if let urlString = appExtra["moe_deeplink"] as? String, let url = URL(string: urlString) {
return url
}
return nil
}
private func switchLocationIfNeeded(parsing screenData: [String: Any]?, using router: DeeplinksRouter) {
guard let screenData = screenData else {
return
}
let cityId = screenData["location"] as? String
let lat = screenData["lat"] as? String
let lon = screenData["lon"] as? String
let fipsCode = screenData["fipsCode"] as? String
guard (lat != nil && lon != nil) || cityId != nil || fipsCode != nil else {
log.debug("MoEngage push: no location data found")
return
}
let newLoc = GeoNamesPlace()
newLoc.latitude = lat
newLoc.longitude = lon
newLoc.optionalCityId = cityId
newLoc.fipsCode = fipsCode
LocationManager.shared.addIfNeeded(partialLocation: newLoc, selectLocation: true)
log.info("MoEngage push: location found: \(newLoc)")
}
private func handleMoEngageDeeplinks(for response: UNNotificationResponse) {
let userInfo = response.notification.request.content.userInfo
guard userInfo["moengage"] as? [String: Any] != nil else {
log.debug("No MoEngage data found.")
return
}
log.info("MoEngage push received: \(userInfo)")
let router = DeeplinksRouter()
if let moEngageUrl = makeMoEngageDeeplinkUrl(from: response) {
router.open(url: moEngageUrl)
}
else {
guard let appExtra = userInfo["app_extra"] as? [String: Any] else {
log.error("MoEngage push: no app_extra found.")
return
}
guard let screenNameStr = (appExtra["screenName"] as? String)?.trim().lowercased(),
let screenName = MoEngageScreenName(rawValue: screenNameStr) else {
log.error("MoEngage push: screenName not found or is not correct.")
return
}
let screenData = appExtra["screenData"] as? [String: Any]
switchLocationIfNeeded(parsing: screenData, using: router)
switch screenName {
case .detailsScreen:
router.openWeatherDetail()
case .videosScreen:
router.openVideo()
case .mainScreen:
guard let launchScreenId = screenData?["LaunchScreenID"] as? String else {
log.error("MoEngage push: LaunchScreenID not found for a MainActivity screen.")
return
}
switch launchScreenId {
case "0":
router.openToday()
case "1":
router.openForecast(timePeriod: nil)
case "2":
router.openPrecipitation()
case "3":
router.openRadar()
case "4":
router.openSunMoon()
default:
log.error("MoEngage push: Unknown launch screen id: \(launchScreenId)")
}
case .alertsScreen:
router.openAlerts()
}
}
}
public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
MoEngage.sharedInstance().userNotificationCenter(center, didReceive: response) // not sure we should call it for PushPin notifications, too
handleMoEngageDeeplinks(for: response)
completionHandler()
}
public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
if UIApplication.shared.applicationState == .active {
analytics(log: .ANALYTICS_PUSH_RECEIVED)
} else {
analytics(log: .ANALYTICS_PUSH_SELECTED, params: [.ANALYTICS_KEY_PUSH_NOTIFICATION_SOURCE: "background"])
}
log.debug("Got a push notification: \(notification.request.content.userInfo)");
completionHandler([.sound,.alert])
}
}
......@@ -7,16 +7,17 @@
import UIKit
private enum AppTab:Int, CaseIterable {
case today = 0
case forecast = 1
case radar = 2
case menu = 3
}
class AppTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
}
public enum AppTab:Int, CaseIterable {
case today = 0
case forecast = 1
case radar = 2
case menu = 3
}
public func setupTabBar() {
......
......@@ -67,13 +67,19 @@ class ForecastViewController: UIViewController {
}
}
public func switchTo(timePeriod: TimePeriod) {
if self.timePeriodControl.selectedSegmentIndex != timePeriod.rawValue {
self.timePeriodControl.selectedSegmentIndex = timePeriod.rawValue
}
forecastCellFactory.setTimePeriod(timePeriod: timePeriod)
self.tableView.reloadData()
}
@objc private func handleSegmentDidChange() {
guard let timePeriod = TimePeriod(rawValue: self.timePeriodControl.selectedSegmentIndex) else {
return
}
forecastCellFactory.setTimePeriod(timePeriod: timePeriod)
self.tableView.reloadData()
self.switchTo(timePeriod: timePeriod)
}
@objc private func handleCityButton() {
......
......@@ -43,7 +43,7 @@ class ForecastViewModel: ViewModelProtocol {
}
public func updateWeather() {
locationManager.updateAllWeatherIfNeeded()
locationManager.updateEverythingIfNeeded()
}
public func selectDailyWeatherAt(index:Int) {
......
......@@ -31,7 +31,7 @@ class TodayViewModel: ViewModelProtocol {
}
public func updateWeather() {
locationManager.updateAllWeatherIfNeeded()
locationManager.updateEverythingIfNeeded()
}
}
......
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