Commit 3117b368 by Dmitriy Stepanets

Added widget analytics

parent cb00631e
......@@ -161,6 +161,7 @@
CDA5542825EF734200A2E08C /* TodayCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA5542725EF734200A2E08C /* TodayCellFactory.swift */; };
CDA5542D25EF7C9700A2E08C /* ReusableCellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA5542C25EF7C9700A2E08C /* ReusableCellProtocol.swift */; };
CDAC9B8526319B0500AC1BF4 /* MapTimeControlItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAC9B8426319B0500AC1BF4 /* MapTimeControlItem.swift */; };
CDAD462C26A95ED800690CB1 /* WidgetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAD462B26A95ED800690CB1 /* WidgetManager.swift */; };
CDAD97B1262042B2007FCFB1 /* MapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAD97B0262042B2007FCFB1 /* MapButton.swift */; };
CDAD97B426207D14007FCFB1 /* MapTimeControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAD97B326207D14007FCFB1 /* MapTimeControlView.swift */; };
CDC3F858269460E600AAE3BF /* PromotionMediumWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC3F857269460E600AAE3BF /* PromotionMediumWidgetView.swift */; };
......@@ -473,6 +474,7 @@
CDA5542725EF734200A2E08C /* TodayCellFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayCellFactory.swift; sourceTree = "<group>"; };
CDA5542C25EF7C9700A2E08C /* ReusableCellProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableCellProtocol.swift; sourceTree = "<group>"; };
CDAC9B8426319B0500AC1BF4 /* MapTimeControlItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTimeControlItem.swift; sourceTree = "<group>"; };
CDAD462B26A95ED800690CB1 /* WidgetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetManager.swift; sourceTree = "<group>"; };
CDAD97B0262042B2007FCFB1 /* MapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapButton.swift; sourceTree = "<group>"; };
CDAD97B326207D14007FCFB1 /* MapTimeControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTimeControlView.swift; sourceTree = "<group>"; };
CDC3F857269460E600AAE3BF /* PromotionMediumWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionMediumWidgetView.swift; sourceTree = "<group>"; };
......@@ -672,6 +674,7 @@
CD1237DA255D5DFA00C98139 /* PG.playground */,
87C171E725FF79CC00DA3464 /* Configuration */,
CE9D181425ECB8370028D9D7 /* Common */,
CDAD462A26A95EB800690CB1 /* Widget */,
CEAFF09925DFC78200DF4EBF /* Network */,
CEAFF08125DFC66F00DF4EBF /* Model */,
CD647D0025ED07AF0034578B /* ViewModels */,
......@@ -1095,6 +1098,14 @@
path = Cells;
sourceTree = "<group>";
};
CDAD462A26A95EB800690CB1 /* Widget */ = {
isa = PBXGroup;
children = (
CDAD462B26A95ED800690CB1 /* WidgetManager.swift */,
);
path = Widget;
sourceTree = "<group>";
};
CDAD97AF26204285007FCFB1 /* Controls */ = {
isa = PBXGroup;
children = (
......@@ -1877,6 +1888,7 @@
CD82300A25D6B2AF00A05501 /* AppTabBarController.swift in Sources */,
CDC6126225E8DAB800188DA7 /* MoonPhaseCell.swift in Sources */,
CD37D3D6260C93B3002669D6 /* MenuCellFactory.swift in Sources */,
CDAD462C26A95ED800690CB1 /* WidgetManager.swift in Sources */,
CD8B60AD263819400055CB3F /* NWSAlertInfoBlockTableViewCell.swift in Sources */,
87D815AC2636D61D0015A6D1 /* NWSAlertViewModel.swift in Sources */,
CD37D3FA260DF714002669D6 /* SettingsThemeCell.swift in Sources */,
......
......@@ -127,6 +127,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
//Handle widget deep link
if #available(iOS 14, *) {
if url.absoluteString.contains("ow-widget") {
WidgetManager.shared.handle(url)
return true
}
}
log.info("Open URL: \(url) with options: \(options)")
let oneWeatherRouter = DeeplinksRouter()
oneWeatherRouter.open(url: url)
......@@ -142,6 +150,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationDidBecomeActive(_ application: UIApplication) {
LocationManager.shared.updateEverythingIfNeeded()
if #available(iOS 14, *) {
WidgetManager.shared.update()
}
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
......
......@@ -29,6 +29,16 @@
<string>oneweather</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>OW-Widget</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ow-widget</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
......
//
// WidgetManager.swift
// 1Weather
//
// Created by Dmitry Stepanets on 22.07.2021.
//
import WidgetKit
import OneWeatherAnalytics
@available(iOS 14, *)
private struct WidgetOptions: OptionSet {
let rawValue: Int
static let tempuratureSmall = WidgetOptions(rawValue: 1 << 0)
static let tempuratureMedium = WidgetOptions(rawValue: 1 << 1)
static let tempuratureLarge = WidgetOptions(rawValue: 1 << 2)
static let precipitationMedium = WidgetOptions(rawValue: 1 << 3)
static let windMedium = WidgetOptions(rawValue: 1 << 4)
static let windLarge = WidgetOptions(rawValue: 1 << 5)
static func option(forKind kind: String, family: WidgetFamily) -> Self? {
switch kind {
case "com.onelouder.oneweather.widget.temperature":
switch family {
case .systemSmall:
return .tempuratureSmall
case .systemMedium:
return .tempuratureMedium
case .systemLarge:
return .tempuratureLarge
default:
return nil
}
case "com.onelouder.oneweather.widget.precipitation":
switch family {
case .systemMedium:
return .precipitationMedium
default:
return nil
}
case "com.onelouder.oneweather.widget.wind":
switch family {
case .systemMedium:
return .windMedium
case .systemLarge:
return .windLarge
default:
return nil
}
default:
return nil
}
}
static func option(forDeepLink url: URL) -> Self? {
guard let widgetType = url.host else {
return nil
}
switch widgetType {
case "temperature-small":
return .tempuratureSmall
case "temperature-medium":
return .tempuratureMedium
case "temperature-large":
return .tempuratureLarge
case "precipitation-medium":
return .precipitationMedium
case "wind-medium":
return .windMedium
case "wind-large":
return .windLarge
default:
return nil
}
}
static func name(for option: WidgetOptions) -> String {
switch option {
case .tempuratureSmall:
return "small"
case .tempuratureMedium, .tempuratureLarge:
return "forecast temperature"
case .precipitationMedium:
return "forecast precipitation"
case .windMedium, .windLarge:
return "forecast wind"
default:
return "n/a"
}
}
}
@available(iOS 14, *)
class WidgetManager {
static let shared = WidgetManager()
private init() {
//Load stored options
let storedWidgetsOptions = UserDefaults.standard.integer(forKey: "widgetOptions")
self.widgetOptions = WidgetOptions(rawValue: storedWidgetsOptions)
}
private let log = Logger(componentName: "WidgetManager")
private var widgetOptions: WidgetOptions = []
func update() {
WidgetCenter.shared.getCurrentConfigurations {[weak self] result in
switch result {
case .success(let widgetInfo):
self?.sync(with: widgetInfo)
case .failure(let error):
self?.log.error(error.localizedDescription)
}
}
}
func handle(_ deepLink: URL) {
guard let option = WidgetOptions.option(forDeepLink: deepLink) else {
return
}
let name = WidgetOptions.name(for: option)
AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_LAUNCH_FROM,
params: [.ANALYTICS_KEY_WIDGET_NAME : name])
}
private func sync(with widgetInfo: [WidgetInfo]) {
var currentOptions: WidgetOptions = []
widgetInfo.forEach {
if let option = WidgetOptions.option(forKind: $0.kind, family: $0.family) {
currentOptions.insert(option)
}
}
//Add missing options
currentOptions.elements().forEach {
if !widgetOptions.contains($0) {
widgetOptions.insert($0)
//Analytic add
addToAnalytics(option: $0, isPlaced: true)
}
}
//Remove from saved if needed
widgetOptions.elements().enumerated().forEach {
if !currentOptions.contains($1) {
widgetOptions.remove($1)
addToAnalytics(option: $1, isPlaced: false)
}
}
//Save changes
save(options: widgetOptions)
}
private func addToAnalytics(option: WidgetOptions, isPlaced: Bool) {
let name = WidgetOptions.name(for: option)
AppAnalytics.shared.log(event: isPlaced ? .ANALYTICS_WIDGET_PLACED : .ANALYTICS_WIDGET_REMOVED,
params: [.ANALYTICS_KEY_WIDGET_NAME : name])
switch option {
case .tempuratureSmall:
AppAnalytics.shared.log(event: isPlaced ? .ANALYTICS_WIDGET_TEMP_SMALL_PLACE : .ANALYTICS_WIDGET_TEMP_SMALL_REMOVE)
case .tempuratureMedium, .tempuratureLarge:
AppAnalytics.shared.log(event: isPlaced ? .ANALYTICS_WIDGET_TEMP_PLACE : .ANALYTICS_WIDGET_TEMP_REMOVE)
case .precipitationMedium:
AppAnalytics.shared.log(event: isPlaced ? .ANALYTICS_WIDGET_PRECIP_PLACE : .ANALYTICS_WIDGET_PRECIP_REMOVE)
case .windMedium, .windLarge:
AppAnalytics.shared.log(event: isPlaced ? .ANALYTICS_WIDGET_WIND_PLACE : .ANALYTICS_WIDGET_WIND_REMOVE)
default:
break
}
}
private func save(options: WidgetOptions) {
UserDefaults.standard.setValue(options.rawValue, forKey: "widgetOptions")
UserDefaults.standard.synchronize()
}
}
private extension OptionSet where RawValue: FixedWidthInteger {
func elements() -> AnySequence<Self> {
var remainingBits = rawValue
var bitMask: RawValue = 1
return AnySequence {
return AnyIterator {
while remainingBits != 0 {
defer { bitMask = bitMask &* 2 }
if remainingBits & bitMask != 0 {
remainingBits = remainingBits & ~bitMask
return Self(rawValue: bitMask)
}
}
return nil
}
}
}
}
......@@ -94,7 +94,21 @@ public enum AnalyticsEvent: String {
case ANALYTICS_WIDGET_PROMO_EXPAND = "WIDGET_PROMO_EXPAND"
case ANALYTICS_WIDGET_BOTTOM_SCROLLED = "WIDGET_PROMO_BOTTOM_SCROLLED"
case ANALYTICS_WIDGET_PROMO_LEARN_CTA = "WIDGET_PROMO_LEARN_CTA"
case ANALYTICS_WIDGET_LAUNCH_FROM = "LAUNCH_FROM_WIDGET"
case ANALYTICS_WIDGET_PLACED = "WIDGET_PLACED"
case ANALYTICS_WIDGET_REMOVED = "WIDGET_REMOVED"
/// When widget is placed to the home screen
case ANALYTICS_WIDGET_TEMP_SMALL_PLACE = "IOS_SMALL_WIDGET_PLACED"
case ANALYTICS_WIDGET_TEMP_PLACE = "IOS_TEMP_FORECAST_WIDGET_PLACED"
case ANALYTICS_WIDGET_PRECIP_PLACE = "IOS_PRECIP_FORECAST_WIDGET_PLACED"
case ANALYTICS_WIDGET_WIND_PLACE = "IOS_WIND_FORECAST_WIDGET_PLACED"
/// When widget has been removed from home screen
case ANALYTICS_WIDGET_TEMP_SMALL_REMOVE = "IOS_SMALL_WIDGET_REMOVED"
case ANALYTICS_WIDGET_TEMP_REMOVE = "IOS_TEMP_FORECAST_WIDGET_REMOVED"
case ANALYTICS_WIDGET_PRECIP_REMOVE = "IOS_PRECIP_FORECAST_WIDGET_REMOVED"
case ANALYTICS_WIDGET_WIND_REMOVE = "IOS_WIND_FORECAST_WIDGET_REMOVED"
/// FTUE Funnel: User has saved his first city after installing the app.
case ANALYTICS_USER_QUALIFIED = "USER_QUALIFIED"
......
......@@ -21,4 +21,5 @@ public enum AnalyticsParameter: String {
case ANALYTICS_KEY_PUSH_NOTIFICATION_SOURCE = "source"
case ANALYTICS_KEY_THEME_CHANGE_NAME = "themeName"
case ANALYTICS_KEY_FIST_OPEN_SOURCE = "Source"
case ANALYTICS_KEY_WIDGET_NAME = "widget_name"
}
......@@ -20,6 +20,7 @@ struct PrecipitationWidget: Widget {
provider: WeatherProvider()
) { weatherEntry in
MediumPrecipitationWidgetView(widgetViewModel: ForecastWidgetViewModel(location: weatherEntry.location))
.widgetURL(URL(string: "ow-widget://precipitation-medium"))
}
.configurationDisplayName("widget.precipitation.title".localized())
.description("widget.precipitation.description".localized())
......
......@@ -37,10 +37,13 @@ private struct WidgetView: View {
switch widgetSize {
case .systemSmall:
SmallTemperatureWidgetView(widgetViewModel: viewModel)
.widgetURL(URL(string: "ow-widget://temperature-small"))
case .systemMedium:
MediumTemperatureWidgetView(widgetViewModel: viewModel)
.widgetURL(URL(string: "ow-widget://temperature-medium"))
case .systemLarge:
LargeTemperatureWidgetView(widgetViewModel: viewModel)
.widgetURL(URL(string: "ow-widget://temperature-large"))
@unknown default:
SmallTemperatureWidgetView(widgetViewModel: viewModel)
}
......
......@@ -37,8 +37,10 @@ private struct WidgetView: View {
switch widgetSize {
case .systemMedium:
MediumWindWidgetView(widgetViewModel: viewModel)
.widgetURL(URL(string: "ow-widget://wind-medium"))
case .systemLarge:
LargeWindWidgetView(widgetViewModel: viewModel)
.widgetURL(URL(string: "ow-widget://wind-large"))
default:
MediumWindWidgetView(widgetViewModel: viewModel)
}
......
import Foundation
import UIKit
var arr = [3, 0, 6, 22, 55, 45, 232, 534, 1, 7, 9, 10]
let maxValues = 6
let steps = (arr.count - 1) / (maxValues - 1)
var result = [Int]()
for index in 0..<maxValues {
print("Fraction: \(index * steps)")
result.append(arr[index * steps])
}
print("Orig: \(arr)")
print("Result: \(result)")
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