Commit 98ee0597 by Demid Merzlyakov

Merge branch 'release/5.3' into develop

# Conflicts:
#	1Weather/AppDelegate.swift
#	OneWeatherAnalytics/OneWeatherAnalytics/AnalyticsEvent.swift
#	OneWeatherCore/OneWeatherCore/Managers/WidgetManager.swift
#	OneWeatherCore/OneWeatherCore/Settings/Settings.swift
parents f7988497 0221947c
...@@ -253,6 +253,7 @@ ...@@ -253,6 +253,7 @@
CEC8FBAF2639756A0001A6BF /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC8FBAE2639756A0001A6BF /* OnboardingViewController.swift */; }; CEC8FBAF2639756A0001A6BF /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC8FBAE2639756A0001A6BF /* OnboardingViewController.swift */; };
CEC8FBB2263976240001A6BF /* OnboardingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC8FBB1263976240001A6BF /* OnboardingCoordinator.swift */; }; CEC8FBB2263976240001A6BF /* OnboardingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC8FBB1263976240001A6BF /* OnboardingCoordinator.swift */; };
CEC8FBB5263976400001A6BF /* OnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC8FBB4263976400001A6BF /* OnboardingViewModel.swift */; }; CEC8FBB5263976400001A6BF /* OnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC8FBB4263976400001A6BF /* OnboardingViewModel.swift */; };
CEE1150626D987C5008FE415 /* WidgetLocationSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE1150526D987C5008FE415 /* WidgetLocationSource.swift */; };
CEE8869526C30F680000161B /* OneWeatherUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD5909CF26A59AAA00448579 /* OneWeatherUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; CEE8869526C30F680000161B /* OneWeatherUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD5909CF26A59AAA00448579 /* OneWeatherUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
CEEB3547266F5D9900E16F90 /* BannerAdCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEB3546266F5D9900E16F90 /* BannerAdCell.swift */; }; CEEB3547266F5D9900E16F90 /* BannerAdCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEB3546266F5D9900E16F90 /* BannerAdCell.swift */; };
CEEB3549266F5DA900E16F90 /* MRECAdCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEB3548266F5DA900E16F90 /* MRECAdCell.swift */; }; CEEB3549266F5DA900E16F90 /* MRECAdCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEB3548266F5DA900E16F90 /* MRECAdCell.swift */; };
...@@ -550,6 +551,7 @@ ...@@ -550,6 +551,7 @@
CEC8FBAE2639756A0001A6BF /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; }; CEC8FBAE2639756A0001A6BF /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
CEC8FBB1263976240001A6BF /* OnboardingCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingCoordinator.swift; sourceTree = "<group>"; }; CEC8FBB1263976240001A6BF /* OnboardingCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingCoordinator.swift; sourceTree = "<group>"; };
CEC8FBB4263976400001A6BF /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = "<group>"; }; CEC8FBB4263976400001A6BF /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = "<group>"; };
CEE1150526D987C5008FE415 /* WidgetLocationSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetLocationSource.swift; sourceTree = "<group>"; };
CEEB3546266F5D9900E16F90 /* BannerAdCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerAdCell.swift; sourceTree = "<group>"; }; CEEB3546266F5D9900E16F90 /* BannerAdCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerAdCell.swift; sourceTree = "<group>"; };
CEEB3548266F5DA900E16F90 /* MRECAdCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MRECAdCell.swift; sourceTree = "<group>"; }; CEEB3548266F5DA900E16F90 /* MRECAdCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MRECAdCell.swift; sourceTree = "<group>"; };
CEEF40FF265E47FF00425D8F /* BlendFIPSSource.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BlendFIPSSource.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CEEF40FF265E47FF00425D8F /* BlendFIPSSource.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BlendFIPSSource.framework; sourceTree = BUILT_PRODUCTS_DIR; };
...@@ -853,6 +855,7 @@ ...@@ -853,6 +855,7 @@
CD415DA22668FFF300177515 /* WeatherProvider.swift */, CD415DA22668FFF300177515 /* WeatherProvider.swift */,
CD5293D92669094E009547C8 /* WeatherEntry.swift */, CD5293D92669094E009547C8 /* WeatherEntry.swift */,
CDA02A1826A6F92F00A8F2F6 /* WeatherLocationMock.swift */, CDA02A1826A6F92F00A8F2F6 /* WeatherLocationMock.swift */,
CEE1150526D987C5008FE415 /* WidgetLocationSource.swift */,
); );
path = Data; path = Data;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -2038,6 +2041,7 @@ ...@@ -2038,6 +2041,7 @@
CD5293E1266A4258009547C8 /* AppFont.swift in Sources */, CD5293E1266A4258009547C8 /* AppFont.swift in Sources */,
CDA02A1926A6F92F00A8F2F6 /* WeatherLocationMock.swift in Sources */, CDA02A1926A6F92F00A8F2F6 /* WeatherLocationMock.swift in Sources */,
CD5293DF266A235F009547C8 /* ForecastWidgetViewModel.swift in Sources */, CD5293DF266A235F009547C8 /* ForecastWidgetViewModel.swift in Sources */,
CEE1150626D987C5008FE415 /* WidgetLocationSource.swift in Sources */,
CDF6E87726A8329D004A9DBD /* WindWidget.swift in Sources */, CDF6E87726A8329D004A9DBD /* WindWidget.swift in Sources */,
CD5293D8266908DB009547C8 /* WidgetPlaceholderView.swift in Sources */, CD5293D8266908DB009547C8 /* WidgetPlaceholderView.swift in Sources */,
CD5293EA266A564E009547C8 /* ThemeProtocol.swift in Sources */, CD5293EA266A564E009547C8 /* ThemeProtocol.swift in Sources */,
......
...@@ -80,17 +80,22 @@ ...@@ -80,17 +80,22 @@
<EnvironmentVariables> <EnvironmentVariables>
<EnvironmentVariable <EnvironmentVariable
key = "_XCWidgetKind" key = "_XCWidgetKind"
value = "com.onelouder.oneweather.widget.radar" value = "com.onelouder.oneweather.widget.temperature"
isEnabled = "YES"> isEnabled = "YES">
</EnvironmentVariable> </EnvironmentVariable>
<EnvironmentVariable <EnvironmentVariable
key = "CG_NUMERICS_SHOW_BACKTRACE"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetDefaultView" key = "_XCWidgetDefaultView"
value = "timeline" value = "timeline"
isEnabled = "NO"> isEnabled = "NO">
</EnvironmentVariable> </EnvironmentVariable>
<EnvironmentVariable <EnvironmentVariable
key = "_XCWidgetFamily" key = "_XCWidgetFamily"
value = "large" value = "medium"
isEnabled = "YES"> isEnabled = "YES">
</EnvironmentVariable> </EnvironmentVariable>
</EnvironmentVariables> </EnvironmentVariables>
......
...@@ -32,9 +32,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ...@@ -32,9 +32,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
private let appsFlyer = AppsFlyerLib.shared() private let appsFlyer = AppsFlyerLib.shared()
private let appsFlyerLog = Logger(componentName: "AppsFlyer") private let appsFlyerLog = Logger(componentName: "AppsFlyer")
private let log = Logger(componentName: "AppDelegate") private let log = Logger(componentName: "AppDelegate")
private let settings = Settings.shared
private enum FirstOpenSource: String {
case icon = "Icon"
case url = "Push Notification"
case pushNotification = "Share Deeplink"
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
settings.appDelegate = self
settings.delegate.add(delegate: self)
ThemeManager.refreshAppearance() ThemeManager.refreshAppearance()
UserDefaults.migrateUserDefaultsToAppGroupsIfNeeded()
if let launchOptions = launchOptions { if let launchOptions = launchOptions {
log.debug("Launch options: \(launchOptions)") log.debug("Launch options: \(launchOptions)")
...@@ -109,17 +119,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ...@@ -109,17 +119,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
MoEngage.sharedInstance().initializeLive(with: moEngageConfig, andLaunchOptions: launchOptions) MoEngage.sharedInstance().initializeLive(with: moEngageConfig, andLaunchOptions: launchOptions)
#endif #endif
if let widgetPromotionTriggerCount = Settings.shared.widgetPromotionTriggerCount { if let widgetPromotionTriggerCount = settings.widgetPromotionTriggerCount {
if widgetPromotionTriggerCount >= 0 { if widgetPromotionTriggerCount >= 0 {
Settings.shared.widgetPromotionTriggerCount = widgetPromotionTriggerCount - 1 settings.widgetPromotionTriggerCount = widgetPromotionTriggerCount - 1
} }
} }
else { else {
if CCPAHelper.shared.isNewUser { if CCPAHelper.shared.isNewUser {
Settings.shared.widgetPromotionTriggerCount = 2 settings.widgetPromotionTriggerCount = 2
} }
else { else {
Settings.shared.widgetPromotionTriggerCount = 0 settings.widgetPromotionTriggerCount = 0
} }
} }
return true return true
...@@ -158,7 +168,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ...@@ -158,7 +168,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
PushNotificationsManager.shared.set(pushToken: deviceToken) PushNotificationsManager.shared.set(pushToken: deviceToken)
} }
func initializeAppsFlyer() { func initializeAppsFlyer() {
appsFlyerLog.info("AppsFlyer initialize with AppsFlyerId: \(kAppsFlyerId), Apple App ID: \(kAppsFlyerAppId)") appsFlyerLog.info("AppsFlyer initialize with AppsFlyerId: \(kAppsFlyerId), Apple App ID: \(kAppsFlyerAppId)")
appsFlyer.appsFlyerDevKey = kAppsFlyerId appsFlyer.appsFlyerDevKey = kAppsFlyerId
...@@ -241,3 +251,35 @@ extension AppDelegate: DeepLinkDelegate { ...@@ -241,3 +251,35 @@ extension AppDelegate: DeepLinkDelegate {
router.open(url: url) router.open(url: url)
} }
} }
extension AppDelegate: SettingsDelegate {
func settingsDidChange() {
DispatchQueue.main.async {
// accessing appTheme right away causes a crash, because settingsDidChange is posted right after it was modified
if #available(iOS 13, *) {
switch self.settings.appTheme {
case .light:
UIApplication.shared.keyWindow?.overrideUserInterfaceStyle = .light
case .dark:
UIApplication.shared.keyWindow?.overrideUserInterfaceStyle = .dark
case .system:
UIApplication.shared.keyWindow?.overrideUserInterfaceStyle = .unspecified
}
}
}
}
}
extension AppDelegate: SettingsAppDelegate {
func openDeviceSettings() {
guard
let bundleIdentifier = Bundle.main.bundleIdentifier,
let appSettingsURL = URL(string: UIApplication.openSettingsURLString + bundleIdentifier)
else {
assert(false, "Failed to create settings URL from: \(UIApplication.openSettingsURLString)")
return
}
UIApplication.shared.open(appSettingsURL)
}
}
...@@ -51,7 +51,7 @@ class CCPAHelper { ...@@ -51,7 +51,7 @@ class CCPAHelper {
locationPermissionsUndeterminedOnLaunch && !hadLocationsOnLaunch locationPermissionsUndeterminedOnLaunch && !hadLocationsOnLaunch
} }
@UserDefaultsOptionalValue("CCPAHelper.canCollectData") @UserDefaultsOptionalValue("CCPAHelper.canCollectData", userDefaults: UserDefaults.appDefaults)
public var canCollectData: Bool? public var canCollectData: Bool?
private var locationPermissionsUndeterminedOnLaunch: Bool = false private var locationPermissionsUndeterminedOnLaunch: Bool = false
...@@ -70,7 +70,7 @@ class CCPAHelper { ...@@ -70,7 +70,7 @@ class CCPAHelper {
private static let statusUpdateDelayAfterRegistration: TimeInterval = 5 private static let statusUpdateDelayAfterRegistration: TimeInterval = 5
private static let defaultMinimumTimeSinceLastSuccessfulUpdate: TimeInterval = 3600 * 24 * 7 // a week private static let defaultMinimumTimeSinceLastSuccessfulUpdate: TimeInterval = 3600 * 24 * 7 // a week
@UserDefaultsValue("CCPAHelper.shownPrivacyNoticeBefore", defaultValue: false) @UserDefaultsValue("CCPAHelper.shownPrivacyNoticeBefore", defaultValue: false, userDefaults: UserDefaults.appDefaults)
public var shownPrivacyNoticeBefore: Bool { public var shownPrivacyNoticeBefore: Bool {
didSet { didSet {
if shownPrivacyNoticeBefore { if shownPrivacyNoticeBefore {
...@@ -79,7 +79,7 @@ class CCPAHelper { ...@@ -79,7 +79,7 @@ class CCPAHelper {
} }
} }
@UserDefaultsValue("CCPAHelper.policyHasBeenUpdated", defaultValue: false) @UserDefaultsValue("CCPAHelper.policyHasBeenUpdated", defaultValue: false, userDefaults: UserDefaults.appDefaults)
public private(set) var policyHasBeenUpdated: Bool { public private(set) var policyHasBeenUpdated: Bool {
didSet { didSet {
if policyHasBeenUpdated { if policyHasBeenUpdated {
...@@ -88,18 +88,18 @@ class CCPAHelper { ...@@ -88,18 +88,18 @@ class CCPAHelper {
} }
} }
@UserDefaultsOptionalValue("CCPAHelper.lastSeenPrivacyPolicyVersion") @UserDefaultsOptionalValue("CCPAHelper.lastSeenPrivacyPolicyVersion", userDefaults: UserDefaults.appDefaults)
public var lastSeenPrivacyPolicyVersion: Int? public var lastSeenPrivacyPolicyVersion: Int?
private let log = Logger(componentName: "CCPAHelper") private let log = Logger(componentName: "CCPAHelper")
@UserDefaultsValue("CCPAHelper.isRegistered", defaultValue: false) @UserDefaultsValue("CCPAHelper.isRegistered", defaultValue: false, userDefaults: UserDefaults.appDefaults)
private var isRegistered: Bool private var isRegistered: Bool
@UserDefaultsOptionalValue("CCPAHelper.privacyString") @UserDefaultsOptionalValue("CCPAHelper.privacyString", userDefaults: UserDefaults.appDefaults)
private var privacyString: String? private var privacyString: String?
@UserDefaultsOptionalValue("CCPAHelper.lastUpdateCallData") @UserDefaultsOptionalValue("CCPAHelper.lastUpdateCallData", userDefaults: UserDefaults.appDefaults)
private var lastSuccessfullUpdateDate: Date? private var lastSuccessfullUpdateDate: Date?
private var minimumTimeSinceLastSuccessfulUpdate: TimeInterval { private var minimumTimeSinceLastSuccessfulUpdate: TimeInterval {
......
...@@ -196,7 +196,7 @@ public class LocationsViewModel { ...@@ -196,7 +196,7 @@ public class LocationsViewModel {
//MARK:- City CUD methods //MARK:- City CUD methods
func add(city: PartialLocation) { func add(city: PartialLocation) {
guard locationManager.locations.count < locationManager.maxLocationsCount else { guard locationManager.locations.count < locationManager.maxLocationsCount else {
self.delegate?.viewModelError(model: self, title: nil, message: "search.error.maxLocationWarning".localized()) self.delegate?.viewModelError(model: self, title: nil, message: String(format:"search.error.maxLocationWarning".localized(), "\(locationManager.maxLocationsCount)"))
return return
} }
locationManager.addIfNeeded(partialLocation: city, selectLocation: true) { success in locationManager.addIfNeeded(partialLocation: city, selectLocation: true) { success in
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
// //
import UIKit import UIKit
import OneWeatherCore
import OneWeatherAnalytics import OneWeatherAnalytics
class SettingsViewController: UIViewController { class SettingsViewController: UIViewController {
...@@ -173,7 +174,7 @@ extension SettingsViewController: UITableViewDelegate { ...@@ -173,7 +174,7 @@ extension SettingsViewController: UITableViewDelegate {
case .other: case .other:
switch settingsCellFactory.rowTypeAt(indexPath: indexPath) { switch settingsCellFactory.rowTypeAt(indexPath: indexPath) {
case .locationAccess: case .locationAccess:
UIApplication.openSettings() Settings.shared.openDeviceSettings()
default: default:
break break
} }
......
...@@ -26,7 +26,7 @@ class PromotionPresentationAnimator: NSObject, UIViewControllerAnimatedTransitio ...@@ -26,7 +26,7 @@ class PromotionPresentationAnimator: NSObject, UIViewControllerAnimatedTransitio
height: contentHeight) height: contentHeight)
container.addSubview(toViewController.view) container.addSubview(toViewController.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext)) { UIView.animate(withDuration: transitionDuration(using: transitionContext)) {
toViewController.view.frame.origin.y = container.frame.height - contentHeight toViewController.view.frame.origin.y = container.bounds.height * 0.46
} completion: { finished in } completion: { finished in
transitionContext.completeTransition(finished) transitionContext.completeTransition(finished)
} }
......
...@@ -79,7 +79,7 @@ private extension PromotionSmallWidgetView { ...@@ -79,7 +79,7 @@ private extension PromotionSmallWidgetView {
func prepareTopLabel() { func prepareTopLabel() {
let select = "widget.promotion.selectList".localized() let select = "widget.promotion.selectList".localized()
let widgets = "16 \("widget.promotion.widgets".localized())" let widgets = "7 \("widget.promotion.widgets".localized())"
let attrString = NSMutableAttributedString(string: select + "\n" + widgets) let attrString = NSMutableAttributedString(string: select + "\n" + widgets)
attrString.addAttribute(.font, attrString.addAttribute(.font,
......
...@@ -20,12 +20,8 @@ class WidgetPromotionController: UIViewController { ...@@ -20,12 +20,8 @@ class WidgetPromotionController: UIViewController {
private let stackView = UIStackView() private let stackView = UIStackView()
private let footerView = UIView() private let footerView = UIView()
private let learnButton = UIButton() private let learnButton = UIButton()
private var initialTouchPoint = CGPoint(x: 0,y: 0) private let fullView: CGFloat = UIScreen.main.bounds.height - UIScreen.main.bounds.height * 0.9
private lazy var panGesture: UIPanGestureRecognizer = { private let partialView: CGFloat = UIScreen.main.bounds.height * 0.46
let gesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(sender:)))
gesture.delegate = self
return gesture
}()
//Public //Public
var controllerContentHeight: CGFloat { var controllerContentHeight: CGFloat {
...@@ -60,17 +56,13 @@ class WidgetPromotionController: UIViewController { ...@@ -60,17 +56,13 @@ class WidgetPromotionController: UIViewController {
updateUI() updateUI()
AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_PROMO_SEEN) AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_PROMO_SEEN)
#warning("--LOOK-")
//In this version the promo controller opens fully everytime
AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_PROMO_EXPAND)
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
if scrollView.frame.height > scrollView.contentSize.height { if scrollView.frame.height > scrollView.contentSize.height {
AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_BOTTOM_SCROLLED) AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_PROMO_BOTTOM_SCROLLED)
} }
} }
...@@ -99,26 +91,33 @@ class WidgetPromotionController: UIViewController { ...@@ -99,26 +91,33 @@ class WidgetPromotionController: UIViewController {
} }
} }
@objc private func handlePanGesture(sender: UIPanGestureRecognizer) { @objc private func handlePanGesture(recognizer: UIPanGestureRecognizer) {
let touchPoint = sender.location(in: self.view?.window) let translation = recognizer.translation(in: self.view)
let originalOffsetY = UIScreen.main.bounds.height - self.view.bounds.height let velocity = recognizer.velocity(in: self.view)
let y = self.view.frame.minY
switch sender.state {
case .began: switch recognizer.state {
initialTouchPoint = touchPoint
case .changed: case .changed:
if touchPoint.y - initialTouchPoint.y > 0 { self.view.frame = CGRect(x: 0, y: y + translation.y, width: view.frame.width, height: view.frame.height)
view.frame.origin.y = originalOffsetY + (touchPoint.y - initialTouchPoint.y) recognizer.setTranslation(CGPoint.zero, in: self.view)
case .ended:
var duration = velocity.y < 0 ? Double((y - fullView) / -velocity.y) : Double((partialView - y) / velocity.y )
duration = duration > 0.3 ? 0.3 : duration
if velocity.y >= 0 {
self.close()
} }
case .cancelled, .ended: else {
if touchPoint.y - initialTouchPoint.y > 80 { UIView.animate(withDuration: duration) {
close() self.view.frame = CGRect(x: 0, y: self.fullView, width: self.view.frame.width, height: self.view.frame.height)
} else { } completion: {[weak self] _ in
UIView.animate(withDuration: 0.25, animations: { if ( velocity.y < 0 ) {
self.view.frame.origin.y = originalOffsetY self?.scrollView.isScrollEnabled = true
}) AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_PROMO_EXPAND)
}
}
} }
break
default: default:
break break
} }
...@@ -135,13 +134,17 @@ class WidgetPromotionController: UIViewController { ...@@ -135,13 +134,17 @@ class WidgetPromotionController: UIViewController {
footerView.layer.shadowColor = UIColor(hex: 0xAAAAAA).cgColor footerView.layer.shadowColor = UIColor(hex: 0xAAAAAA).cgColor
} }
} }
var scrollViewObserver: NSKeyValueObservation?
} }
//MARK:- Prepare //MARK:- Prepare
@available(iOS 14, *) @available(iOS 14, *)
private extension WidgetPromotionController { private extension WidgetPromotionController {
func prepareView() { func prepareView() {
// view.addGestureRecognizer(panGesture) let gesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(recognizer:)))
gesture.delegate = self
view.addGestureRecognizer(gesture)
view.clipsToBounds = true view.clipsToBounds = true
view.layer.cornerRadius = 24 view.layer.cornerRadius = 24
...@@ -180,8 +183,18 @@ private extension WidgetPromotionController { ...@@ -180,8 +183,18 @@ private extension WidgetPromotionController {
} }
} }
func prepareScrollView() { func prepareScrollView() {
scrollView.delegate = self scrollViewObserver = scrollView.observe(\UIScrollView.contentOffset, options: .new) { [weak self] scrollView, valueChange in
guard let self = self else { return }
guard self.scrollView.contentSize.height > 10 && self.scrollView.frame.size.height > 10 else {
return
}
if self.scrollView.contentOffset.y >= self.scrollView.contentSize.height - self.scrollView.frame.height - 1 {
AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_PROMO_BOTTOM_SCROLLED)
self.scrollViewObserver = nil
}
}
view.addSubview(scrollView) view.addSubview(scrollView)
scrollView.snp.makeConstraints { make in scrollView.snp.makeConstraints { make in
make.left.right.equalToSuperview() make.left.right.equalToSuperview()
...@@ -199,9 +212,9 @@ private extension WidgetPromotionController { ...@@ -199,9 +212,9 @@ private extension WidgetPromotionController {
stackView.addArrangedSubview(PromotionWidgetViewWrapper(widgetType: .temperatureMedium)) stackView.addArrangedSubview(PromotionWidgetViewWrapper(widgetType: .temperatureMedium))
stackView.addArrangedSubview(PromotionWidgetViewWrapper(widgetType: .temperatureLarge)) stackView.addArrangedSubview(PromotionWidgetViewWrapper(widgetType: .temperatureLarge))
stackView.addArrangedSubview(PromotionWidgetViewWrapper(widgetType: .precipitationMedium)) stackView.addArrangedSubview(PromotionWidgetViewWrapper(widgetType: .precipitationMedium))
// stackView.addArrangedSubview(PromotionWidgetViewWrapper(widgetType: .windMedium)) stackView.addArrangedSubview(PromotionWidgetViewWrapper(widgetType: .windMedium))
// stackView.addArrangedSubview(PromotionWidgetViewWrapper(widgetType: .windLarge)) stackView.addArrangedSubview(PromotionWidgetViewWrapper(widgetType: .windLarge))
// stackView.addArrangedSubview(PromotionWidgetViewWrapper(widgetType: .radarLarge)) stackView.addArrangedSubview(PromotionWidgetViewWrapper(widgetType: .radarLarge))
scrollView.addSubview(stackView) scrollView.addSubview(stackView)
stackView.snp.makeConstraints { make in stackView.snp.makeConstraints { make in
...@@ -253,46 +266,20 @@ extension WidgetPromotionController: UIViewControllerTransitioningDelegate { ...@@ -253,46 +266,20 @@ extension WidgetPromotionController: UIViewControllerTransitioningDelegate {
} }
} }
//MARK:- UIScrollView Delegate
@available(iOS 14, *)
extension WidgetPromotionController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y == scrollView.contentSize.height - scrollView.frame.height {
AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_BOTTOM_SCROLLED)
}
// if scrollView.contentOffset.y < 0 {
// let originalOffsetY = UIScreen.main.bounds.height - self.view.bounds.height
// view.frame.origin.y = originalOffsetY - (scrollView.contentOffset.y - initialTouchPoint.y)
// return
// }
}
}
//MARK:- UIGesture Delegate //MARK:- UIGesture Delegate
@available(iOS 14, *) @available(iOS 14, *)
extension WidgetPromotionController: UIGestureRecognizerDelegate { extension WidgetPromotionController: UIGestureRecognizerDelegate {
// func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
// if scrollView.contentOffset.y > 0 { let gesture = (gestureRecognizer as! UIPanGestureRecognizer)
// return false let direction = gesture.velocity(in: view).y
// }
// let y = view.frame.minY
// scrollView.contentOffset.y = 0 if (y == fullView && scrollView.contentOffset.y == 0 && direction > 0) || (y == partialView) {
// return true scrollView.isScrollEnabled = false
// } } else {
scrollView.isScrollEnabled = true
// func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, }
// shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
// // Do not begin the pan until the swipe fails. return false
// if otherGestureRecognizer != self.panGesture { }
// print("ScrollView Offset \(scrollView.contentOffset.y)")
// if scrollView.contentOffset.y >= 0 {
// return false
// }
//
// return true
// }
//
// return false
// }
} }
...@@ -12,6 +12,7 @@ class CoreDataStack { ...@@ -12,6 +12,7 @@ class CoreDataStack {
private let log = Logger(componentName: "CoreDataStorage 💾") private let log = Logger(componentName: "CoreDataStorage 💾")
private let modelName = "1WModel" private let modelName = "1WModel"
private lazy var persistentContainer: NSPersistentContainer = { private lazy var persistentContainer: NSPersistentContainer = {
log.verbose("Make persistant container")
var container: NSPersistentContainer var container: NSPersistentContainer
if if
let modelURL = Bundle(for: CoreDataStorage.self).url(forResource: "1WModel", withExtension: "momd"), let modelURL = Bundle(for: CoreDataStorage.self).url(forResource: "1WModel", withExtension: "momd"),
...@@ -42,55 +43,141 @@ class CoreDataStack { ...@@ -42,55 +43,141 @@ class CoreDataStack {
return containerUrl.appendingPathComponent("\(modelName).sqlite") return containerUrl.appendingPathComponent("\(modelName).sqlite")
} }
private func isInWidget() -> Bool {
guard let extesion = Bundle.main.infoDictionary?["NSExtension"] as? [String: String] else { return false }
guard let widget = extesion["NSExtensionPointIdentifier"] else { return false }
return widget == "com.apple.widgetkit-extension"
}
/// The Persistent Store used to be in the app directory, but was then moved to an app group container to be shared with widgets. /// The Persistent Store used to be in the app directory, but was then moved to an app group container to be shared with widgets.
/// For all users who still have it in the old location, we need to move it to the new one. /// For all users who still have it in the old location, we need to move it to the new one.
private func movePersistentStoreIfNeeded() { private func movePersistentStoreIfNeeded() {
log.verbose("Move store: start")
let legacyStoreUrl = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("\(modelName).sqlite") let legacyStoreUrl = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("\(modelName).sqlite")
guard let storeUrl = self.storeUrl else { guard let storeUrl = self.storeUrl else {
return return
} }
let fileManager = FileManager.default let fileManager = FileManager.default
log.verbose("\nLegacy path: \(legacyStoreUrl.path)\nNew path: \(storeUrl.path)")
log.verbose("Move store: will move: \(fileManager.fileExists(atPath: legacyStoreUrl.path) && !fileManager.fileExists(atPath: storeUrl.path)) old exists: \(fileManager.fileExists(atPath: legacyStoreUrl.path)) new exists: \(fileManager.fileExists(atPath: storeUrl.path))")
guard fileManager.fileExists(atPath: legacyStoreUrl.path) && !fileManager.fileExists(atPath: storeUrl.path) else { guard fileManager.fileExists(atPath: legacyStoreUrl.path) && !fileManager.fileExists(atPath: storeUrl.path) else {
log.verbose("Move store: won't do. return.")
return return
} }
do { do {
log.info("Moving the persistent store to the app group location.") log.info("Moving the persistent store to the app group location.")
try fileManager.moveItem(at: legacyStoreUrl, to: storeUrl) try fileManager.moveItem(at: legacyStoreUrl, to: storeUrl)
let additionalFileSuffixes = ["-wal", "-shm"]
for additionalSuffix in additionalFileSuffixes {
let oldPath = legacyStoreUrl.path + additionalSuffix
if fileManager.fileExists(atPath: oldPath) {
let newPath = storeUrl.path + additionalSuffix
log.debug("Move \(oldPath)")
try fileManager.moveItem(atPath: oldPath, toPath: newPath)
}
}
} }
catch { catch {
log.error("Error moving the model from the old location to the new one: \(error)") log.error("Error moving the model from the old location to the new one: \(error)")
} }
log.verbose("Move store: done")
} }
public func getManagedObjectContext(_ completion: @escaping (NSManagedObjectContext?) -> ()) { public func getManagedObjectContext(_ completion: @escaping (NSManagedObjectContext?) -> ()) {
log.verbose("get context: start")
if let existingContext = self.managedContext { if let existingContext = self.managedContext {
log.verbose("get context: got existing")
completion(existingContext) completion(existingContext)
return return
} }
else { else {
initializationSemaphore.wait() log.verbose("get context: no existing")
defer { guard shouldInit() else {
initializationSemaphore.signal() log.verbose("get context: NIL (shouldn't init)")
completion(nil)
return
}
if isInWidget() {
log.verbose("get context: WIDGET. Start wait")
initializationSemaphore.wait()
log.verbose("get context: WIDGET. Resume wait")
if !initializing {
log.debug("get context: WIDGET. Initialize stack...")
initializationSemaphore.signal()
initializeStack()
}
else {
log.verbose("get context: WIDGET. Signal!")
initializationSemaphore.signal()
}
} }
log.verbose("get context: start wait")
initializationSemaphore.wait()
log.verbose("get context: resume wait")
if self.managedContext == nil { if self.managedContext == nil {
self.managedContext = self.persistentContainer.newBackgroundContext() self.managedContext = self.persistentContainer.newBackgroundContext()
log.verbose("get context: initialized (\(self.managedContext == nil ? "fail" : "success"))")
} }
log.verbose("get context: SIGNAL")
initializationSemaphore.signal()
log.verbose("get context: completion")
completion(self.managedContext) completion(self.managedContext)
} }
} }
public init() {
private func shouldInit() -> Bool {
log.verbose("should init: start")
guard isInWidget() else {
log.verbose("should init: true (app)")
return true
}
guard let storeUrl = self.storeUrl else {
log.verbose("should init: false (no url)")
return false
}
let fileManager = FileManager.default
let storeExists = fileManager.fileExists(atPath: storeUrl.path)
log.verbose("should init: \(storeExists) (store exists)")
return storeExists
}
private var initializing = false
private func initializeStack() {
let log = self.log
log.verbose("Initialize stack (start wait)")
initializationSemaphore.wait() initializationSemaphore.wait()
log.verbose("Initialize stack (resume wait)")
initializing = true
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
log.verbose("Initialize stack: async start")
self.movePersistentStoreIfNeeded() self.movePersistentStoreIfNeeded()
log.verbose("Initialize stack: load stores")
self.persistentContainer.loadPersistentStores { [weak self] (description, error) in self.persistentContainer.loadPersistentStores { [weak self] (description, error) in
defer { if self == nil {
self?.initializationSemaphore.signal() log.verbose("Initialize stack: SELF IS NIL")
} }
log.verbose("Initialize stack: signal")
self?.initializationSemaphore.signal()
log.verbose("Initialize stack: signal (done)")
if let error = error { if let error = error {
self?.log.error("Error loading persistent stores: \(error)") log.error("Error loading persistent stores: \(error)")
} }
} }
} }
} }
public init() {
log.verbose("Init")
if shouldInit() {
initializeStack()
}
}
deinit {
log.verbose("Deinit")
}
} }
...@@ -57,11 +57,12 @@ public class CoreDataStorage: Storage { ...@@ -57,11 +57,12 @@ public class CoreDataStorage: Storage {
public func load(completion: @escaping StorageCompletion) { public func load(completion: @escaping StorageCompletion) {
log.info("Load: start") log.info("Load: start")
stack.getManagedObjectContext { [weak self] context in stack.getManagedObjectContext { [weak self] context in
context?.perform { guard let context = context else {
completion([], nil, nil)
return
}
context.perform {
guard let self = self else { return } guard let self = self else { return }
guard let context = context else {
return
}
let completionWithErrorHandling: StorageCompletion = { [weak self] (locations, selectedIndex, error) in let completionWithErrorHandling: StorageCompletion = { [weak self] (locations, selectedIndex, error) in
if error != nil { if error != nil {
self?.log.error("Load: error.") self?.log.error("Load: error.")
......
...@@ -92,7 +92,7 @@ public enum AnalyticsEvent: String { ...@@ -92,7 +92,7 @@ public enum AnalyticsEvent: String {
case ANALYTICS_WIDGET_PROMO_SEEN = "WIDGET_PROMO_SEEN" case ANALYTICS_WIDGET_PROMO_SEEN = "WIDGET_PROMO_SEEN"
case ANALYTICS_WIDGET_PROMO_DISMISS = "WIDGET_PROMO_DISMISS" case ANALYTICS_WIDGET_PROMO_DISMISS = "WIDGET_PROMO_DISMISS"
case ANALYTICS_WIDGET_PROMO_EXPAND = "WIDGET_PROMO_EXPAND" case ANALYTICS_WIDGET_PROMO_EXPAND = "WIDGET_PROMO_EXPAND"
case ANALYTICS_WIDGET_BOTTOM_SCROLLED = "WIDGET_PROMO_BOTTOM_SCROLLED" case ANALYTICS_WIDGET_PROMO_BOTTOM_SCROLLED = "WIDGET_PROMO_BOTTOM_SCROLLED"
case ANALYTICS_WIDGET_PROMO_LEARN_CTA = "WIDGET_PROMO_LEARN_CTA" case ANALYTICS_WIDGET_PROMO_LEARN_CTA = "WIDGET_PROMO_LEARN_CTA"
case ANALYTICS_WIDGET_LAUNCH_FROM = "LAUNCH_FROM_WIDGET" case ANALYTICS_WIDGET_LAUNCH_FROM = "LAUNCH_FROM_WIDGET"
......
...@@ -23,6 +23,7 @@ public enum AnalyticsParameter: String { ...@@ -23,6 +23,7 @@ public enum AnalyticsParameter: String {
case ANALYTICS_KEY_FIRST_OPEN_SOURCE = "Source" case ANALYTICS_KEY_FIRST_OPEN_SOURCE = "Source"
case ANALYTICS_KEY_FIRST_OPEN_FLOW = "Flow" case ANALYTICS_KEY_FIRST_OPEN_FLOW = "Flow"
case ANALYTICS_KEY_WIDGET_NAME = "widget_name" case ANALYTICS_KEY_WIDGET_NAME = "widget_name"
case ANALYTICS_KEY_WIDGET_SMART_TEXT = "smart_text"
case ANALYTICS_KEY_SHORTS_CARD_POSITION = "position" case ANALYTICS_KEY_SHORTS_CARD_POSITION = "position"
case ANALYTICS_KEY_SHORTS_CARD_ID = "card_id" case ANALYTICS_KEY_SHORTS_CARD_ID = "card_id"
case ANALYTICS_KEY_SHORTS_TIME_SPENT = "time_spent" case ANALYTICS_KEY_SHORTS_TIME_SPENT = "time_spent"
......
...@@ -57,7 +57,6 @@ ...@@ -57,7 +57,6 @@
CD615FC32655295C00B717DB /* Dimension+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD615FB02655293100B717DB /* Dimension+Name.swift */; }; CD615FC32655295C00B717DB /* Dimension+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD615FB02655293100B717DB /* Dimension+Name.swift */; };
CD615FC42655295C00B717DB /* UnitPressure+Atmosphere.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD615FB12655293100B717DB /* UnitPressure+Atmosphere.swift */; }; CD615FC42655295C00B717DB /* UnitPressure+Atmosphere.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD615FB12655293100B717DB /* UnitPressure+Atmosphere.swift */; };
CD615FC52655295C00B717DB /* CLAuthorizationStatus+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD615FB22655293100B717DB /* CLAuthorizationStatus+Localized.swift */; }; CD615FC52655295C00B717DB /* CLAuthorizationStatus+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD615FB22655293100B717DB /* CLAuthorizationStatus+Localized.swift */; };
CD615FC62655295C00B717DB /* UIApplication+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD615FB32655293100B717DB /* UIApplication+Settings.swift */; };
CD6C22F026677E0200D75659 /* PushNotificationsManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6C22EF26677E0200D75659 /* PushNotificationsManagerProtocol.swift */; }; CD6C22F026677E0200D75659 /* PushNotificationsManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6C22EF26677E0200D75659 /* PushNotificationsManagerProtocol.swift */; };
CD71B9C6265E629D00803DBB /* String+NewLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD71B9C5265E629D00803DBB /* String+NewLine.swift */; }; CD71B9C6265E629D00803DBB /* String+NewLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD71B9C5265E629D00803DBB /* String+NewLine.swift */; };
CD8E48A526651414008E7F8D /* NWSCurrentEventsReponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD8E48A426651414008E7F8D /* NWSCurrentEventsReponse.swift */; }; CD8E48A526651414008E7F8D /* NWSCurrentEventsReponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD8E48A426651414008E7F8D /* NWSCurrentEventsReponse.swift */; };
...@@ -77,6 +76,7 @@ ...@@ -77,6 +76,7 @@
CDFE458D26566BD50021A29F /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDFE458C26566BD50021A29F /* Storage.swift */; }; CDFE458D26566BD50021A29F /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDFE458C26566BD50021A29F /* Storage.swift */; };
CDFE459426566D7B0021A29F /* HealthSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDFE459326566D7B0021A29F /* HealthSource.swift */; }; CDFE459426566D7B0021A29F /* HealthSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDFE459326566D7B0021A29F /* HealthSource.swift */; };
CDFE459626566D860021A29F /* FIPSSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDFE459526566D860021A29F /* FIPSSource.swift */; }; CDFE459626566D860021A29F /* FIPSSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDFE459526566D860021A29F /* FIPSSource.swift */; };
CE3A112726CD3CDE00D925C7 /* UserDefaults+OneWeather.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3A112626CD3CDE00D925C7 /* UserDefaults+OneWeather.swift */; };
CEFE851826948C15003C67D3 /* SmartTextProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFE851726948C15003C67D3 /* SmartTextProvider.swift */; }; CEFE851826948C15003C67D3 /* SmartTextProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFE851726948C15003C67D3 /* SmartTextProvider.swift */; };
CEFE851C2694986D003C67D3 /* SmartText.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFE851B2694986D003C67D3 /* SmartText.swift */; }; CEFE851C2694986D003C67D3 /* SmartText.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFE851B2694986D003C67D3 /* SmartText.swift */; };
CEFE85202694C4BC003C67D3 /* DefaultSmartText.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFE851F2694C4BC003C67D3 /* DefaultSmartText.swift */; }; CEFE85202694C4BC003C67D3 /* DefaultSmartText.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFE851F2694C4BC003C67D3 /* DefaultSmartText.swift */; };
...@@ -150,7 +150,6 @@ ...@@ -150,7 +150,6 @@
CD615FB02655293100B717DB /* Dimension+Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dimension+Name.swift"; sourceTree = "<group>"; }; CD615FB02655293100B717DB /* Dimension+Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dimension+Name.swift"; sourceTree = "<group>"; };
CD615FB12655293100B717DB /* UnitPressure+Atmosphere.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UnitPressure+Atmosphere.swift"; sourceTree = "<group>"; }; CD615FB12655293100B717DB /* UnitPressure+Atmosphere.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UnitPressure+Atmosphere.swift"; sourceTree = "<group>"; };
CD615FB22655293100B717DB /* CLAuthorizationStatus+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CLAuthorizationStatus+Localized.swift"; sourceTree = "<group>"; }; CD615FB22655293100B717DB /* CLAuthorizationStatus+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CLAuthorizationStatus+Localized.swift"; sourceTree = "<group>"; };
CD615FB32655293100B717DB /* UIApplication+Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Settings.swift"; sourceTree = "<group>"; };
CD615FCD265529DE00B717DB /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; }; CD615FCD265529DE00B717DB /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
CD615FCE265529DE00B717DB /* DefaultSettingsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultSettingsFactory.swift; sourceTree = "<group>"; }; CD615FCE265529DE00B717DB /* DefaultSettingsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultSettingsFactory.swift; sourceTree = "<group>"; };
CD615FCF265529DE00B717DB /* DefaultSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultSettings.swift; sourceTree = "<group>"; }; CD615FCF265529DE00B717DB /* DefaultSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultSettings.swift; sourceTree = "<group>"; };
...@@ -176,6 +175,7 @@ ...@@ -176,6 +175,7 @@
CDFE458C26566BD50021A29F /* Storage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; }; CDFE458C26566BD50021A29F /* Storage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
CDFE459326566D7B0021A29F /* HealthSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthSource.swift; sourceTree = "<group>"; }; CDFE459326566D7B0021A29F /* HealthSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthSource.swift; sourceTree = "<group>"; };
CDFE459526566D860021A29F /* FIPSSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FIPSSource.swift; sourceTree = "<group>"; }; CDFE459526566D860021A29F /* FIPSSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FIPSSource.swift; sourceTree = "<group>"; };
CE3A112626CD3CDE00D925C7 /* UserDefaults+OneWeather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+OneWeather.swift"; sourceTree = "<group>"; };
CEFE851726948C15003C67D3 /* SmartTextProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartTextProvider.swift; sourceTree = "<group>"; }; CEFE851726948C15003C67D3 /* SmartTextProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartTextProvider.swift; sourceTree = "<group>"; };
CEFE851B2694986D003C67D3 /* SmartText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartText.swift; sourceTree = "<group>"; }; CEFE851B2694986D003C67D3 /* SmartText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartText.swift; sourceTree = "<group>"; };
CEFE851D2694C477003C67D3 /* SmartTextMacro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartTextMacro.swift; sourceTree = "<group>"; }; CEFE851D2694C477003C67D3 /* SmartTextMacro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartTextMacro.swift; sourceTree = "<group>"; };
...@@ -376,8 +376,8 @@ ...@@ -376,8 +376,8 @@
CD615FB02655293100B717DB /* Dimension+Name.swift */, CD615FB02655293100B717DB /* Dimension+Name.swift */,
CD615FB12655293100B717DB /* UnitPressure+Atmosphere.swift */, CD615FB12655293100B717DB /* UnitPressure+Atmosphere.swift */,
CD615FB22655293100B717DB /* CLAuthorizationStatus+Localized.swift */, CD615FB22655293100B717DB /* CLAuthorizationStatus+Localized.swift */,
CD615FB32655293100B717DB /* UIApplication+Settings.swift */,
CD71B9C5265E629D00803DBB /* String+NewLine.swift */, CD71B9C5265E629D00803DBB /* String+NewLine.swift */,
CE3A112626CD3CDE00D925C7 /* UserDefaults+OneWeather.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -667,12 +667,12 @@ ...@@ -667,12 +667,12 @@
CD615FC32655295C00B717DB /* Dimension+Name.swift in Sources */, CD615FC32655295C00B717DB /* Dimension+Name.swift in Sources */,
CD615FC42655295C00B717DB /* UnitPressure+Atmosphere.swift in Sources */, CD615FC42655295C00B717DB /* UnitPressure+Atmosphere.swift in Sources */,
CD615FC52655295C00B717DB /* CLAuthorizationStatus+Localized.swift in Sources */, CD615FC52655295C00B717DB /* CLAuthorizationStatus+Localized.swift in Sources */,
CD615FC62655295C00B717DB /* UIApplication+Settings.swift in Sources */,
CEFE85202694C4BC003C67D3 /* DefaultSmartText.swift in Sources */, CEFE85202694C4BC003C67D3 /* DefaultSmartText.swift in Sources */,
CD8E48A526651414008E7F8D /* NWSCurrentEventsReponse.swift in Sources */, CD8E48A526651414008E7F8D /* NWSCurrentEventsReponse.swift in Sources */,
CD427D19266F5DCE00B4350A /* ShortsSource.swift in Sources */, CD427D19266F5DCE00B4350A /* ShortsSource.swift in Sources */,
CD2D55D8265533F4007B70F4 /* UserDefaultsWrapper.swift in Sources */, CD2D55D8265533F4007B70F4 /* UserDefaultsWrapper.swift in Sources */,
CDD2F8F62665117400B48322 /* NWSAlertsManager.swift in Sources */, CDD2F8F62665117400B48322 /* NWSAlertsManager.swift in Sources */,
CE3A112726CD3CDE00D925C7 /* UserDefaults+OneWeather.swift in Sources */,
CD11AFE726651BF900EC4BA0 /* LegacyWdtLocation.swift in Sources */, CD11AFE726651BF900EC4BA0 /* LegacyWdtLocation.swift in Sources */,
CD11AFE326651B6300EC4BA0 /* LegacyMigrationManager.swift in Sources */, CD11AFE326651B6300EC4BA0 /* LegacyMigrationManager.swift in Sources */,
CD2D55DD2655377F007B70F4 /* NWSAlertExtendedInfo.swift in Sources */, CD2D55DD2655377F007B70F4 /* NWSAlertExtendedInfo.swift in Sources */,
......
...@@ -11,18 +11,20 @@ import Foundation ...@@ -11,18 +11,20 @@ import Foundation
@propertyWrapper @propertyWrapper
public struct UserDefaultsOptionalValue<T> { public struct UserDefaultsOptionalValue<T> {
public let key: String public let key: String
private let userDefaults: UserDefaults
public init(_ key: String) { public init(_ key: String, userDefaults: UserDefaults) {
self.key = key self.key = key
self.userDefaults = userDefaults
} }
public var wrappedValue: T? { public var wrappedValue: T? {
get { get {
return UserDefaults.standard.object(forKey: key) as? T return userDefaults.object(forKey: key) as? T
} }
set { set {
UserDefaults.standard.set(newValue, forKey: key) userDefaults.set(newValue, forKey: key)
UserDefaults.standard.synchronize() userDefaults.synchronize()
} }
} }
} }
...@@ -12,19 +12,21 @@ import Foundation ...@@ -12,19 +12,21 @@ import Foundation
public struct UserDefaultsValue<T> { public struct UserDefaultsValue<T> {
public let key: String public let key: String
public let defaultValue: T public let defaultValue: T
private let userDefaults: UserDefaults
public init(_ key: String, defaultValue: T) { public init(_ key: String, defaultValue: T, userDefaults: UserDefaults) {
self.key = key self.key = key
self.defaultValue = defaultValue self.defaultValue = defaultValue
self.userDefaults = userDefaults
} }
public var wrappedValue: T { public var wrappedValue: T {
get { get {
return (UserDefaults.standard.object(forKey: key) as? T) ?? defaultValue return (userDefaults.object(forKey: key) as? T) ?? defaultValue
} }
set { set {
UserDefaults.standard.set(newValue, forKey: key) userDefaults.set(newValue, forKey: key)
UserDefaults.standard.synchronize() userDefaults.synchronize()
} }
} }
} }
...@@ -6,9 +6,21 @@ ...@@ -6,9 +6,21 @@
// //
import Foundation import Foundation
import WidgetKit
@propertyWrapper @propertyWrapper
public struct UserDefaultsUnitValue<T> { public struct UserDefaultsUnitValue<T> {
private let key: String
private let defaultValue: T
private var lastKnownValue: T?
private let userDefaults: UserDefaults
public init(wrappedValue defaultValue:T, key:String, userDefaults: UserDefaults) {
self.defaultValue = defaultValue
self.key = key
self.userDefaults = userDefaults
}
public var wrappedValue: T { public var wrappedValue: T {
get { get {
// TODO: If we will ever have 2 instances of UserDefaultsUnitValue with the same key at the same time, this cache will become desynchronized. // TODO: If we will ever have 2 instances of UserDefaultsUnitValue with the same key at the same time, this cache will become desynchronized.
...@@ -16,7 +28,7 @@ public struct UserDefaultsUnitValue<T> { ...@@ -16,7 +28,7 @@ public struct UserDefaultsUnitValue<T> {
return knownValue return knownValue
} }
guard guard
let data = UserDefaults.standard.data(forKey: key), let data = userDefaults.data(forKey: key),
let decoded = NSKeyedUnarchiver.unarchiveObject(with: data) as? T let decoded = NSKeyedUnarchiver.unarchiveObject(with: data) as? T
else { else {
return defaultValue return defaultValue
...@@ -27,46 +39,46 @@ public struct UserDefaultsUnitValue<T> { ...@@ -27,46 +39,46 @@ public struct UserDefaultsUnitValue<T> {
set { set {
lastKnownValue = newValue lastKnownValue = newValue
let data = NSKeyedArchiver.archivedData(withRootObject: newValue) let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
UserDefaults.standard.set(data, forKey: key) userDefaults.set(data, forKey: key)
UserDefaults.standard.synchronize()
Settings.shared.delegate.invoke { (delegate) in Settings.shared.delegate.invoke { (delegate) in
delegate.settingsDidChange() delegate.settingsDidChange()
} }
if #available(iOS 14, *) {
WidgetCenter.shared.reloadAllTimelines()
}
} }
} }
}
@propertyWrapper
public struct UserDefaultsBasicValue<T> {
private let key: String private let key: String
private let defaultValue: T private let defaultValue: T
private var lastKnownValue: T? private let userDefaults: UserDefaults
init(wrappedValue defaultValue:T, key:String) { public init(wrappedValue defaultValue:T, key:String, userDefaults: UserDefaults) {
self.defaultValue = defaultValue self.defaultValue = defaultValue
self.key = key self.key = key
self.userDefaults = userDefaults
} }
}
@propertyWrapper
public struct UserDefaultsBasicValue<T> {
public var wrappedValue: T { public var wrappedValue: T {
get { get {
let value = UserDefaults.standard.value(forKey: key) as? T let value = userDefaults.value(forKey: key) as? T
return value ?? defaultValue return value ?? defaultValue
} }
set { set {
UserDefaults.standard.setValue(newValue, forKey: key) userDefaults.setValue(newValue, forKey: key)
UserDefaults.standard.synchronize() Settings.shared.delegate.invoke { (delegate) in
delegate.settingsDidChange()
}
if #available(iOS 14, *) {
WidgetCenter.shared.reloadAllTimelines()
}
} }
} }
private let key: String
private let defaultValue: T
init(wrappedValue defaultValue:T, key:String) {
self.defaultValue = defaultValue
self.key = key
}
} }
//
// UIApplication+Settings.swift
// 1Weather
//
// Created by Dmitry Stepanets on 02.04.2021.
//
import UIKit
public extension UIApplication {
static func openSettings() {
guard
let bundleIdentifier = Bundle.main.bundleIdentifier,
let appSettingsURL = URL(string: UIApplication.openSettingsURLString + bundleIdentifier)
else {
assert(false, "Failed to create settings URL from: \(UIApplication.openSettingsURLString)")
return
}
UIApplication.shared.open(appSettingsURL)
}
}
//
// UserDefaults+OneWeather.swift
// OneWeatherCore
//
// Created by Demid Merzlyakov on 18.08.2021.
//
import Foundation
import OneWeatherAnalytics
extension UserDefaults {
private static let appGroup = "group.com.onelouder.oneweather"
public static var appDefaults: UserDefaults {
if let appGroupDefaults = UserDefaults(suiteName: UserDefaults.appGroup) {
return appGroupDefaults
}
// At least it won't crash, but hopefully we'll never end up here.
assertionFailure("Failed to create a shared user defaults.")
return UserDefaults.standard
}
public static func migrateUserDefaultsToAppGroupsIfNeeded() {
// User Defaults - Old
let userDefaults = UserDefaults.standard
// App Groups Default - New
let groupDefaults = UserDefaults.appDefaults
// Key to track if we migrated
let didMigrateToAppGroups = "DidMigrateToAppGroup_\(UserDefaults.appGroup)"
let log = Logger(componentName: "UserDefaults")
if !groupDefaults.bool(forKey: didMigrateToAppGroups) {
for key in userDefaults.dictionaryRepresentation().keys {
groupDefaults.set(userDefaults.dictionaryRepresentation()[key], forKey: key)
}
groupDefaults.set(true, forKey: didMigrateToAppGroups)
groupDefaults.synchronize()
log.info("Successfully migrated defaults")
} else {
log.info("No need to migrate defaults")
}
}
}
...@@ -138,37 +138,37 @@ public enum WindDirection: String, Codable, CaseIterable { ...@@ -138,37 +138,37 @@ public enum WindDirection: String, Codable, CaseIterable {
public var degrees: CGFloat { public var degrees: CGFloat {
switch self { switch self {
case .north: case .north:
return 0 return 180
case .northNorthEast: case .northNorthEast:
return 22.5 return 202.5
case .northEast: case .northEast:
return 45 return 225
case .eastNorthEast: case .eastNorthEast:
return 67.5 return 247.5
case .east: case .east:
return 90 return 270
case .eastSouthEast: case .eastSouthEast:
return 112.5 return 292.5
case .southEast: case .southEast:
return 135 return 315
case .southSouthEast: case .southSouthEast:
return 157.5 return 337.5
case .south: case .south:
return 180 return 0
case .southSouthWest: case .southSouthWest:
return 202.5 return 22.5
case .southWest: case .southWest:
return 225 return 45
case .westSouthWest: case .westSouthWest:
return 247.5 return 67.5
case .west: case .west:
return 270 return 90
case .westNorthWest: case .westNorthWest:
return 292.5 return 112.5
case .northWest: case .northWest:
return 315 return 135
case .northNorthWest: case .northNorthWest:
return 337.5 return 157.5
} }
} }
......
...@@ -104,10 +104,11 @@ public class WidgetManager { ...@@ -104,10 +104,11 @@ public class WidgetManager {
public static let shared = WidgetManager() public static let shared = WidgetManager()
private init() { private init() {
//Load stored options //Load stored options
let storedWidgetsOptions = UserDefaults.standard.integer(forKey: "widgetOptions") let storedWidgetsOptions = UserDefaults.appDefaults.integer(forKey: "widgetOptions")
self.widgetOptions = WidgetOptions(rawValue: storedWidgetsOptions) self.widgetOptions = WidgetOptions(rawValue: storedWidgetsOptions)
} }
private let log = Logger(componentName: "WidgetManager") private let log = Logger(componentName: "WidgetManager")
private let smartTextProvider = SmartTextProvider()
private var widgetOptions: WidgetOptions = [] private var widgetOptions: WidgetOptions = []
public func refreshAnalytics() { public func refreshAnalytics() {
...@@ -128,25 +129,28 @@ public class WidgetManager { ...@@ -128,25 +129,28 @@ public class WidgetManager {
let name = WidgetOptions.name(for: option) let name = WidgetOptions.name(for: option)
AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_CARD_CLICK) AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_CARD_CLICK)
AppAnalytics.shared.log(event: .ANALYTICS_LAUNCH_FROM_WIDGET) AppAnalytics.shared.log(event: .ANALYTICS_LAUNCH_FROM_WIDGET,
params: [.ANALYTICS_KEY_WIDGET_NAME : name])
AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_LAUNCH_FROM, AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_LAUNCH_FROM,
params: [.ANALYTICS_KEY_WIDGET_NAME : name]) params: [.ANALYTICS_KEY_WIDGET_NAME : name])
} }
public func logUpdate() { public func logUpdate(forLocation location: Location?, kind: String, family: WidgetFamily) {
WidgetCenter.shared.getCurrentConfigurations {[weak self] result in if let option = WidgetOptions.option(forKind: kind, family: family) {
switch result { let name = WidgetOptions.name(for: option)
case .success(let widgetInfo): var params: [AnalyticsParameter : String] = [.ANALYTICS_KEY_WIDGET_NAME : name]
for info in widgetInfo {
if let option = WidgetOptions.option(forKind: info.kind, family: info.family) { // Radar widget doesn't have smart text (IOS-260).
let name = WidgetOptions.name(for: option) // TODO: this needs to be unified. Any widget not using smart text shouldn't have that parameter.
AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_UPDATED, if option != .radarLarge {
params: [.ANALYTICS_KEY_WIDGET_NAME : name]) if let updatedLocation = location {
} let smartText = smartTextProvider.smartText(for: updatedLocation)
params[.ANALYTICS_KEY_WIDGET_SMART_TEXT] = #"\#(smartText)"#
} }
case .failure(let error):
self?.log.error(error.localizedDescription)
} }
AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_UPDATED,
params: params)
} }
} }
...@@ -209,8 +213,8 @@ public class WidgetManager { ...@@ -209,8 +213,8 @@ public class WidgetManager {
} }
private func save(options: WidgetOptions) { private func save(options: WidgetOptions) {
UserDefaults.standard.setValue(options.rawValue, forKey: "widgetOptions") UserDefaults.appDefaults.setValue(options.rawValue, forKey: "widgetOptions")
UserDefaults.standard.synchronize() UserDefaults.appDefaults.synchronize()
} }
} }
......
...@@ -174,7 +174,7 @@ internal class DeviceLocationMonitor: NSObject { ...@@ -174,7 +174,7 @@ internal class DeviceLocationMonitor: NSObject {
let alertGoToSettings = UIAlertController(title: "location.goToSettings.title".localized(), message: text, preferredStyle: .alert) let alertGoToSettings = UIAlertController(title: "location.goToSettings.title".localized(), message: text, preferredStyle: .alert)
let actionGoToSettings = UIAlertAction(title: "location.goToSettings.goToSettingsAction".localized(), style: .default) {(_) in let actionGoToSettings = UIAlertAction(title: "location.goToSettings.goToSettingsAction".localized(), style: .default) {(_) in
UIApplication.openSettings() Settings.shared.openDeviceSettings()
completion(.denied) completion(.denied)
} }
......
...@@ -12,6 +12,10 @@ public protocol SettingsDelegate: AnyObject { ...@@ -12,6 +12,10 @@ public protocol SettingsDelegate: AnyObject {
func settingsDidChange() func settingsDidChange()
} }
public protocol SettingsAppDelegate: AnyObject {
func openDeviceSettings()
}
public enum AppTheme: Int { public enum AppTheme: Int {
case light = 0 case light = 0
case dark case dark
...@@ -32,9 +36,10 @@ public enum AppTheme: Int { ...@@ -32,9 +36,10 @@ public enum AppTheme: Int {
public class Settings { public class Settings {
public static let shared = Settings() public static let shared = Settings()
public let delegate = MulticastDelegate<SettingsDelegate>() public let delegate = MulticastDelegate<SettingsDelegate>()
public weak var appDelegate: SettingsAppDelegate?
private init() {} private init() {}
@UserDefaultsBasicValue(key: "app_theme") @UserDefaultsBasicValue(key: "app_theme", userDefaults: UserDefaults.appDefaults)
private var _appTheme = AppTheme.system.rawValue private var _appTheme = AppTheme.system.rawValue
public var appTheme:AppTheme { public var appTheme:AppTheme {
...@@ -43,65 +48,57 @@ public class Settings { ...@@ -43,65 +48,57 @@ public class Settings {
} }
set { set {
_appTheme = newValue.rawValue _appTheme = newValue.rawValue
if #available(iOS 13, *) {
switch newValue {
case .light:
UIApplication.shared.keyWindow?.overrideUserInterfaceStyle = .light
case .dark:
UIApplication.shared.keyWindow?.overrideUserInterfaceStyle = .dark
case .system:
UIApplication.shared.keyWindow?.overrideUserInterfaceStyle = .unspecified
}
}
Settings.shared.delegate.invoke { (delegate) in Settings.shared.delegate.invoke { (delegate) in
delegate.settingsDidChange() delegate.settingsDidChange()
} }
} }
} }
@UserDefaultsUnitValue(key: "temperature_type") public func openDeviceSettings() {
appDelegate?.openDeviceSettings()
}
@UserDefaultsUnitValue(key: "temperature_type", userDefaults: UserDefaults.appDefaults)
public var temperatureType = DefaultSettingsFactory().getSettings().temperatureType public var temperatureType = DefaultSettingsFactory().getSettings().temperatureType
@UserDefaultsUnitValue(key: "wind_speed_type") @UserDefaultsUnitValue(key: "wind_speed_type", userDefaults: UserDefaults.appDefaults)
public var windSpeedType = DefaultSettingsFactory().getSettings().windSpeedType public var windSpeedType = DefaultSettingsFactory().getSettings().windSpeedType
@UserDefaultsUnitValue(key: "pressure_type") @UserDefaultsUnitValue(key: "pressure_type", userDefaults: UserDefaults.appDefaults)
public var pressureType = DefaultSettingsFactory().getSettings().pressureType public var pressureType = DefaultSettingsFactory().getSettings().pressureType
@UserDefaultsUnitValue(key: "distance_type") @UserDefaultsUnitValue(key: "distance_type", userDefaults: UserDefaults.appDefaults)
public var distanceType = DefaultSettingsFactory().getSettings().distanceType public var distanceType = DefaultSettingsFactory().getSettings().distanceType
public var locale:Locale { public var locale:Locale {
return Locale(identifier: Localize.currentLanguage()) return Locale(identifier: Localize.currentLanguage())
} }
@UserDefaultsBasicValue(key: "pinnedLayers") @UserDefaultsBasicValue(key: "pinnedLayers", userDefaults: UserDefaults.appDefaults)
public var pinnedLayerIds: [String] = DefaultSettingsFactory().getSettings().pinnedLayerIds public var pinnedLayerIds: [String] = DefaultSettingsFactory().getSettings().pinnedLayerIds
@UserDefaultsBasicValue(key: "selectedLayer") @UserDefaultsBasicValue(key: "selectedLayer", userDefaults: UserDefaults.appDefaults)
public var selectedLayerId: String = DefaultSettingsFactory().getSettings().selectedLayerId public var selectedLayerId: String = DefaultSettingsFactory().getSettings().selectedLayerId
@UserDefaultsOptionalValue("userQualifiedDate") @UserDefaultsOptionalValue("userQualifiedDate", userDefaults: UserDefaults.appDefaults)
public var userQualifiedDate: Date? public var userQualifiedDate: Date?
@UserDefaultsOptionalValue("firstOpenDate") @UserDefaultsOptionalValue("firstOpenDate", userDefaults: UserDefaults.appDefaults)
public var firstOpenDate: Date? public var firstOpenDate: Date?
@UserDefaultsOptionalValue("d3RetentionDate") @UserDefaultsOptionalValue("d3RetentionDate", userDefaults: UserDefaults.appDefaults)
public var d3RetentionDate: Date? public var d3RetentionDate: Date?
@UserDefaultsBasicValue(key: "locationDidAdded") @UserDefaultsBasicValue(key: "locationDidAdded", userDefaults: UserDefaults.appDefaults)
public var locationDidAdded: Bool = false public var locationDidAdded: Bool = false
@UserDefaultsOptionalValue("widgetPromotionTriggerCount") @UserDefaultsOptionalValue("widgetPromotionTriggerCount", userDefaults: UserDefaults.appDefaults)
public var widgetPromotionTriggerCount: Int? public var widgetPromotionTriggerCount: Int?
@UserDefaultsBasicValue(key: "initial_onboarding_showed") @UserDefaultsBasicValue(key: "initial_onboarding_showed", userDefaults: UserDefaults.appDefaults)
public var initialOnboardingShowed = false public var initialOnboardingShowed = false
@UserDefaultsBasicValue(key: "shorts_showed_swipeUp_count") @UserDefaultsBasicValue(key: "shorts_showed_swipeUp_count", userDefaults: UserDefaults.appDefaults)
public var shortsSwipeUpNudgeShowedCount = 0 public var shortsSwipeUpNudgeShowedCount = 0
#warning("Not implemented!") #warning("Not implemented!")
......
...@@ -33,7 +33,7 @@ struct HourlyTemperatureView: View { ...@@ -33,7 +33,7 @@ struct HourlyTemperatureView: View {
} }
) )
if hourlyWeather.count >= 2 { if hourlyWeather.count >= 2 && hourlyStackFrame.width > 0 && hourlyStackFrame.height > 55 {
TemperatureGraphView(hourlyWeather: hourlyWeather, TemperatureGraphView(hourlyWeather: hourlyWeather,
viewModel: .init(graphSize: .init(width: hourlyStackFrame.width, viewModel: .init(graphSize: .init(width: hourlyStackFrame.width,
height: hourlyStackFrame.height - 55), height: hourlyStackFrame.height - 55),
......
...@@ -121,7 +121,10 @@ struct TemperatureGraphView: View { ...@@ -121,7 +121,10 @@ struct TemperatureGraphView: View {
let diff = maxTemp - minTemp let diff = maxTemp - minTemp
for index in 0..<hourlyWeather.count { for index in 0..<hourlyWeather.count {
let multiply = CGFloat(maxTemp - hourlyWeather[index].temp) / CGFloat(diff) var multiply: CGFloat = 0.5
if diff > 0 {
multiply = CGFloat(maxTemp - hourlyWeather[index].temp) / CGFloat(diff)
}
let space:CGFloat = index > 0 && index <= 3 ? graphViewModel.spacingPerItem : 0 let space:CGFloat = index > 0 && index <= 3 ? graphViewModel.spacingPerItem : 0
if index == 0 { if index == 0 {
...@@ -136,6 +139,8 @@ struct TemperatureGraphView: View { ...@@ -136,6 +139,8 @@ struct TemperatureGraphView: View {
} }
private mutating func prepareSectionsAndLinePath() { private mutating func prepareSectionsAndLinePath() {
print("[DEBUG] graphInset: \(graphViewModel.graphInset) graphSize: \(graphViewModel.graphSize)")
let startPoint = CGPoint(x: graphViewModel.graphInset.leading, y: points.first?.y ?? 0) let startPoint = CGPoint(x: graphViewModel.graphInset.leading, y: points.first?.y ?? 0)
let endPoint = CGPoint(x: graphViewModel.graphSize.width, y: points.last?.y ?? 0) let endPoint = CGPoint(x: graphViewModel.graphSize.width, y: points.last?.y ?? 0)
......
...@@ -18,8 +18,17 @@ import OneWeatherUI ...@@ -18,8 +18,17 @@ import OneWeatherUI
class WeatherProvider: TimelineProvider { class WeatherProvider: TimelineProvider {
typealias Entry = WeatherEntry typealias Entry = WeatherEntry
var storage: Storage = CoreDataStorage() private let needsRadar: Bool
var weatherSource: WeatherSource = WdtWeatherSource() private var weatherSource: WeatherSource
private let locationSource: WidgetLocationSource
private let widgetKind: String
public init(widgetKind: String, locationSource: WidgetLocationSource = WidgetLocationSource.shared, weatherSource: WeatherSource = WdtWeatherSource(), needsRadar: Bool) {
self.widgetKind = widgetKind
self.locationSource = locationSource
self.weatherSource = weatherSource
self.needsRadar = needsRadar
}
func placeholder(in context: Context) -> WeatherEntry { func placeholder(in context: Context) -> WeatherEntry {
return WeatherEntry() return WeatherEntry()
...@@ -30,53 +39,33 @@ class WeatherProvider: TimelineProvider { ...@@ -30,53 +39,33 @@ class WeatherProvider: TimelineProvider {
completion(entry) completion(entry)
} }
func isFreshEnough(_ location: Location) -> Bool {
guard let lastTimeUpdated = location.lastWeatherUpdateDate else {
return false
}
return Date().timeIntervalSince(lastTimeUpdated) < self.weatherSource.weatherUpdateInterval
}
func getUpToDateLocation(_ completion: @escaping (Location?) -> () ) {
storage.load { [weak self] (locations, selectedIndex, error) in
guard let self = self else {
completion(nil)
return
}
guard let locations = locations, let selectedIndex = selectedIndex, selectedIndex < locations.count else {
completion(nil)
return
}
let selectedLocation = locations[selectedIndex]
guard !self.isFreshEnough(selectedLocation) else {
completion(selectedLocation)
return
}
self.weatherSource.updateWeather(for: selectedLocation, type: .preferIncremental) { updatedLocation, error in
guard let updatedLocation = updatedLocation else {
completion(selectedLocation)
return
}
completion(updatedLocation)
}
}
}
func getTimeline(in context: Context, completion: @escaping (Timeline<WeatherEntry>) -> Void) { func getTimeline(in context: Context, completion: @escaping (Timeline<WeatherEntry>) -> Void) {
getUpToDateLocation { location in locationSource.getUpToDateLocation { [weak self] location in
guard let self = self else { return }
var needToLogUpdate = false
if if
let fetchedLocation = location, let fetchedLocation = location,
let coordinates = fetchedLocation.coordinates let coordinates = fetchedLocation.coordinates
{ {
SnapshotLoader.load(at: coordinates, if self.needsRadar {
size: .init(width: 340, height: 280) SnapshotLoader.load(for: fetchedLocation,
) { mapImage in at: coordinates,
size: .init(width: 340, height: 280)
) { mapImage in
let nextRefresh = Calendar.current.date(byAdding: .minute, value: 30, to: Date())!
let entry = WeatherEntry(location: fetchedLocation, date: nextRefresh, radarMapImage: mapImage)
let timeline = Timeline(entries: [entry], policy: .atEnd)
if !context.isPreview {
WidgetManager.shared.logUpdate(forLocation: location, kind: self.widgetKind, family: context.family)
}
completion(timeline)
}
}
else {
let nextRefresh = Calendar.current.date(byAdding: .minute, value: 30, to: Date())! let nextRefresh = Calendar.current.date(byAdding: .minute, value: 30, to: Date())!
let entry = WeatherEntry(location: fetchedLocation, date: nextRefresh, radarMapImage: mapImage) let entry = WeatherEntry(location: fetchedLocation, date: nextRefresh, radarMapImage: nil)
let timeline = Timeline(entries: [entry], policy: .atEnd) let timeline = Timeline(entries: [entry], policy: .atEnd)
WidgetManager.shared.logUpdate() needToLogUpdate = true
completion(timeline) completion(timeline)
} }
} }
...@@ -84,9 +73,16 @@ class WeatherProvider: TimelineProvider { ...@@ -84,9 +73,16 @@ class WeatherProvider: TimelineProvider {
let nextRefresh = Calendar.current.date(byAdding: .minute, value: 30, to: Date())! let nextRefresh = Calendar.current.date(byAdding: .minute, value: 30, to: Date())!
let entry = WeatherEntry(location: location, date: nextRefresh) let entry = WeatherEntry(location: location, date: nextRefresh)
let timeline = Timeline(entries: [entry], policy: .atEnd) let timeline = Timeline(entries: [entry], policy: .atEnd)
WidgetManager.shared.logUpdate() needToLogUpdate = true
completion(timeline) completion(timeline)
} }
if !context.isPreview {
WidgetManager.shared.refreshAnalytics()
if needToLogUpdate {
WidgetManager.shared.logUpdate(forLocation: location, kind: self.widgetKind, family: context.family)
}
}
} }
} }
} }
//
// WidgetLocationSource.swift
// OneWeatherWidgetExtension
//
// Created by Demid Merzlyakov on 28.08.2021.
//
import Foundation
import OneWeatherCore
import WDTWeatherSource
import CoreDataStorage
private class WeatherRequester {
public typealias Completion = (Result<Location, Error>) -> ()
private let requestedLocation: Location
private let weatherSource: WeatherSource
private var result: Result<Location, Error>?
private let syncQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
private var awaitingCompletions = [Completion]()
public init(for location: Location, with weatherSource: WeatherSource) {
self.requestedLocation = location
self.weatherSource = weatherSource
request()
}
public func await(completion: @escaping Completion) {
syncQueue.addOperation { [weak self] in
guard let self = self else { return }
if let result = self.result {
completion(result)
return
}
let extendedCompletion: Completion = { result in
completion(result)
}
self.awaitingCompletions.append(extendedCompletion)
}
}
private func request() {
weatherSource.updateWeather(for: requestedLocation, type: .preferIncremental) { [weak self] location, error in
guard let self = self else { return }
let result: Result<Location, Error>
if let location = location {
result = .success(location)
}
else if let error = error {
result = .failure(error)
}
else {
result = .failure(NSError())
}
self.syncQueue.addOperation { [weak self] in
guard let self = self else { return }
for completion in self.awaitingCompletions {
completion(result)
}
}
}
}
}
class WidgetLocationSource {
public static let shared = WidgetLocationSource()
private let weatherSource: WeatherSource = WdtWeatherSource()
private let storage: Storage = CoreDataStorage()
private var locationsCache = Set<Location>()
private var activeRequests = [Location: WeatherRequester]()
private let syncQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
private func pickMoreRecent(storageLocation: Location, cacheLocation: Location) -> Location {
var moreRecentLocation = storageLocation
if let cacheUpdateTime = cacheLocation.lastWeatherUpdateDate {
if let storageUpdateTime = storageLocation.lastWeatherUpdateDate {
if cacheUpdateTime > storageUpdateTime {
moreRecentLocation = cacheLocation
}
}
else {
moreRecentLocation = cacheLocation
}
self.locationsCache.remove(cacheLocation)
self.locationsCache.insert(moreRecentLocation)
}
return moreRecentLocation
}
public func getUpToDateLocation(_ completion: @escaping (Location?) -> () ) {
storage.load { [weak self] (locations, selectedIndex, error) in
guard let self = self else {
completion(nil)
return
}
guard let locations = locations, let selectedIndex = selectedIndex, selectedIndex < locations.count else {
completion(nil)
return
}
self.syncQueue.addOperation { [weak self] in
guard let self = self else { return }
var location: Location = locations[selectedIndex]
if let cacheLocation = self.locationsCache.first(where: { $0 == location}) {
location = self.pickMoreRecent(storageLocation: locations[selectedIndex], cacheLocation: cacheLocation)
}
else {
location = locations[selectedIndex]
}
guard !self.isFreshEnough(location) else {
completion(location)
return
}
let requester = self.activeRequests[location] ?? WeatherRequester(for: location, with: self.weatherSource)
self.activeRequests[location] = requester
requester.await { [weak self] result in
self?.syncQueue.addOperation {
switch result {
case .success(let updatedLocation):
completion(updatedLocation)
self?.locationsCache.remove(location)
self?.locationsCache.insert(updatedLocation)
case .failure(_):
completion(location)
}
self?.activeRequests[location] = nil
}
}
}
}
}
private func isFreshEnough(_ location: Location) -> Bool {
guard let lastTimeUpdated = location.lastWeatherUpdateDate else {
return false
}
return Date().timeIntervalSince(lastTimeUpdated) < self.weatherSource.weatherUpdateInterval
}
}
...@@ -12,7 +12,7 @@ struct OneWeatherWidgets: WidgetBundle { ...@@ -12,7 +12,7 @@ struct OneWeatherWidgets: WidgetBundle {
var body: some Widget { var body: some Widget {
TemperatureWidget() TemperatureWidget()
PrecipitationWidget() PrecipitationWidget()
// WindWidget() WindWidget()
// RadarWidget() RadarWidget()
} }
} }
...@@ -9,18 +9,19 @@ import SwiftUI ...@@ -9,18 +9,19 @@ import SwiftUI
import MapKit import MapKit
import Swarm import Swarm
import OneWeatherCore
private let WDT_APP_ID = "e3b73414" private let WDT_APP_ID = "e3b73414"
private let WDT_APP_KEY = "25e8d6b72de3bcd528f7769b073cc335" private let WDT_APP_KEY = "25e8d6b72de3bcd528f7769b073cc335"
@available(iOS 14, *) @available(iOS 14, *)
class SnapshotLoader: ObservableObject { class SnapshotLoader: ObservableObject {
private static let swarmOverlay = SwarmManager.sharedManager.overlayForGroup(.none, baseLayer: .radar) static func load(for location: Location, at coordinates: CLLocationCoordinate2D, size: CGSize, completion:@escaping (_ snapshot:UIImage?) -> Void) {
static func load(at coordinates: CLLocationCoordinate2D, size: CGSize, completion:@escaping (_ snapshot:UIImage?) -> Void) {
SwarmManager.sharedManager.authentication = SkywiseAuthentication ( SwarmManager.sharedManager.authentication = SkywiseAuthentication (
app_id: WDT_APP_ID, app_id: WDT_APP_ID,
app_key: WDT_APP_KEY app_key: WDT_APP_KEY
) )
rebuildLayers(for: location)
let region = MKCoordinateRegion(center: coordinates, let region = MKCoordinateRegion(center: coordinates,
latitudinalMeters: 30000, latitudinalMeters: 30000,
...@@ -43,6 +44,62 @@ class SnapshotLoader: ObservableObject { ...@@ -43,6 +44,62 @@ class SnapshotLoader: ObservableObject {
} }
} }
private static var weatherLayers = [RadarLayer]()
private static var severeLayers = [RadarLayer]()
//Private
private static func rebuildLayers(for location: Location) {
let pinnedLayerIds = Settings.shared.pinnedLayerIds
//Fill weather layers
if location.supportsRadar == true {
weatherLayers = WeatherLayerType.allCases.map{ RadarLayer(pinned: pinnedLayerIds.contains($0.id), layer: $0) }
severeLayers = SevereLayerType.allCases.map{ RadarLayer(pinned: pinnedLayerIds.contains($0.id), layer: $0) }
}
else {
weatherLayers = [RadarLayer(pinned: pinnedLayerIds.contains(WeatherLayerType.clouds.id), layer: WeatherLayerType.clouds),
RadarLayer(pinned: pinnedLayerIds.contains(WeatherLayerType.uvIndex.id), layer: WeatherLayerType.uvIndex)]
severeLayers = [RadarLayer(pinned: pinnedLayerIds.contains(SevereLayerType.hurricaneTropicalTracks.id),
layer: SevereLayerType.hurricaneTropicalTracks)]
}
}
public static var selectedLayer: RadarLayer? {
let selectedLayerId = Settings.shared.selectedLayerId
if let weatherSelectedLayer = (weatherLayers.first{$0.layer.id == selectedLayerId}) {
return weatherSelectedLayer
}
if let severeSelectedLayer = (severeLayers.first{$0.layer.id == selectedLayerId}) {
return severeSelectedLayer
}
return weatherLayers.first
}
private static func getOverlay() -> SwarmOverlay? {
guard let selectedLayer = self.selectedLayer else {
return nil
}
var rawOverlay:SwarmOverlay?
if let weatherLayer = selectedLayer.layer as? WeatherLayerType {
rawOverlay = SwarmManager.sharedManager.overlayForBaseLayer(weatherLayer.swarmLayer)
// This is needed to use enhanced styles for international radar
if weatherLayer.swarmLayer == .satellite {
rawOverlay?.style = "enhanced"
}
}
if let severeLayer = selectedLayer.layer as? SevereLayerType {
rawOverlay = SwarmManager.sharedManager.overlayForGroup(severeLayer.swarmLayer, baseLayer: .radar)
}
return rawOverlay
}
private static func addTile(to snapshot: MKMapSnapshotter.Snapshot, private static func addTile(to snapshot: MKMapSnapshotter.Snapshot,
region: MKCoordinateRegion, region: MKCoordinateRegion,
scale: CGFloat, scale: CGFloat,
...@@ -58,8 +115,8 @@ class SnapshotLoader: ObservableObject { ...@@ -58,8 +115,8 @@ class SnapshotLoader: ObservableObject {
let b = MKMapPoint(bottomRight) let b = MKMapPoint(bottomRight)
let rect = MKMapRect(origin: MKMapPoint(x:min(a.x,b.x), y:min(a.y,b.y)), let rect = MKMapRect(origin: MKMapPoint(x:min(a.x,b.x), y:min(a.y,b.y)),
size: MKMapSize(width: abs(a.x-b.x), height: abs(a.y-b.y))) size: MKMapSize(width: abs(a.x-b.x), height: abs(a.y-b.y)))
let overlay: SwarmOverlay = getOverlay() ?? SwarmManager.sharedManager.overlayForGroup(.none, baseLayer: .radar)
let renderer = SwarmTileOverlayRenderer(overlay: SnapshotLoader.swarmOverlay) let renderer = SwarmTileOverlayRenderer(overlay: overlay)
let zoomLevel = Int(SnapshotLoader.zoomLevel(for: region).rounded(.down)) let zoomLevel = Int(SnapshotLoader.zoomLevel(for: region).rounded(.down))
renderer.getTilesImage(rect, zoomLevel: zoomLevel, scale: scale) { image in renderer.getTilesImage(rect, zoomLevel: zoomLevel, scale: scale) { image in
guard let tilesImage = image else { guard let tilesImage = image else {
......
...@@ -17,7 +17,7 @@ struct PrecipitationWidget: Widget { ...@@ -17,7 +17,7 @@ struct PrecipitationWidget: Widget {
// We currently display selectedLocation, so it's not really configurable, // We currently display selectedLocation, so it's not really configurable,
// but we'll probably need to switch to an IntentConfiguration at some point. // but we'll probably need to switch to an IntentConfiguration at some point.
StaticConfiguration(kind: kind, StaticConfiguration(kind: kind,
provider: WeatherProvider() provider: WeatherProvider(widgetKind: kind, needsRadar: false)
) { weatherEntry in ) { weatherEntry in
MediumPrecipitationWidgetView(widgetViewModel: ForecastWidgetViewModel(location: weatherEntry.location)) MediumPrecipitationWidgetView(widgetViewModel: ForecastWidgetViewModel(location: weatherEntry.location))
.widgetURL(URL(string: "ow-widget://precipitation-medium")) .widgetURL(URL(string: "ow-widget://precipitation-medium"))
......
...@@ -17,7 +17,7 @@ struct RadarWidget: Widget { ...@@ -17,7 +17,7 @@ struct RadarWidget: Widget {
// We currently display selectedLocation, so it's not really configurable, // We currently display selectedLocation, so it's not really configurable,
// but we'll probably need to switch to an IntentConfiguration at some point. // but we'll probably need to switch to an IntentConfiguration at some point.
StaticConfiguration(kind: kind, StaticConfiguration(kind: kind,
provider: WeatherProvider() provider: WeatherProvider(widgetKind: kind, needsRadar: true)
) { weatherEntry in ) { weatherEntry in
LargeRadarWidgetView(widgetViewModel: ForecastWidgetViewModel(location: weatherEntry.location, LargeRadarWidgetView(widgetViewModel: ForecastWidgetViewModel(location: weatherEntry.location,
radarImage: weatherEntry.radarMapImage)) radarImage: weatherEntry.radarMapImage))
......
...@@ -17,7 +17,7 @@ struct TemperatureWidget: Widget { ...@@ -17,7 +17,7 @@ struct TemperatureWidget: Widget {
// We currently display selectedLocation, so it's not really configurable, // We currently display selectedLocation, so it's not really configurable,
// but we'll probably need to switch to an IntentConfiguration at some point. // but we'll probably need to switch to an IntentConfiguration at some point.
StaticConfiguration(kind: kind, StaticConfiguration(kind: kind,
provider: WeatherProvider() provider: WeatherProvider(widgetKind: kind, needsRadar: false)
) { weatherEntry in ) { weatherEntry in
WidgetView(entry: weatherEntry) WidgetView(entry: weatherEntry)
} }
......
...@@ -17,7 +17,7 @@ struct WindWidget: Widget { ...@@ -17,7 +17,7 @@ struct WindWidget: Widget {
// We currently display selectedLocation, so it's not really configurable, // We currently display selectedLocation, so it's not really configurable,
// but we'll probably need to switch to an IntentConfiguration at some point. // but we'll probably need to switch to an IntentConfiguration at some point.
StaticConfiguration(kind: kind, StaticConfiguration(kind: kind,
provider: WeatherProvider() provider: WeatherProvider(widgetKind: kind, needsRadar: false)
) { weatherEntry in ) { weatherEntry in
WidgetView(entry: weatherEntry) WidgetView(entry: weatherEntry)
} }
......
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