Commit 589f5120 by Dmitriy Stepanets

Merge branch 'release/5.3' into develop

# Conflicts:
#	1Weather-Icons.sketch
#	1Weather.xcodeproj/project.pbxproj
#	1Weather.xcodeproj/xcuserdata/dstepanets.xcuserdatad/xcschemes/xcschememanagement.plist
#	1Weather.xcworkspace/contents.xcworkspacedata
#	1Weather.xcworkspace/xcuserdata/dstepanets.xcuserdatad/xcschemes/xcschememanagement.plist
#	1Weather/AppDelegate.swift
#	1Weather/Resources/en.lproj/Localizable.strings
#	1Weather/UI/Helpers/AppFont.swift
#	OneWeatherAnalytics/OneWeatherAnalytics/AnalyticsEvent.swift
#	OneWeatherAnalytics/OneWeatherAnalytics/AnalyticsParameter.swift
#	OneWeatherCore/OneWeatherCore.xcodeproj/project.pbxproj
#	OneWeatherCore/OneWeatherCore/Model/LocationManager.swift
#	PG.playground/Contents.swift
#	Podfile.lock
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_armv7/DTBiOSSDK.framework/Headers/DTBAdBannerDispatcher.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_armv7/DTBiOSSDK.framework/Headers/DTBAdInterstitialViewController.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_armv7/DTBiOSSDK.framework/Headers/DTBAdLoader.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_armv7/DTBiOSSDK.framework/Headers/DTBAdResponse+Mediation.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_armv7/DTBiOSSDK.framework/Headers/DTBAdResponse.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_armv7/DTBiOSSDK.framework/Headers/DTBAdView.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_armv7/DTBiOSSDK.framework/Headers/DTBDebugProperties.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_armv7/DTBiOSSDK.framework/Headers/DTBErrorCodes.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_armv7/DTBiOSSDK.framework/Headers/DTBExpectedSize.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_armv7/DTBiOSSDK.framework/Headers/DTBLog.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_armv7/DTBiOSSDK.framework/Headers/DTBMediationConstants.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_armv7/DTBiOSSDK.framework/Headers/UIView+DTB.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_armv7/DTBiOSSDK.framework/aps_mobile_client_config.json
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_i386_x86_64-simulator/DTBiOSSDK.framework/Headers/DTBAdBannerDispatcher.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_i386_x86_64-simulator/DTBiOSSDK.framework/Headers/DTBAdInterstitialViewController.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_i386_x86_64-simulator/DTBiOSSDK.framework/Headers/DTBAdLoader.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_i386_x86_64-simulator/DTBiOSSDK.framework/Headers/DTBAdResponse+Mediation.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_i386_x86_64-simulator/DTBiOSSDK.framework/Headers/DTBAdResponse.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_i386_x86_64-simulator/DTBiOSSDK.framework/Headers/DTBAdView.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_i386_x86_64-simulator/DTBiOSSDK.framework/Headers/DTBDebugProperties.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_i386_x86_64-simulator/DTBiOSSDK.framework/Headers/DTBErrorCodes.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_i386_x86_64-simulator/DTBiOSSDK.framework/Headers/DTBExpectedSize.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_i386_x86_64-simulator/DTBiOSSDK.framework/Headers/DTBLog.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_i386_x86_64-simulator/DTBiOSSDK.framework/Headers/DTBMediationConstants.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_i386_x86_64-simulator/DTBiOSSDK.framework/Headers/UIView+DTB.h
#	Pods/AmazonPublisherServicesSDK/APS_iOS_SDK-4.1.0/DTBiOSSDK.xcframework/ios-arm64_i386_x86_64-simulator/DTBiOSSDK.framework/aps_mobile_client_config.json
#	Pods/Manifest.lock
#	Pods/Pods.xcodeproj/project.pbxproj
#	Pods/Swarm/Swarm/SwarmTileOverlayRenderer.swift
#	Pods/Target Support Files/FBSDKCoreKit/FBSDKCoreKit-Info.plist
#	Pods/Target Support Files/Nuke-WebP-Plugin/Nuke-WebP-Plugin-Info.plist
#	Pods/Target Support Files/Pods-1Weather/Pods-1Weather.debug.xcconfig
#	Pods/Target Support Files/Pods-1Weather/Pods-1Weather.release.xcconfig
#	Pods/Target Support Files/Pods-OneWeatherWidgetExtension/Pods-OneWeatherWidgetExtension-Info.plist
parents 04fe4182 fa078b67
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>1Weather.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
<key>OneWeatherNotificationServiceExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>3</integer>
</dict>
<key>OneWeatherWidgetExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>6</integer>
</dict>
<key>PG (Playground) 1.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>2</integer>
</dict>
<key>PG (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>3</integer>
</dict>
<key>PG (Playground).xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>
...@@ -5,28 +5,28 @@ ...@@ -5,28 +5,28 @@
location = "group:OneWeatherUI/OneWeatherUI.xcodeproj"> location = "group:OneWeatherUI/OneWeatherUI.xcodeproj">
</FileRef> </FileRef>
<FileRef <FileRef
location = "group:OneWeatherAnalytics/OneWeatherAnalytics.xcodeproj"> location = "group:1Weather.xcodeproj">
</FileRef> </FileRef>
<FileRef <FileRef
location = "group:DelayedSaveStorage/DelayedSaveStorage.xcodeproj"> location = "group:BlendFIPS/BlendFIPSSource.xcodeproj">
</FileRef> </FileRef>
<FileRef <FileRef
location = "group:CoreDataStorage/CoreDataStorage.xcodeproj"> location = "group:BlendHealthSource/BlendHealthSource.xcodeproj">
</FileRef> </FileRef>
<FileRef <FileRef
location = "group:BlendFIPS/BlendFIPSSource.xcodeproj"> location = "group:CoreDataStorage/CoreDataStorage.xcodeproj">
</FileRef> </FileRef>
<FileRef <FileRef
location = "group:BlendHealthSource/BlendHealthSource.xcodeproj"> location = "group:DelayedSaveStorage/DelayedSaveStorage.xcodeproj">
</FileRef> </FileRef>
<FileRef <FileRef
location = "group:WDTWeatherSource/WDTWeatherSource.xcodeproj"> location = "group:OneWeatherAnalytics/OneWeatherAnalytics.xcodeproj">
</FileRef> </FileRef>
<FileRef <FileRef
location = "group:OneWeatherCore/OneWeatherCore.xcodeproj"> location = "group:OneWeatherCore/OneWeatherCore.xcodeproj">
</FileRef> </FileRef>
<FileRef <FileRef
location = "group:1Weather.xcodeproj"> location = "group:WDTWeatherSource/WDTWeatherSource.xcodeproj">
</FileRef> </FileRef>
<FileRef <FileRef
location = "group:Pods/Pods.xcodeproj"> location = "group:Pods/Pods.xcodeproj">
......
...@@ -14,9 +14,9 @@ ...@@ -14,9 +14,9 @@
filePath = "Pods/Swarm/Swarm/SwarmTileOverlayRenderer.swift" filePath = "Pods/Swarm/Swarm/SwarmTileOverlayRenderer.swift"
startingColumnNumber = "9223372036854775807" startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807" endingColumnNumber = "9223372036854775807"
startingLineNumber = "107" startingLineNumber = "108"
endingLineNumber = "107" endingLineNumber = "108"
landmarkName = "getTilesImage(_:zoomLevel:scale:completion:)" landmarkName = "getTilesImage(_:zoomLevel:completion:)"
landmarkType = "7"> landmarkType = "7">
</BreakpointContent> </BreakpointContent>
</BreakpointProxy> </BreakpointProxy>
......
...@@ -16,6 +16,7 @@ import DTBiOSSDK ...@@ -16,6 +16,7 @@ import DTBiOSSDK
import OneWeatherCore import OneWeatherCore
import AppsFlyerLib import AppsFlyerLib
import OneWeatherAnalytics import OneWeatherAnalytics
import NukeWebPPlugin
import WDTWeatherSource import WDTWeatherSource
import BlendHealthSource import BlendHealthSource
...@@ -47,6 +48,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ...@@ -47,6 +48,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
log.debug("Launch options: -") log.debug("Launch options: -")
} }
//Nuke WebP plugin
WebPImageDecoder.enable()
log.debug("Work directory: \(NSHomeDirectory())") log.debug("Work directory: \(NSHomeDirectory())")
//LocationManager //LocationManager
......
...@@ -11,6 +11,17 @@ public protocol ReusableCellProtocol: AnyObject { ...@@ -11,6 +11,17 @@ public protocol ReusableCellProtocol: AnyObject {
static var kIdentifier:String { get } static var kIdentifier:String { get }
} }
extension UICollectionViewCell: ReusableCellProtocol {
public static var kIdentifier: String {
let identifier = String(describing: self)
if identifier == "UICollectionViewCell" {
assertionFailure("Invalid reuse identifier name. Please provide a valid name.")
}
return String(describing: self)
}
}
extension UITableViewCell: ReusableCellProtocol { extension UITableViewCell: ReusableCellProtocol {
public static var kIdentifier: String { public static var kIdentifier: String {
let identifier = String(describing: self) let identifier = String(describing: self)
......
//
// ThreadSafeDictionary.swift
// 1Weather
//
// Created by Dmitry Stepanets on 10.07.2021.
//
import Foundation
class ThreadSafeDictionary<V: Hashable,T>: Collection {
private var dictionary: [V: T]
private let concurrentQueue = DispatchQueue(label: "com.oneweather.dictionary",
attributes: .concurrent)
var startIndex: Dictionary<V, T>.Index {
self.concurrentQueue.sync {
return self.dictionary.startIndex
}
}
var endIndex: Dictionary<V, T>.Index {
self.concurrentQueue.sync {
return self.dictionary.endIndex
}
}
init(dict: [V: T] = [V:T]()) {
self.dictionary = dict
}
// this is because it is an apple protocol method
// swiftlint:disable identifier_name
func index(after i: Dictionary<V, T>.Index) -> Dictionary<V, T>.Index {
self.concurrentQueue.sync {
return self.dictionary.index(after: i)
}
}
// swiftlint:enable identifier_name
subscript(key: V) -> T? {
set(newValue) {
self.concurrentQueue.async(flags: .barrier) {[weak self] in
self?.dictionary[key] = newValue
}
}
get {
self.concurrentQueue.sync {
return self.dictionary[key]
}
}
}
// has implicity get
subscript(index: Dictionary<V, T>.Index) -> Dictionary<V, T>.Element {
self.concurrentQueue.sync {
return self.dictionary[index]
}
}
func removeValue(forKey key: V) {
self.concurrentQueue.async(flags: .barrier) {[weak self] in
self?.dictionary.removeValue(forKey: key)
}
}
func removeAll() {
self.concurrentQueue.async(flags: .barrier) {[weak self] in
self?.dictionary.removeAll()
}
}
}
...@@ -24,6 +24,8 @@ class AppCoordinator: Coordinator { ...@@ -24,6 +24,8 @@ class AppCoordinator: Coordinator {
//TODO: this is very bad, fix it //TODO: this is very bad, fix it
AppCoordinator.instance = self AppCoordinator.instance = self
LocationManager.shared.add(delegate: self)
} }
func start() { func start() {
...@@ -35,6 +37,12 @@ class AppCoordinator: Coordinator { ...@@ -35,6 +37,12 @@ class AppCoordinator: Coordinator {
forecastCoordinator.start() forecastCoordinator.start()
childCoordinators.append(forecastCoordinator) childCoordinators.append(forecastCoordinator)
if ShortsManager.shared.shortsAvailable {
let shortsCoordinator = ShortsCoordinator(tabBarController: tabBarController)
shortsCoordinator.start()
childCoordinators.append(shortsCoordinator)
}
let radarCoordinator = RadarCoordinator(tabBarController: tabBarController) let radarCoordinator = RadarCoordinator(tabBarController: tabBarController)
radarCoordinator.start() radarCoordinator.start()
childCoordinators.append(radarCoordinator) childCoordinators.append(radarCoordinator)
...@@ -44,6 +52,7 @@ class AppCoordinator: Coordinator { ...@@ -44,6 +52,7 @@ class AppCoordinator: Coordinator {
childCoordinators.append(menuCoordinator) childCoordinators.append(menuCoordinator)
tabBarController.setupTabBar() tabBarController.setupTabBar()
tabBarController.updateTabs()
let animationController = SplashAnimationViewController(appCoordinator: self) let animationController = SplashAnimationViewController(appCoordinator: self)
window.rootViewController = animationController window.rootViewController = animationController
...@@ -69,6 +78,18 @@ class AppCoordinator: Coordinator { ...@@ -69,6 +78,18 @@ class AppCoordinator: Coordinator {
tabBarController.selectedIndex = AppTabBarController.AppTab.radar.rawValue tabBarController.selectedIndex = AppTabBarController.AppTab.radar.rawValue
} }
public func openShorts(atIndex index:Int) {
guard
ShortsManager.shared.shortsAvailable,
let shortsCoordinator = (childCoordinators.first{ $0 is ShortsCoordinator } as? ShortsCoordinator)
else {
return
}
shortsCoordinator.setIndexToScroll(shortIndex: index)
tabBarController.selectedIndex = AppTabBarController.AppTab.shorts.rawValue
}
public func openNotifications() { public func openNotifications() {
let notificationsCoordinator = NotificationsCoordinator(parentViewController: tabBarController) let notificationsCoordinator = NotificationsCoordinator(parentViewController: tabBarController)
notificationsCoordinator.parentCoordinator = self notificationsCoordinator.parentCoordinator = self
...@@ -85,7 +106,43 @@ class AppCoordinator: Coordinator { ...@@ -85,7 +106,43 @@ class AppCoordinator: Coordinator {
} }
} }
private func updateTabBarVisibleItems() {
//Hide shorts tab if not available for selected location
guard let shortsIsPresent = (tabBarController.viewControllers?.contains{ $0 is ShortsViewController }) else {
return
}
if ShortsManager.shared.shortsAvailable {
if !shortsIsPresent {
let shortsCoordinator = ShortsCoordinator(tabBarController: tabBarController)
shortsCoordinator.start(withIndex: 2)
childCoordinators.append(shortsCoordinator)
}
}
else {
if shortsIsPresent {
guard let shortsCoordinator = (childCoordinators.first{ $0 is ShortsCoordinator }) else {
return
}
self.childDidFinish(child: shortsCoordinator)
self.tabBarController.removeViewController(atIndex: 2)
}
}
tabBarController.updateTabs()
}
func viewControllerDidEnd(controller: UIViewController) { func viewControllerDidEnd(controller: UIViewController) {
// //do nothing
}
}
//MARK:- LocationManager Delegate
extension AppCoordinator: LocationManagerDelegate {
func locationManager(_ locationManager: LocationManager, updatedLocationsList newList: [Location]) {
//do nothing
}
func locationManager(_ locationManager: LocationManager, changedSelectedLocation newLocation: Location?) {
updateTabBarVisibleItems()
} }
} }
...@@ -10,7 +10,7 @@ import UIKit ...@@ -10,7 +10,7 @@ import UIKit
class RadarCoordinator: Coordinator { class RadarCoordinator: Coordinator {
//Private //Private
private let navigationController = UINavigationController(nibName: nil, bundle: nil) private let navigationController = UINavigationController(nibName: nil, bundle: nil)
private var tabBarController:UITabBarController? private let tabBarController:UITabBarController
//Public //Public
var childCoordinators = [Coordinator]() var childCoordinators = [Coordinator]()
...@@ -23,7 +23,7 @@ class RadarCoordinator: Coordinator { ...@@ -23,7 +23,7 @@ class RadarCoordinator: Coordinator {
func start() { func start() {
let radarViewController = RadarViewController(coordinator: self) let radarViewController = RadarViewController(coordinator: self)
navigationController.viewControllers = [radarViewController] navigationController.viewControllers = [radarViewController]
tabBarController?.add(viewController: navigationController) tabBarController.add(viewController: navigationController)
} }
public func openNotificationsScreen() { public func openNotificationsScreen() {
......
//
// ShortsCoordinator.swift
// 1Weather
//
// Created by Dmitry Stepanets on 10.06.2021.
//
import UIKit
class ShortsCoordinator: Coordinator {
//Private
private var tabBarController: UITabBarController
private lazy var shortsViewController: ShortsViewController = {
return ShortsViewController(coordinator: self)
}()
//Public
var childCoordinators = [Coordinator]()
var parentCoordinator: Coordinator?
init(tabBarController:UITabBarController) {
self.tabBarController = tabBarController
}
func start() {
tabBarController.add(viewController: shortsViewController)
}
func start(withIndex index:Int) {
tabBarController.add(viewController: shortsViewController, atIndex: index)
}
func setIndexToScroll(shortIndex index: Int) {
shortsViewController.set(indexToScroll: index)
}
func viewControllerDidEnd(controller: UIViewController) {
parentCoordinator?.childDidFinish(child: self)
}
}
//
// UIColor+Highlight.swift
// 1Weather
//
// Created by Dmitry Stepanets on 28.11.2020.
//
import UIKit
public extension UIColor {
var isDarkColor: Bool {
var r, g, b, a: CGFloat
(r, g, b, a) = (0, 0, 0, 0)
self.getRed(&r, green: &g, blue: &b, alpha: &a)
let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b
return lum < 0.60 ? true : false
}
var highlighted:UIColor {
return self.isDarkColor ? self.adjust(by: 30) : self.adjust(by: -30)
}
func lighter(by percentage: CGFloat = 30.0) -> UIColor? {
return self.adjust(by: abs(percentage) )
}
func darker(by percentage: CGFloat = 30.0) -> UIColor? {
return self.adjust(by: -1 * abs(percentage) )
}
func adjust(by percentage: CGFloat = 30.0) -> UIColor {
var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0
if self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) {
return UIColor(red: min(red + percentage/100, 1.0),
green: min(green + percentage/100, 1.0),
blue: min(blue + percentage/100, 1.0),
alpha: alpha)
} else {
return self
}
}
}
//
// UIImage+AverageColor.swift
// 1Weather
//
// Created by Dmitry Stepanets on 09.06.2021.
//
import UIKit
extension UIImage {
var averageColor: UIColor? {
guard let inputImage = CIImage(image: self) else { return nil }
let extentVector = CIVector(x: inputImage.extent.origin.x, y: inputImage.extent.origin.y, z: inputImage.extent.size.width, w: inputImage.extent.size.height)
guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: extentVector]) else { return nil }
guard let outputImage = filter.outputImage else { return nil }
var bitmap = [UInt8](repeating: 0, count: 4)
let context = CIContext(options: [.workingColorSpace: kCFNull!])
context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil)
return UIColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255)
}
}
...@@ -8,10 +8,22 @@ ...@@ -8,10 +8,22 @@
import UIKit import UIKit
public extension UITabBarController { public extension UITabBarController {
func add(viewController:UIViewController) { func add(viewController: UIViewController) {
var existingControllers = [UIViewController]() var existingControllers = [UIViewController]()
existingControllers.append(contentsOf: self.viewControllers ?? [UIViewController]()) existingControllers.append(contentsOf: self.viewControllers ?? [UIViewController]())
existingControllers.append(viewController) existingControllers.append(viewController)
self.viewControllers = existingControllers self.viewControllers = existingControllers
} }
func add(viewController: UIViewController, atIndex index: Int) {
var existingControllers = [UIViewController]()
existingControllers.append(contentsOf: self.viewControllers ?? [UIViewController]())
existingControllers.insert(viewController, at: index)
self.viewControllers = existingControllers
}
func removeViewController(atIndex index:Int) {
guard index < viewControllers?.count ?? 0 else { return }
viewControllers?.remove(at: index)
}
} }
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
// //
import UIKit import UIKit
import OneWeatherCore
public enum AppInterfaceStyle { public enum AppInterfaceStyle {
case light case light
......
...@@ -406,9 +406,6 @@ ...@@ -406,9 +406,6 @@
</array> </array>
<key>UIAppFonts</key> <key>UIAppFonts</key>
<array> <array>
<string>SF-Pro-Display-Light.otf</string>
<string>SF-Pro-Display-Regular.otf</string>
<string>SF-Pro-Display-Thin.otf</string>
<string>SF-Pro.ttf</string> <string>SF-Pro.ttf</string>
<string>SF-Compact-Display-Light.otf</string> <string>SF-Compact-Display-Light.otf</string>
<string>SF-Compact-Display-Regular.otf</string> <string>SF-Compact-Display-Regular.otf</string>
......
...@@ -18,13 +18,19 @@ public struct AppConfig: Codable { ...@@ -18,13 +18,19 @@ public struct AppConfig: Codable {
public let ccpaUpdateInterval: TimeInterval? public let ccpaUpdateInterval: TimeInterval?
public let nwsAlertsViaMoEngageEnabled: Bool public let nwsAlertsViaMoEngageEnabled: Bool
public let showAttPrompt: Bool public let showAttPrompt: Bool
public let shortsLeftBelowCount: Int
public let shortsLastNudgeEnabled: Bool
public let shortsSwipeUpNudgeCount: Int
public init(popularCities: [GeoNamesPlace]?, adConfig: AdConfig, ccpaUpdateInterval: TimeInterval?, nwsAlertsViaMoEngageEnabled: Bool, showAttPrompt: Bool) { public init(popularCities: [GeoNamesPlace]?, adConfig: AdConfig, ccpaUpdateInterval: TimeInterval?, nwsAlertsViaMoEngageEnabled: Bool, showAttPrompt: Bool, shortsLeftBelowCountKey: Int, shortsLastNudgeEnabledKey: Bool, shortsSwipeUpNudgeCountKey: Int) {
self.popularCities = popularCities self.popularCities = popularCities
self.adConfig = adConfig self.adConfig = adConfig
self.ccpaUpdateInterval = ccpaUpdateInterval self.ccpaUpdateInterval = ccpaUpdateInterval
self.nwsAlertsViaMoEngageEnabled = nwsAlertsViaMoEngageEnabled self.nwsAlertsViaMoEngageEnabled = nwsAlertsViaMoEngageEnabled
self.showAttPrompt = showAttPrompt self.showAttPrompt = showAttPrompt
self.shortsLeftBelowCount = shortsLeftBelowCountKey
self.shortsLastNudgeEnabled = shortsLastNudgeEnabledKey
self.shortsSwipeUpNudgeCount = shortsSwipeUpNudgeCountKey
} }
} }
...@@ -38,6 +44,9 @@ public class ConfigManager { ...@@ -38,6 +44,9 @@ public class ConfigManager {
private static let ccpaUpdateIntervalConfigKey = "ccpa_update_interval_ios" private static let ccpaUpdateIntervalConfigKey = "ccpa_update_interval_ios"
private static let nwsAlertsViaMoEngageEnabledKey = "ios_nws_alerts_via_moengage_enabled" private static let nwsAlertsViaMoEngageEnabledKey = "ios_nws_alerts_via_moengage_enabled"
private static let showAttPromptKey = "ios_show_att_prompt" private static let showAttPromptKey = "ios_show_att_prompt"
private static let shortsLeftBelowCountKey = "shorts_left_below_nudge_every_x_cards"
private static let shortsLastNudgeEnabledKey = "shorts_swipe_down_nudge_enabled"
private static let shortsSwipeUpNudgeCountKey = "shorts_swipe_up_nudge_on_x_cards"
private let delegates = MulticastDelegate<ConfigManagerDelegate>() private let delegates = MulticastDelegate<ConfigManagerDelegate>()
...@@ -53,7 +62,14 @@ public class ConfigManager { ...@@ -53,7 +62,14 @@ public class ConfigManager {
public static let shared = ConfigManager() public static let shared = ConfigManager()
public var config: AppConfig = AppConfig(popularCities: nil, adConfig: AdConfig(), ccpaUpdateInterval: nil, nwsAlertsViaMoEngageEnabled: true, showAttPrompt: false) public var config: AppConfig = AppConfig(popularCities: nil,
adConfig: AdConfig(),
ccpaUpdateInterval: nil,
nwsAlertsViaMoEngageEnabled: true,
showAttPrompt: false,
shortsLeftBelowCountKey: 0,
shortsLastNudgeEnabledKey: false,
shortsSwipeUpNudgeCountKey: 0)
public func updateConfig() { public func updateConfig() {
log.info("update config") log.info("update config")
...@@ -127,9 +143,24 @@ public class ConfigManager { ...@@ -127,9 +143,24 @@ public class ConfigManager {
let showAttPromptValue = remoteConfig.configValue(forKey: ConfigManager.showAttPromptKey) let showAttPromptValue = remoteConfig.configValue(forKey: ConfigManager.showAttPromptKey)
let showAttPrompt = showAttPromptValue.boolValue let showAttPrompt = showAttPromptValue.boolValue
let shortsLeftBelowCountValue = remoteConfig.configValue(forKey: ConfigManager.shortsLeftBelowCountKey)
let shortsLeftBelowCount = shortsLeftBelowCountValue.numberValue.intValue
let shortsLastNudgeEnabledValue = remoteConfig.configValue(forKey: ConfigManager.shortsLastNudgeEnabledKey)
let shortsLastNudgeEnabled = shortsLastNudgeEnabledValue.boolValue
let shortsSwipeNudgeCountValue = remoteConfig.configValue(forKey: ConfigManager.shortsSwipeUpNudgeCountKey)
let shortsSwipeNudgeCount = shortsSwipeNudgeCountValue.numberValue.intValue
DispatchQueue.main.async { DispatchQueue.main.async {
self.config = AppConfig(popularCities: popularCities, adConfig: adConfig, ccpaUpdateInterval: ccpaUpdateInterval, nwsAlertsViaMoEngageEnabled: nwsAlertsViaMoEngageEnabled, showAttPrompt: showAttPrompt) self.config = AppConfig(popularCities: popularCities,
adConfig: adConfig,
ccpaUpdateInterval:ccpaUpdateInterval,
nwsAlertsViaMoEngageEnabled: nwsAlertsViaMoEngageEnabled,
showAttPrompt: showAttPrompt,
shortsLeftBelowCountKey: shortsLeftBelowCount,
shortsLastNudgeEnabledKey: shortsLastNudgeEnabled,
shortsSwipeUpNudgeCountKey: shortsSwipeNudgeCount)
self.notifyAboutConfigUpdate() self.notifyAboutConfigUpdate()
DispatchQueue.global().async { DispatchQueue.global().async {
let encoder = JSONEncoder() let encoder = JSONEncoder()
......
...@@ -236,7 +236,7 @@ extension PushNotificationsManager: UNUserNotificationCenterDelegate { ...@@ -236,7 +236,7 @@ extension PushNotificationsManager: UNUserNotificationCenterDelegate {
if UIApplication.shared.applicationState == .active { if UIApplication.shared.applicationState == .active {
analytics(log: .ANALYTICS_PUSH_RECEIVED) analytics(log: .ANALYTICS_PUSH_RECEIVED)
} else { } else {
analytics(log: .ANALYTICS_PUSH_SELECTED, params: [.ANALYTICS_KEY_PUSH_NOTIFICATION_SOURCE: "background"]) analytics(log: .ANALYTICS_PUSH_SELECTED, params: [.ANALYTICS_KEY_SOURCE: "background"])
} }
log.debug("Got a push notification: \(notification.request.content.userInfo)"); log.debug("Got a push notification: \(notification.request.content.userInfo)");
......
//
// ShortsManager.swift
// 1Weather
//
// Created by Dmitry Stepanets on 08.06.2021.
//
import UIKit
import OneWeatherCore
import InMobiShortsSource
import OneWeatherAnalytics
import Nuke
protocol ShortsManagerDelegate: AnyObject {
func shortsDidChange()
}
class ShortsManager {
//Public
static let shared = ShortsManager()
let multicastDelegate = MulticastDelegate<ShortsManagerDelegate>()
private(set) var shorts = [ShortsItem]()
var shortsAvailable: Bool {
return LocationManager.shared.selectedLocation?.countryCode == "US" && UIDevice.current.userInterfaceIdiom == .phone
}
//Private
private let avgColorQueue = OperationQueue()
private let colorCache = NSCache<NSString, UIColor>()
private let source = InMobiShortSource()
private let log = Logger(componentName: "ShortsManager")
private var isUpdating = false
private init() {}
//MARK: Private
private func sync(with newShorts: inout [ShortsItem]) {
for (index, newShort) in newShorts.enumerated() {
if let presentShortIndex = (shorts.firstIndex{$0.id == newShort.id}) {
let presentShort = shorts[presentShortIndex]
newShorts[index].isLiked = presentShort.isLiked
newShorts[index].viewDate = presentShort.viewDate
}
}
}
//MARK: Public
public func refreshShorts() {
if isUpdating {
return
}
isUpdating = true
source.updateShorts {[weak self] shortItems, error in
defer {
self?.isUpdating = false
}
guard error == nil else {
self?.log.debug("Failed to update shorts \(String(describing: error?.localizedDescription))")
return
}
guard var items = shortItems else {
self?.log.debug("Fetched shorts are empty")
return
}
self?.sync(with: &items)
self?.shorts.removeAll()
self?.shorts.append(contentsOf: items)
self?.reordering()
self?.multicastDelegate.invoke(invocation: { delegate in
delegate.shortsDidChange()
})
}
}
public func reordering() {
let unviewedShorts = shorts.filter{$0.viewDate == nil}
let viewedShorts = shorts.filter {
$0.viewDate != nil
}.sorted {
guard let viewDate0 = $0.viewDate, let viewDate1 = $1.viewDate else {
return false
}
return viewDate0 < viewDate1
}
shorts = unviewedShorts + viewedShorts
self.multicastDelegate.invoke { delegate in
delegate.shortsDidChange()
}
}
func like(item: ShortsItem) {
guard let shortsToActionIndex = (shorts.firstIndex { $0.id == item.id }) else {
return
}
if self.shorts[shortsToActionIndex].isLiked {
self.shorts[shortsToActionIndex].dislike()
}
else {
self.shorts[shortsToActionIndex].like()
}
}
func markAsViewed(index: Int) {
guard index >= 0, index < shorts.count else {
return
}
self.shorts[index].markAsViewed()
}
func backgroundColor(for short:ShortsItem, shortImage: UIImage, completion:@escaping(_ color: UIColor?,_ shortId: String) -> Void) {
if let cachedColor = colorCache.object(forKey: short.id as NSString) {
completion(cachedColor, short.id)
return
}
avgColorQueue.addOperation {
guard let color = shortImage.averageColor?.darker(by: 20) else {
completion(nil, short.id)
return
}
self.colorCache.setObject(color, forKey: short.id as NSString)
completion(color, short.id)
}
}
}
{
"images" : [
{
"filename" : "like_filled.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
{
"images" : [
{
"filename" : "like_ouline.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
{
"images" : [
{
"filename" : "shorts_swipe_down.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
{
"images" : [
{
"filename" : "tab_shorts.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
{
"images" : [
{
"filename" : "tab_shorts_selected.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
...@@ -155,6 +155,12 @@ ...@@ -155,6 +155,12 @@
"location.status.whenInUser" = "when in use"; "location.status.whenInUser" = "when in use";
"location.status.unknown" = "unknown"; "location.status.unknown" = "unknown";
//Shorts
"shorts.source" = "Source";
"shorts.unread" = "%d unread Shorts left below";
"shorts.viewedAll" = "You've viewed all Shorts\nSwipe down to revisit";
"shorts.swipeUp" = "Swipe up for more Shorts";
//Radar //Radar
"radar.layers.base" = "Base layer"; "radar.layers.base" = "Base layer";
"radar.layers.severe" = "Severe weather layer"; "radar.layers.severe" = "Severe weather layer";
......
...@@ -34,6 +34,11 @@ public struct AppFont { ...@@ -34,6 +34,11 @@ public struct AppFont {
static func regular(size: CGFloat) -> UIFont { static func regular(size: CGFloat) -> UIFont {
font(weigth: .regular, size: size) font(weigth: .regular, size: size)
} }
static func light(size: CGFloat) -> UIFont {
font(weigth: .light, size: size)
}
static func bold(size: CGFloat) -> UIFont { static func bold(size: CGFloat) -> UIFont {
font(weigth: .bold, size: size) font(weigth: .bold, size: size)
} }
...@@ -44,34 +49,6 @@ public struct AppFont { ...@@ -44,34 +49,6 @@ public struct AppFont {
font(weigth: .semibold, size: size) font(weigth: .semibold, size: size)
} }
} }
public struct SFProDisplay {
private static var fontCache = [UIFont.Weight: [CGFloat: UIFont]]()
static func font(weigth: UIFont.Weight, size: CGFloat) -> UIFont {
if let cached = fontCache[weigth]?[size] {
return cached
}
let descriptor = AppFont.fontDescriptor(family: "SF Pro Display", size: size, weight: weigth)
let font = UIFont(descriptor: descriptor, size: size)
if fontCache[weigth] == nil {
fontCache[weigth] = [size: font]
}
else {
fontCache[weigth]![size] = font
}
return font
}
static func light(size: CGFloat) -> UIFont {
font(weigth: .light, size: size)
}
static func regular(size: CGFloat) -> UIFont {
font(weigth: .regular, size: size)
}
static func thin(size: CGFloat) -> UIFont {
font(weigth: .thin, size: size)
}
}
public struct SFCompactDisplay { public struct SFCompactDisplay {
private static var fontCache = [UIFont.Weight: [CGFloat: UIFont]]() private static var fontCache = [UIFont.Weight: [CGFloat: UIFont]]()
static func font(weigth: UIFont.Weight, size: CGFloat) -> UIFont { static func font(weigth: UIFont.Weight, size: CGFloat) -> UIFont {
......
...@@ -147,6 +147,8 @@ class MoonPhaseCell: UITableViewCell { ...@@ -147,6 +147,8 @@ class MoonPhaseCell: UITableViewCell {
MoonPhaseCell.nowDateFormatter.timeZone = today?.timeZone MoonPhaseCell.nowDateFormatter.timeZone = today?.timeZone
moonTypeLabel.text = today?.moonPhase?.localized moonTypeLabel.text = today?.moonPhase?.localized
moonTypeImageView.image = today?.moonPhase?.image moonTypeImageView.image = today?.moonPhase?.image
//TODO:- Set default moon image if we don't have phase
moonImageView.image = today?.moonPhase?.pathImage moonImageView.image = today?.moonPhase?.pathImage
guard guard
......
...@@ -16,32 +16,50 @@ class AppTabBarController: UITabBarController { ...@@ -16,32 +16,50 @@ class AppTabBarController: UITabBarController {
public enum AppTab:Int, CaseIterable { public enum AppTab:Int, CaseIterable {
case today = 0 case today = 0
case forecast = 1 case forecast = 1
case radar = 2 case shorts = 2
case menu = 3 case radar = 3
case menu = 4
}
private var availableTabs: [AppTab] {
if ShortsManager.shared.shortsAvailable {
return [.today, .forecast, .shorts, .radar, .menu]
}
else {
return [.today, .forecast, .radar, .menu]
}
} }
public func setupTabBar() { public func setupTabBar() {
tabBar.tintColor = ThemeManager.currentTheme.tabBarTintColor tabBar.tintColor = ThemeManager.currentTheme.tabBarTintColor
tabBar.unselectedItemTintColor = ThemeManager.currentTheme.tabBarNormalColor tabBar.unselectedItemTintColor = ThemeManager.currentTheme.tabBarNormalColor
}
AppTab.allCases.forEach {
switch $0 { public func updateTabs() {
availableTabs.enumerated().forEach {
switch $1 {
case .today: case .today:
tabBar.items?[$0.rawValue].title = "TODAY" tabBar.items?[$0].title = "TODAY"
tabBar.items?[$0.rawValue].image = UIImage(named: "tab_today") tabBar.items?[$0].image = UIImage(named: "tab_today")
tabBar.items?[$0.rawValue].selectedImage = UIImage(named: "tab_today_selected") tabBar.items?[$0].selectedImage = UIImage(named: "tab_today_selected")
case .forecast: case .forecast:
tabBar.items?[$0.rawValue].title = "FORECAST" tabBar.items?[$0].title = "FORECAST"
tabBar.items?[$0.rawValue].image = UIImage(named: "tab_forecast") tabBar.items?[$0].image = UIImage(named: "tab_forecast")
tabBar.items?[$0.rawValue].selectedImage = UIImage(named: "tab_forecast_selected") tabBar.items?[$0].selectedImage = UIImage(named: "tab_forecast_selected")
case .shorts:
if ShortsManager.shared.shortsAvailable {
tabBar.items?[$0].title = "SHORTS"
tabBar.items?[$0].image = UIImage(named: "tab_shorts")
tabBar.items?[$0].selectedImage = UIImage(named: "tab_shorts_selected")
}
case .radar: case .radar:
tabBar.items?[$0.rawValue].title = "RADAR" tabBar.items?[$0].title = "RADAR"
tabBar.items?[$0.rawValue].image = UIImage(named: "tab_radar") tabBar.items?[$0].image = UIImage(named: "tab_radar")
tabBar.items?[$0.rawValue].selectedImage = UIImage(named: "tab_radar_selected") tabBar.items?[$0].selectedImage = UIImage(named: "tab_radar_selected")
case .menu: case .menu:
tabBar.items?[$0.rawValue].title = "MENU" tabBar.items?[$0].title = "MENU"
tabBar.items?[$0.rawValue].image = UIImage(named: "tab_menu") tabBar.items?[$0].image = UIImage(named: "tab_menu")
tabBar.items?[$0.rawValue].selectedImage = UIImage(named: "tab_menu_selected") tabBar.items?[$0].selectedImage = UIImage(named: "tab_menu_selected")
} }
} }
} }
......
...@@ -156,7 +156,7 @@ private extension ForecastViewController { ...@@ -156,7 +156,7 @@ private extension ForecastViewController {
func prepareTableView() { func prepareTableView() {
forecastCellFactory.registerCells(on: tableView) forecastCellFactory.registerCells(on: tableView)
tableView.contentInset = .init(top: 0, left: 0, bottom: 15, right: 0) tableView.contentInset = .init(top: 0, left: 0, bottom: 0, right: 0)
tableView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor tableView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
tableView.separatorStyle = .none tableView.separatorStyle = .none
tableView.tableFooterView = UIView() tableView.tableFooterView = UIView()
...@@ -173,7 +173,7 @@ private extension ForecastViewController { ...@@ -173,7 +173,7 @@ private extension ForecastViewController {
func prepareTimePeriodControl() { func prepareTimePeriodControl() {
let container = UIView() let container = UIView()
container.addSubview(self.timePeriodControl) container.addSubview(self.timePeriodControl)
timePeriodControl.selectedSegmentIndex = 0 timePeriodControl.selectedSegmentIndex = forecastCellFactory.timePeriod.rawValue
timePeriodControl.addTarget(self, action: #selector(handleSegmentDidChange), for: .valueChanged) timePeriodControl.addTarget(self, action: #selector(handleSegmentDidChange), for: .valueChanged)
self.timePeriodControl.snp.makeConstraints { (make) in self.timePeriodControl.snp.makeConstraints { (make) in
......
...@@ -81,10 +81,13 @@ private extension SettingsCell { ...@@ -81,10 +81,13 @@ private extension SettingsCell {
func prepareLabels() { func prepareLabels() {
settingsNameLabel.font = AppFont.SFPro.bold(size: 16) settingsNameLabel.font = AppFont.SFPro.bold(size: 16)
settingsNameLabel.textAlignment = .left settingsNameLabel.textAlignment = .left
settingsNameLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
container.addSubview(settingsNameLabel) container.addSubview(settingsNameLabel)
selectedValueLabel.font = AppFont.SFPro.regular(size: 16) selectedValueLabel.font = AppFont.SFPro.regular(size: 16)
selectedValueLabel.textAlignment = .right selectedValueLabel.textAlignment = .right
selectedValueLabel.lineBreakMode = .byTruncatingMiddle
selectedValueLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
container.addSubview(selectedValueLabel) container.addSubview(selectedValueLabel)
arrowLabel.font = AppFont.SFPro.regular(size: 18) arrowLabel.font = AppFont.SFPro.regular(size: 18)
...@@ -106,6 +109,7 @@ private extension SettingsCell { ...@@ -106,6 +109,7 @@ private extension SettingsCell {
selectedValueLabel.snp.makeConstraints { (make) in selectedValueLabel.snp.makeConstraints { (make) in
make.right.equalTo(arrowLabel.snp.left).inset(-8) make.right.equalTo(arrowLabel.snp.left).inset(-8)
make.left.equalTo(settingsNameLabel.snp.right).offset(8)
make.centerY.equalToSuperview() make.centerY.equalToSuperview()
} }
} }
......
...@@ -157,7 +157,7 @@ private extension SettingsThemeCell { ...@@ -157,7 +157,7 @@ private extension SettingsThemeCell {
lightThemeImageView.snp.makeConstraints { (make) in lightThemeImageView.snp.makeConstraints { (make) in
make.size.equalTo(CGSize(width: 85, height: 154)) make.size.equalTo(CGSize(width: 85, height: 154))
make.top.equalToSuperview().inset(24) make.top.equalToSuperview().inset(24)
make.left.equalToSuperview().inset(57) make.centerX.equalToSuperview().offset(-85)
} }
lightThemeLabel.snp.makeConstraints { (make) in lightThemeLabel.snp.makeConstraints { (make) in
...@@ -197,7 +197,7 @@ private extension SettingsThemeCell { ...@@ -197,7 +197,7 @@ private extension SettingsThemeCell {
darkThemeImageView.snp.makeConstraints { (make) in darkThemeImageView.snp.makeConstraints { (make) in
make.size.equalTo(CGSize(width: 85, height: 154)) make.size.equalTo(CGSize(width: 85, height: 154))
make.top.equalToSuperview().inset(24) make.top.equalToSuperview().inset(24)
make.right.equalToSuperview().inset(57) make.centerX.equalToSuperview().offset(85)
} }
darkThemeLabel.snp.makeConstraints { (make) in darkThemeLabel.snp.makeConstraints { (make) in
......
//
// ShortsSwipeHelperView.swift
// 1Weather
//
// Created by Dmitry Stepanets on 21.06.2021.
//
import UIKit
enum ShortsSwipeState {
case downViewedAll
case upNext
}
class ShortsSwipeHelperView: UIView {
//Private
private let label = UILabel()
private let imageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
prepareLabel()
prepareImageView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(forState state:ShortsSwipeState) {
switch state {
case .downViewedAll:
imageView.transform = .identity
label.text = "shorts.viewedAll".localized()
case .upNext:
imageView.transform = CGAffineTransform(rotationAngle: 180 * .pi / 180)
label.text = "shorts.swipeUp".localized()
}
}
}
//MARK:- Prepare
private extension ShortsSwipeHelperView {
func prepareLabel() {
label.font = AppFont.SFPro.regular(size: 12)
label.numberOfLines = 0
label.textAlignment = .center
label.lineBreakMode = .byWordWrapping
label.textColor = .white
addSubview(label)
label.snp.makeConstraints { make in
make.left.bottom.right.equalToSuperview()
}
}
func prepareImageView() {
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(named: "shorts_swipe_down")
addSubview(imageView)
imageView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalToSuperview()
make.width.equalTo(14)
make.height.equalTo(30)
make.bottom.equalTo(label.snp.top).offset(-8)
}
}
}
//
// ShortsUnreadNudgeView.swift
// 1Weather
//
// Created by Dmitry Stepanets on 21.06.2021.
//
import UIKit
class ShortsUnreadNudgeView: UIView {
//Private
private let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
prepareLabel()
updateUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateUI()
}
override func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = (bounds.height / 2).rounded(.down)
}
func configure(withCount count:Int) {
label.text = "shorts.unread".localizedFormat(count)
}
private func updateUI() {
switch interfaceStyle {
case .light:
label.textColor = ThemeManager.currentTheme.secondaryTextColor
case .dark:
label.textColor = ThemeManager.currentTheme.primaryTextColor
}
backgroundColor = ThemeManager.currentTheme.containerBackgroundColor
}
}
private extension ShortsUnreadNudgeView {
func prepareLabel() {
label.font = AppFont.SFPro.regular(size: 12)
label.textAlignment = .center
label.text = "shorts.unread".localizedFormat(0)
addSubview(label)
label.snp.makeConstraints { make in
make.left.right.equalToSuperview().inset(26)
make.top.bottom.equalToSuperview().inset(12)
}
}
}
...@@ -14,6 +14,7 @@ public enum TodayCellType:Int { ...@@ -14,6 +14,7 @@ public enum TodayCellType:Int {
case adBanner case adBanner
case adMREC case adMREC
case conditions case conditions
case shorts
case forecastPeriod case forecastPeriod
case precipitation case precipitation
case airQuality case airQuality
...@@ -55,9 +56,9 @@ class TodayCellFactory: CellFactory { ...@@ -55,9 +56,9 @@ class TodayCellFactory: CellFactory {
private var cellsToUpdate:CellsToUpdate = [.condition, .timePeriod, .precipitation, .dayTime] private var cellsToUpdate:CellsToUpdate = [.condition, .timePeriod, .precipitation, .dayTime]
private let todayViewModel:TodayViewModel private let todayViewModel:TodayViewModel
private var todaySection = TodaySection(allRows: [.alert, .forecast, .adBanner, private var todaySection = TodaySection(allRows: [.alert, .forecast, .adBanner, .conditions,
.conditions, .forecastPeriod, .precipitation, .adMREC, .shorts, .forecastPeriod, .precipitation, .adMREC,
.airQuality, .dayTime, .sun, .moon]) .airQuality, .dayTime, .sun, .moon])
private var adViewCache = [IndexPath: AdView]() private var adViewCache = [IndexPath: AdView]()
...@@ -80,6 +81,7 @@ class TodayCellFactory: CellFactory { ...@@ -80,6 +81,7 @@ class TodayCellFactory: CellFactory {
registerCell(type: TodayForecastCell.self, tableView: tableView) registerCell(type: TodayForecastCell.self, tableView: tableView)
registerCell(type: BannerAdCell.self, tableView: tableView) registerCell(type: BannerAdCell.self, tableView: tableView)
registerCell(type: MRECAdCell.self, tableView: tableView) registerCell(type: MRECAdCell.self, tableView: tableView)
registerCell(type: TodayShortsCell.self, tableView: tableView)
registerCell(type: TodayConditionsCell.self, tableView: tableView) registerCell(type: TodayConditionsCell.self, tableView: tableView)
registerCell(type: TodayForecastTimePeriodCell.self, tableView: tableView) registerCell(type: TodayForecastTimePeriodCell.self, tableView: tableView)
registerCell(type: PrecipitationCell.self, tableView: tableView) registerCell(type: PrecipitationCell.self, tableView: tableView)
...@@ -97,11 +99,13 @@ class TodayCellFactory: CellFactory { ...@@ -97,11 +99,13 @@ class TodayCellFactory: CellFactory {
adView.delegate = self adView.delegate = self
adView.loggingAlias = "📍 Today Banner" adView.loggingAlias = "📍 Today Banner"
var adType = AdType.banner var adType = AdType.banner
var placementName = placementNameTodayBanner
if cellType(at: indexPath) == .adMREC { if cellType(at: indexPath) == .adMREC {
adType = .square adType = .square
placementName = placementNameTodaySquare
adView.loggingAlias = "📅 Today MREC" adView.loggingAlias = "📅 Today MREC"
} }
adView.set(placementName: placementNameTodayBanner, adType: adType) adView.set(placementName: placementName, adType: adType)
adViewCache[indexPath] = adView adViewCache[indexPath] = adView
return adView return adView
} }
...@@ -134,6 +138,11 @@ class TodayCellFactory: CellFactory { ...@@ -134,6 +138,11 @@ class TodayCellFactory: CellFactory {
let cell = dequeueReusableCell(type: MRECAdCell.self, tableView: tableView, indexPath: indexPath) let cell = dequeueReusableCell(type: MRECAdCell.self, tableView: tableView, indexPath: indexPath)
cell.adView = adView(for: indexPath) cell.adView = adView(for: indexPath)
return cell return cell
case .shorts:
let cell = dequeueReusableCell(type: TodayShortsCell.self, tableView: tableView, indexPath: indexPath)
cell.delegate = self
cell.reload()
return cell
case .conditions: case .conditions:
let cell = dequeueReusableCell(type: TodayConditionsCell.self, tableView: tableView, indexPath: indexPath) let cell = dequeueReusableCell(type: TodayConditionsCell.self, tableView: tableView, indexPath: indexPath)
cell.configure(with: loc) cell.configure(with: loc)
...@@ -199,6 +208,8 @@ class TodayCellFactory: CellFactory { ...@@ -199,6 +208,8 @@ class TodayCellFactory: CellFactory {
moonCell.updateMoonPosition() moonCell.updateMoonPosition()
case let adCell as AdCell: case let adCell as AdCell:
adCell.adView?.start() adCell.adView?.start()
case cell as TodayShortsCell:
todayViewModel.logShortsSectionEvent()
default: default:
break break
} }
...@@ -230,14 +241,25 @@ class TodayCellFactory: CellFactory { ...@@ -230,14 +241,25 @@ class TodayCellFactory: CellFactory {
rowsToHide.insert(.airQuality) rowsToHide.insert(.airQuality)
} }
if location?.today?.moonset == nil && (location?.today?.moonrise == nil || location?.today?.approximateMoonrise == nil) { if location?.today?.moonset == nil || (location?.today?.moonrise == nil && location?.today?.approximateMoonrise == nil) {
rowsToHide.insert(.moon) rowsToHide.insert(.moon)
} }
if !ShortsManager.shared.shortsAvailable {
rowsToHide.insert(.shorts)
}
todaySection.hiddenRows = rowsToHide todaySection.hiddenRows = rowsToHide
} }
} }
//MARK:- TodayShortsCell Delegate
extension TodayCellFactory: TodayShortsCellDelegate {
func didSelectShort(atIndex index: Int) {
todayViewModel.openShorts(atIndex: index)
}
}
// MARK: - AdViewDelegate // MARK: - AdViewDelegate
extension TodayCellFactory: AdViewDelegate { extension TodayCellFactory: AdViewDelegate {
func succeeded(adView: AdView, hadAdBefore: Bool) { func succeeded(adView: AdView, hadAdBefore: Bool) {
......
...@@ -55,40 +55,40 @@ class TodayDayTimesCell: UITableViewCell { ...@@ -55,40 +55,40 @@ class TodayDayTimesCell: UITableViewCell {
public func configure(with location: Location) { public func configure(with location: Location) {
let maxNumberOfItems = 4 let maxNumberOfItems = 4
guard !location.dayTimeForecast.isEmpty else {
return
}
var validDayTimeWeather = [DayTimeWeather]() var validDayTimeWeather = [DayTimeWeather]()
for index in 0..<location.dayTimeForecast.count { for dayTimeWeather: DayTimeWeather in location.dayTimeForecast {
if location.dayTimeForecast[index].date >= Date() { if dayTimeWeather.date >= Date() {
validDayTimeWeather.append(location.dayTimeForecast[index]) validDayTimeWeather.append(dayTimeWeather)
} if validDayTimeWeather.count == maxNumberOfItems {
} break
}
if validDayTimeWeather.isEmpty {
return
}
if stackView.arrangedSubviews.isEmpty {
for index in 0..<maxNumberOfItems {
let view = DayTimeView(dayTimeWeather: validDayTimeWeather[index],
withSeparator: index != maxNumberOfItems - 1)
stackView.addArrangedSubview(view)
} }
stackView.layoutIfNeeded()
return
} }
if stackView.arrangedSubviews.count == maxNumberOfItems { var currentValidWeatherIndex = 0
for (index, arrangedSubview) in stackView.arrangedSubviews.enumerated() { for (index, arrangedSubview) in stackView.arrangedSubviews.enumerated() {
// Configure existing views
if currentValidWeatherIndex < validDayTimeWeather.count {
guard let dayTimeView = arrangedSubview as? DayTimeView else { guard let dayTimeView = arrangedSubview as? DayTimeView else {
continue continue
} }
dayTimeView.configure(with: validDayTimeWeather[currentValidWeatherIndex])
dayTimeView.configure(with: validDayTimeWeather[index]) currentValidWeatherIndex += 1
} }
else {
// Remove excessive views
arrangedSubview.removeFromSuperview()
}
}
// Add new views if needed.
while currentValidWeatherIndex < validDayTimeWeather.count {
let isLastItem = currentValidWeatherIndex == validDayTimeWeather.count - 1
let view = DayTimeView(dayTimeWeather: validDayTimeWeather[currentValidWeatherIndex],
withSeparator: !isLastItem)
stackView.addArrangedSubview(view)
currentValidWeatherIndex += 1
} }
stackView.layoutIfNeeded()
} }
} }
......
//
// TodayShortsCell.swift
// 1Weather
//
// Created by Dmitry Stepanets on 09.06.2021.
//
import UIKit
import OneWeatherCore
protocol TodayShortsCellDelegate: AnyObject {
func didSelectShort(atIndex index: Int)
}
class TodayShortsCell: UITableViewCell {
//Private
private let headingLabel = UILabel()
private let shortsView = ShortsView()
//Public
weak var delegate: TodayShortsCellDelegate?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
prepareHeadingLabel()
prepareShortsView()
updateUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateUI()
}
func reload() {
self.shortsView.reload()
}
private func updateUI() {
contentView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
switch interfaceStyle {
case .light:
headingLabel.textColor = ThemeManager.currentTheme.secondaryTextColor
case .dark:
headingLabel.textColor = ThemeManager.currentTheme.primaryTextColor
}
}
}
//MARK:- Prepare
private extension TodayShortsCell {
func prepareHeadingLabel() {
headingLabel.font = AppFont.SFPro.bold(size: 16)
headingLabel.text = "1W SHORTS"
contentView.addSubview(headingLabel)
headingLabel.snp.makeConstraints { make in
make.left.equalToSuperview().inset(18)
make.top.equalToSuperview().inset(15)
}
}
func prepareShortsView() {
shortsView.delegate = self
contentView.addSubview(shortsView)
shortsView.snp.makeConstraints { make in
make.top.equalTo(headingLabel.snp.bottom).offset(18)
make.left.bottom.right.equalToSuperview()
make.height.equalTo(170)
}
}
}
//MARK:- ShortsView Delegate
extension TodayShortsCell: ShortsViewDelegate {
func didSelectShort(at index: Int) {
delegate?.didSelectShort(atIndex: index)
}
}
//
// ShortsCollectionViewCell.swift
// 1Weather
//
// Created by Dmitry Stepanets on 09.06.2021.
//
import UIKit
import Nuke
import OneWeatherCore
class ShortsCollectionViewCell: UICollectionViewCell {
//Private
private let imageView = UIImageView()
private let footerView = UIView()
private let footerLabel = UILabel()
private var currentShortId: String?
override init(frame: CGRect) {
super.init(frame: frame)
prepare()
prepareImage()
prepareFooter()
}
func configure(shortsItem: ShortsItem) {
self.currentShortId = shortsItem.id
footerLabel.text = shortsItem.title
if let image = self.bestImageForCellSize(images: shortsItem.images) {
let resizeOptions = ImageProcessors.Resize(size: self.bounds.size, crop: true)
let cornerRadius = ImageProcessors.RoundedCorners(radius: 12)
let imageRequest = ImageRequest(url: image.url, processors: [resizeOptions, cornerRadius])
ImagePipeline.shared.loadImage(with: imageRequest) { result in
switch result {
case .success(let response):
ShortsManager.shared.backgroundColor(for: shortsItem, shortImage: response.image) {[weak self] avgColor, shortId in
guard self?.currentShortId == shortId else { return }
onMain {
if let color = avgColor {
self?.imageView.image = response.image
self?.footerView.backgroundColor = color
}
}
}
default:
break
}
}
}
else {
imageView.image = nil
footerView.backgroundColor = ThemeManager.currentTheme.containerBackgroundColor
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
self.imageView.image = nil
self.footerView.backgroundColor = .clear
}
private func bestImageForCellSize(images:[ShortsItemImage]) -> ShortsItemImage? {
guard !images.isEmpty && self.frame != .zero else {
return nil
}
var image = images.first
for itemImage in images {
if CGFloat(itemImage.width) >= self.frame.width && CGFloat(itemImage.height) >= self.frame.height {
image = itemImage
break
}
}
return image
}
func startZoomAnimation() {
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.beginTime = 0
animation.fromValue = 1.0
animation.toValue = 1.8
animation.duration = 10
animation.autoreverses = true
animation.isRemovedOnCompletion = true
imageView.layer.add(animation, forKey: "scale-layer")
}
func stopZoomAnimation() {
imageView.layer.removeAllAnimations()
}
}
//MARK:- Prepare
private extension ShortsCollectionViewCell {
func prepare() {
contentView.clipsToBounds = true
contentView.layer.cornerRadius = 12
contentView.backgroundColor = ThemeManager.currentTheme.containerBackgroundColor
}
func prepareImage() {
imageView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill
contentView.addSubview(imageView)
imageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
func prepareFooter() {
//View
footerView.clipsToBounds = true
footerView.layer.cornerRadius = 12
footerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
contentView.addSubview(footerView)
footerView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.height.equalToSuperview().multipliedBy(0.37)
}
//Label
footerLabel.font = AppFont.SFPro.medium(size: 14)
footerLabel.textColor = .white
footerLabel.lineBreakMode = .byWordWrapping
footerLabel.numberOfLines = 3
footerView.addSubview(footerLabel)
footerLabel.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(6)
}
}
}
//
// ShortsCollectionViewLayout.swift
// 1Weather
//
// Created by Dmitry Stepanets on 09.06.2021.
//
import UIKit
class ShortsCollectionViewLayout: UICollectionViewFlowLayout {
private let kItemWidth:CGFloat = 138
override init() {
super.init()
commonInit()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func commonInit() {
scrollDirection = .horizontal
minimumLineSpacing = 12
minimumInteritemSpacing = 0
sectionInset = .init(top: 0, left: 16, bottom: 0, right: 16)
}
//MARK: Overrides
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override func prepare() {
super.prepare()
guard let collectionView = collectionView else {
return
}
let size = collectionView.bounds.size
if size.height != itemSize.height {
itemSize = .init(width: kItemWidth, height: size.height)
invalidateLayout()
}
}
}
//
// ShortsView.swift
// 1Weather
//
// Created by Dmitry Stepanets on 09.06.2021.
//
import UIKit
import OneWeatherCore
import OneWeatherAnalytics
import Nuke
protocol ShortsViewDelegate: AnyObject {
func didSelectShort(at index:Int)
}
class ShortsView: UIView {
//Private
private let prefetcher = ImagePrefetcher()
private let headingLabel = UILabel()
private let collectionView = UICollectionView(frame: .zero, collectionViewLayout: ShortsCollectionViewLayout())
private let avgColorProcessQueue = OperationQueue()
private let avgColorReadQueue = OperationQueue()
private var shorts: [ShortsItem] {
return ShortsManager.shared.shorts
}
private var averageColorCache = ThreadSafeDictionary<AnyHashable, UIColor>()
//Public
weak var delegate: ShortsViewDelegate?
init() {
super.init(frame: .zero)
//Setup queues
avgColorReadQueue.maxConcurrentOperationCount = 1
prepareCollectionView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func reload() {
onMain {
self.collectionView.reloadData()
}
}
}
//MARK:- Prepare
private extension ShortsView {
func prepareCollectionView() {
collectionView.isPrefetchingEnabled = true
collectionView.showsVerticalScrollIndicator = false
collectionView.showsHorizontalScrollIndicator = false
collectionView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
collectionView.register(ShortsCollectionViewCell.self, forCellWithReuseIdentifier: ShortsCollectionViewCell.kIdentifier)
collectionView.dataSource = self
collectionView.delegate = self
// collectionView.prefetchDataSource = self
addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.height.equalToSuperview()
}
}
}
//MARK:- UICollectionView Data Source
extension ShortsView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return shorts.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ShortsCollectionViewCell.kIdentifier,
for: indexPath) as! ShortsCollectionViewCell
cell.configure(shortsItem: shorts[indexPath.row])
return cell
}
}
//MARK:- UICollectionView Delegate
extension ShortsView: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.row == 0 {
(cell as? ShortsCollectionViewCell)?.startZoomAnimation()
}
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.row == 0 {
(cell as? ShortsCollectionViewCell)?.stopZoomAnimation()
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let visiblePath = (collectionView.indexPathsForVisibleItems.sorted{$0.row < $1.row})
guard let lastVisibleRow = visiblePath.last?.row else { return }
AppAnalytics.shared.log(event: .ANALYTICS_SHORTS_CARD_VIEW,
params: [.ANALYTICS_KEY_SHORTS_CARD_POSITION : lastVisibleRow])
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let visiblePath = (collectionView.indexPathsForVisibleItems.sorted{$0.row < $1.row})
guard let lastVisibleRow = visiblePath.last?.row else { return }
AppAnalytics.shared.log(event: .ANALYTICS_SHORTS_CARD_VIEW,
params: [.ANALYTICS_KEY_SHORTS_CARD_POSITION : lastVisibleRow])
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
delegate?.didSelectShort(at: indexPath.row)
AppAnalytics.shared.log(event: .ANALYTICS_SHORTS_CARD_CLICK,
params: [.ANALYTICS_KEY_SHORTS_CARD_ID : shorts[indexPath.row].id,
.ANALYTICS_KEY_SHORTS_CARD_POSITION : indexPath.row,
.ANALYTICS_KEY_SOURCE : "TODAY"])
}
}
//MARK:- UICollectionView Data Source Prefetching
extension ShortsView: UICollectionViewDataSourcePrefetching {
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
let urls = shorts.map{ $0.images[0].url }
prefetcher.startPrefetching(with: urls)
}
func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
let urls = shorts.map{ $0.images[0].url }
prefetcher.stopPrefetching(with: urls)
}
}
...@@ -12,7 +12,7 @@ import OneWeatherAnalytics ...@@ -12,7 +12,7 @@ import OneWeatherAnalytics
class TodayViewController: UIViewController { class TodayViewController: UIViewController {
//Private //Private
private let coordinator:TodayCoordinator private let coordinator:TodayCoordinator
private let viewModel = TodayViewModel() private let viewModel = TodayViewModel(shortsManager: ShortsManager.shared)
private let notificationButton = UIButton() private let notificationButton = UIButton()
private let cityButton = NavigationCityButton() private let cityButton = NavigationCityButton()
......
...@@ -218,7 +218,7 @@ private extension WidgetPromotionController { ...@@ -218,7 +218,7 @@ private extension WidgetPromotionController {
learnButton.addTarget(self, action: #selector(handleLearnButton), for: .touchUpInside) learnButton.addTarget(self, action: #selector(handleLearnButton), for: .touchUpInside)
learnButton.setTitle("widget.promotion.learn".localized(), for: .normal) learnButton.setTitle("widget.promotion.learn".localized(), for: .normal)
learnButton.setTitleColor(ThemeManager.currentTheme.graphTintColor, for: .normal) learnButton.setTitleColor(ThemeManager.currentTheme.graphTintColor, for: .normal)
learnButton.setTitleColor(ThemeManager.currentTheme.graphTintColor.darken(by: 10), for: .highlighted) learnButton.setTitleColor(ThemeManager.currentTheme.graphTintColor.darker(by: 10), for: .highlighted)
learnButton.layer.borderWidth = 2 learnButton.layer.borderWidth = 2
learnButton.layer.borderColor = ThemeManager.currentTheme.graphTintColor.cgColor learnButton.layer.borderColor = ThemeManager.currentTheme.graphTintColor.cgColor
learnButton.layer.cornerRadius = 6 learnButton.layer.cornerRadius = 6
......
//
// ShortsViewModel.swift
// 1Weather
//
// Created by Dmitry Stepanets on 10.06.2021.
//
import UIKit
import OneWeatherCore
import OneWeatherAnalytics
class ShortsViewModel: ViewModelProtocol {
//Private
private let shortsManager: ShortsManager
//Public
weak var delegate: ViewModelDelegate?
var shorts: [ShortsItem] {
return shortsManager.shorts
}
init(shortsManager: ShortsManager) {
self.shortsManager = shortsManager
shortsManager.multicastDelegate.add(delegate: self)
}
func refreshShorts() {
shortsManager.refreshShorts()
}
func like(item: ShortsItem) {
shortsManager.like(item: item)
self.delegate?.viewModelDidChange(model: self)
AppAnalytics.shared.log(event: .ANALYTICS_SHORTS_LIKE_BUTTON_CLICK,
params: [.ANALYTICS_KEY_SHORTS_CARD_ID : item.id])
}
func openMore(item: ShortsItem) {
guard let url = item.ctaURL else { return }
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:])
AppAnalytics.shared.log(event: .ANALYTICS_SHORTS_READ_MORE_CLICK,
params: [.ANALYTICS_KEY_SHORTS_CARD_ID : item.id,
.ANALYTICS_KEY_SHORTS_READ_MORE_VIEW : "external browser"])
}
}
func markAsViewed(atIndex index: Int) {
guard index < shorts.count else { return }
if Settings.shared.shortsSwipeUpNudgeShowedCount <= ConfigManager.shared.config.shortsSwipeUpNudgeCount {
Settings.shared.shortsSwipeUpNudgeShowedCount += 1
}
shortsManager.markAsViewed(index: index)
}
}
//MARK:- ShortManager Delegate
extension ShortsViewModel: ShortsManagerDelegate {
func shortsDidChange() {
self.delegate?.viewModelDidChange(model: self)
}
}
...@@ -21,8 +21,19 @@ class TodayViewModel: ViewModelProtocol { ...@@ -21,8 +21,19 @@ class TodayViewModel: ViewModelProtocol {
public weak var delegate:TodayViewModelDelegate? public weak var delegate:TodayViewModelDelegate?
//Private //Private
private let locationManager: LocationManager! = LocationManager.shared private let locationManager: LocationManager
private let configManager: ConfigManager
private let kAnalyticsThresholdSec = 2.5
private let log = Logger(componentName: "TodayViewModel")
private var ccpaHelper = CCPAHelper.shared
private let shortsManager: ShortsManager
private var canSendAnalytics = true
private var showingAttPrompt: Bool = false
private(set) var location:Location? private(set) var location:Location?
var shorts: [ShortsItem] {
shortsManager.shorts
}
public lazy var todayCellFactory:TodayCellFactory = { public lazy var todayCellFactory:TodayCellFactory = {
let factory = TodayCellFactory(viewModel: self) let factory = TodayCellFactory(viewModel: self)
...@@ -36,13 +47,18 @@ class TodayViewModel: ViewModelProtocol { ...@@ -36,13 +47,18 @@ class TodayViewModel: ViewModelProtocol {
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
} }
public init() { public init(locationManager: LocationManager = LocationManager.shared, configManager: ConfigManager = ConfigManager.shared, shortsManager: ShortsManager = ShortsManager.shared) {
self.location = LocationManager.shared.selectedLocation self.locationManager = locationManager
self.configManager = configManager
self.shortsManager = shortsManager
self.shortsManager.multicastDelegate.add(delegate: self)
self.location = locationManager.selectedLocation
locationManager.add(delegate: self) locationManager.add(delegate: self)
Settings.shared.delegate.add(delegate: self) Settings.shared.delegate.add(delegate: self)
NotificationCenter.default.addObserver(self, selector: #selector(handlePremiumStateChange), name: Notification.Name(rawValue: kEventInAppPurchasedCompleted), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handlePremiumStateChange), name: Notification.Name(rawValue: kEventInAppPurchasedCompleted), object: nil)
} }
//MARK: Private
@objc @objc
private func handlePremiumStateChange() { private func handlePremiumStateChange() {
onMain { onMain {
...@@ -51,15 +67,11 @@ class TodayViewModel: ViewModelProtocol { ...@@ -51,15 +67,11 @@ class TodayViewModel: ViewModelProtocol {
} }
} }
public func updateWeather() {
locationManager.updateEverythingIfNeeded()
}
private func initializeAllAdsIfNeeded() { private func initializeAllAdsIfNeeded() {
onMain { onMain {
let adManager = AdManager.shared let adManager = AdManager.shared
guard !adManager.initialized else { return } guard !adManager.initialized else { return }
CCPAHelper.shared.updateCCPAStatus(reason: "initialization") self.ccpaHelper.updateCCPAStatus(reason: "initialization")
if !isAppPro() { if !isAppPro() {
// In Debug mode we allow the user to change the environment // In Debug mode we allow the user to change the environment
...@@ -68,7 +80,7 @@ class TodayViewModel: ViewModelProtocol { ...@@ -68,7 +80,7 @@ class TodayViewModel: ViewModelProtocol {
adManager.start(a9AppKey: a9AppKey, mopubAppKey: kAdMoPubInitializationAdUnitId, useGeolocation: true) adManager.start(a9AppKey: a9AppKey, mopubAppKey: kAdMoPubInitializationAdUnitId, useGeolocation: true)
// Location SDK setup // Location SDK setup
let canCollectData = CCPAHelper.shared.canCollectData ?? false let canCollectData = self.ccpaHelper.canCollectData ?? false
PSMLocation.start(withIdentifier: "OneWeatheriosCfg3", halted: !canCollectData) PSMLocation.start(withIdentifier: "OneWeatheriosCfg3", halted: !canCollectData)
print("PSM Location SDK version \(PSMLocation.sdkVersion())") print("PSM Location SDK version \(PSMLocation.sdkVersion())")
PSMLocation.sharedInstance().powerSaveMode = true PSMLocation.sharedInstance().powerSaveMode = true
...@@ -76,7 +88,19 @@ class TodayViewModel: ViewModelProtocol { ...@@ -76,7 +88,19 @@ class TodayViewModel: ViewModelProtocol {
} }
} }
private var ccpaHelper = CCPAHelper.shared private func onboardingFlowCompleted() {
self.initializeAllAdsIfNeeded()
PushNotificationsManager.shared.registerForRemoteNotifications()
analytics(log: .ANALYTICS_VIEW_TODAY)
}
//MARK: Public
public func updateWeather() {
locationManager.updateEverythingIfNeeded()
if shortsManager.shortsAvailable {
shortsManager.refreshShorts()
}
}
public func privacyPolicyHasBeenViewed() { public func privacyPolicyHasBeenViewed() {
ccpaHelper.shownPrivacyNoticeBefore = true ccpaHelper.shownPrivacyNoticeBefore = true
...@@ -99,8 +123,17 @@ class TodayViewModel: ViewModelProtocol { ...@@ -99,8 +123,17 @@ class TodayViewModel: ViewModelProtocol {
} }
else { else {
if #available(iOS 14.5, *) { if #available(iOS 14.5, *) {
if !CCPAHelper.shared.isNewUser && ATTrackingManager.trackingAuthorizationStatus == .notDetermined { guard !self.showingAttPrompt else {
// not calling onboardingFlowCompleted, because it will be called in the ATT prompt completion handler.
return
}
if self.configManager.config.showAttPrompt
&& !self.ccpaHelper.isNewUser
&& ATTrackingManager.trackingAuthorizationStatus == .notDetermined {
self.showingAttPrompt = true
ATTrackingManager.requestTrackingAuthorization { result in ATTrackingManager.requestTrackingAuthorization { result in
self.showingAttPrompt = false
self.onboardingFlowCompleted() self.onboardingFlowCompleted()
if result == .authorized { if result == .authorized {
analytics(log: .ANALYTICS_ATT_ACCEPTED) analytics(log: .ANALYTICS_ATT_ACCEPTED)
...@@ -126,10 +159,17 @@ class TodayViewModel: ViewModelProtocol { ...@@ -126,10 +159,17 @@ class TodayViewModel: ViewModelProtocol {
} }
} }
private func onboardingFlowCompleted() { public func logShortsSectionEvent() {
self.initializeAllAdsIfNeeded() guard canSendAnalytics else { return }
PushNotificationsManager.shared.registerForRemoteNotifications() canSendAnalytics = false
analytics(log: .ANALYTICS_VIEW_TODAY) AppAnalytics.shared.log(event: .ANALYTICS_SHORTS_SECTION_VIEW, params: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + kAnalyticsThresholdSec) {
self.canSendAnalytics = true
}
}
public func openShorts(atIndex index: Int) {
AppCoordinator.instance.openShorts(atIndex: index)
} }
} }
...@@ -138,6 +178,9 @@ extension TodayViewModel: LocationManagerDelegate { ...@@ -138,6 +178,9 @@ extension TodayViewModel: LocationManagerDelegate {
func locationManager(_ locationManager: LocationManager, changedSelectedLocation newLocation: Location?) { func locationManager(_ locationManager: LocationManager, changedSelectedLocation newLocation: Location?) {
onMain { onMain {
self.location = newLocation self.location = newLocation
if self.shortsManager.shortsAvailable {
self.shortsManager.refreshShorts()
}
self.todayCellFactory.setNeedsUpdate() self.todayCellFactory.setNeedsUpdate()
self.delegate?.viewModelDidChange(model: self) self.delegate?.viewModelDidChange(model: self)
} }
...@@ -161,3 +204,12 @@ extension TodayViewModel: CellFactoryDelegate { ...@@ -161,3 +204,12 @@ extension TodayViewModel: CellFactoryDelegate {
delegate?.viewModelDidChange(model: self) delegate?.viewModelDidChange(model: self)
} }
} }
//MARK:- ShortsManager Delegate
extension TodayViewModel: ShortsManagerDelegate {
func shortsDidChange() {
onMain {
self.delegate?.viewModelDidChange(model: self)
}
}
}
import UIKit
let urlString = "https://kiowacountypress.net/content/hurricanes-wildfires-tornadoes-floods-–-whatever-your-local-risk-heres-how-be-more-weather"
let encodedString = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
let url = URL(string: encodedString!)
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<playground version='5.0' target-platform='ios' buildActiveScheme='true' importAppTypes='true'>
<timeline fileName='timeline.xctimeline'/>
</playground>
\ No newline at end of file
//
// InMobiShortSource.swift
// InMobiShortsSource
//
// Created by Dmitry Stepanets on 07.06.2021.
//
import Foundation
import OneWeatherCore
import OneWeatherAnalytics
public enum InMobiShortSourceError: Error {
case badUrl
case invalidIds
case networkError(Error?)
case badServerResponse(Error?)
case dataEncodingError(String)
}
public class InMobiShortSource: ShortSource {
private let log = Logger(componentName: "InMobiShortSource")
private let baseURL = "https://in.api.glance.inmobi.com/api/v0/glance"
private let apiKey = "9b5f4682b74f75cbedb34ee235c988f9"
private let region = "US"
private let sdkV = "80000"
public init() {}
public func updateShorts(completion: @escaping ShortsSourceCompletion) {
self.getGlanceIds {[weak self] glanceIds, glanceIdsError in
guard let self = self else { return }
guard glanceIdsError == nil else {
completion(nil, glanceIdsError)
return
}
guard
let ids = glanceIds,
!ids.isEmpty
else {
self.log.debug("Glance IDs is empty")
completion(nil, InMobiShortSourceError.invalidIds)
return
}
var urlString = "https://in.api.glance.inmobi.com/api/v0/glance/data/ad?region=\(self.region)&sdkV=\(self.sdkV)&ids="
for (index, id) in ids.enumerated() {
urlString += id.glanceId
if index != ids.count - 1 {
urlString += ","
}
}
guard let updateURL = URL(string: urlString) else {
assertionFailure("Should never happen. The URL should be correct.")
return
}
var request = URLRequest(url: updateURL)
request.httpMethod = "GET"
request.addValue(self.apiKey, forHTTPHeaderField:"X-Api-Key")
let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
guard error == nil else {
self.log.debug("Network response: error \(String(describing: error))")
completion(nil, InMobiShortSourceError.networkError(error))
return
}
guard let data = data else {
self.log.debug("Network response: error Invalid data")
completion(nil, InMobiShortSourceError.dataEncodingError("Invalid data"))
return
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .useDefaultKeys
do {
let glancesList = try decoder.decode(GlancesList.self, from: data)
completion(glancesList.glances.map{self.toAppModel(glanceDetails: $0)}, nil)
}
catch {
completion(nil, InMobiShortSourceError.dataEncodingError(error.localizedDescription))
}
}
dataTask.resume()
}
}
private func getGlanceIds(completion: @escaping (_ ids:[Glance]?, _ error:InMobiShortSourceError?) -> Void) {
let urlString = "\(self.baseURL)/updates/deef?region=\(self.region)&sdkV=\(self.sdkV)"
guard let updateURL = URL(string: urlString) else {
assertionFailure("Should never happen. The URL should be correct.")
return
}
var request = URLRequest(url: updateURL)
request.httpMethod = "GET"
request.addValue(self.apiKey, forHTTPHeaderField: "X-Api-Key")
let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
guard error == nil else {
self.log.debug("Network response: error \(String(describing: error))")
completion(nil, .networkError(error))
return
}
guard let data = data else {
self.log.debug("Network response: error Invalid data")
completion(nil, .dataEncodingError("Invalid data"))
return
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let glancesDict = try decoder.decode([String:[Glance]].self, from: data)
completion(glancesDict["updates"], nil)
}
catch {
completion(nil, .dataEncodingError(error.localizedDescription))
}
}
dataTask.resume()
}
private func toAppModel(glanceDetails:GlanceDetails) -> ShortsItem {
ShortsItem(id: glanceDetails.id,
images: glanceDetails.image.supportedImages.map{ .init(width: $0.width, height: $0.height, url: $0.url) },
overlayImages: glanceDetails.overlayImage.supportedImages.map{ .init(width: $0.width, height: $0.height, url: $0.url) },
updatedAtInSecs: glanceDetails.updatedAtInSecs,
startsAtInSecs: glanceDetails.startsAtInSecs,
endsAtInSecs: glanceDetails.endsAtInSecs,
shareURL: glanceDetails.encodedShareURL,
title: glanceDetails.peekData.title,
summaryText: glanceDetails.peek.articlePeek.summary,
sourceName: glanceDetails.peekData.sourceName,
heartCount: glanceDetails.peekData.heartCount,
shortURL: glanceDetails.peekData.encodedShortURL,
ctaText: glanceDetails.peekData.ctaText,
ctaURL: glanceDetails.peek.articlePeek.cta.encodedUrl,
likeCount: glanceDetails.glanceInteractionDetails.likeCount,
shareCount: glanceDetails.glanceInteractionDetails.shareCount)
}
}
//
// InMobiShortsSource.h
// InMobiShortsSource
//
// Created by Dmitry Stepanets on 07.06.2021.
//
#import <Foundation/Foundation.h>
//! Project version number for InMobiShortsSource.
FOUNDATION_EXPORT double InMobiShortsSourceVersionNumber;
//! Project version string for InMobiShortsSource.
FOUNDATION_EXPORT const unsigned char InMobiShortsSourceVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <InMobiShortsSource/PublicHeader.h>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>
//
// Glance.swift
// InMobiShortsSource
//
// Created by Dmitry Stepanets on 07.06.2021.
//
import Foundation
enum GlanceUpdateType: String, Codable {
case ADD = "ADD"
case REMOVE = "REMOVE"
}
struct Glance: Codable {
let glanceId: String
let updatedAtInSecs: TimeInterval
let glanceType: String
let updateType: GlanceUpdateType
}
//
// GlanceDetails.swift
// InMobiShortsSource
//
// Created by Dmitry Stepanets on 07.06.2021.
//
import Foundation
struct GlanceInteractionDetails: Codable {
let likeCount: Int
let shareCount: Int
}
struct GlanceDetails: Codable {
let id: String
let image: GlanceImage
let overlayImage: GlanceOverlayImage
let updatedAtInSecs: TimeInterval
let startsAtInSecs: TimeInterval
let endsAtInSecs: TimeInterval
var encodedShareURL: URL? {
guard let urlStirng = shareUrl else {
return nil
}
return URL(string: urlStirng)
}
let peekData: PeekData
let peek: Peek
let glanceInteractionDetails: GlanceInteractionDetails
private let shareUrl: String?
}
//
// GlanceImage.swift
// InMobiShortsSource
//
// Created by Dmitry Stepanets on 07.06.2021.
//
import Foundation
struct GlanceImageFormat: Codable {
let width:Int
let height:Int
let url:URL
}
struct GlanceImage: Codable {
let id:String
let supportedImages:[GlanceImageFormat]
}
//
// GlanceOverlayImage.swift
// InMobiShortsSource
//
// Created by Dmitry Stepanets on 16.06.2021.
//
import Foundation
struct GlanceOverlayImage: Codable {
let id: String
var supportedImages: [GlanceImageFormat]
}
//
// GlancesList.swift
// InMobiShortsSource
//
// Created by Dmitry Stepanets on 06.08.2021.
//
import Foundation
struct GlancesList: Codable {
@LossyCodableList
var glances: [GlanceDetails]
}
//
// LossyCodableList.swift
// InMobiShortsSource
//
// Created by Dmitry Stepanets on 06.08.2021.
//
import Foundation
@propertyWrapper
struct LossyCodableList<Element> {
var elements: [Element]
var wrappedValue: [Element] {
get { elements }
set { elements = newValue }
}
}
extension LossyCodableList: Decodable where Element: Decodable {
private struct ElementWrapper: Decodable {
var element: Element?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
element = try? container.decode(Element.self)
}
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let wrappers = try container.decode([ElementWrapper].self)
elements = wrappers.compactMap(\.element)
}
}
extension LossyCodableList: Encodable where Element: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
for element in elements {
try? container.encode(element)
}
}
}
//
// Peek.swift
// InMobiShortsSource
//
// Created by Dmitry Stepanets on 07.06.2021.
//
import Foundation
struct Peek: Codable {
let articlePeek: ArticlePeek
}
struct ArticlePeek: Codable {
let summary: String
let cta: GlanceCTA
}
struct GlanceCTA: Codable {
var encodedUrl: URL? {
guard let urlString = url?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
return nil
}
return URL(string: urlString)
}
private let url: String?
let text: String
}
//
// PeekData.swift
// InMobiShortsSource
//
// Created by Dmitry Stepanets on 07.06.2021.
//
import Foundation
struct PeekData: Codable {
let title:String
let sourceName:String
let heartCount:Int
var encodedShortURL:URL? {
guard let urlString = shortUrl else {
return nil
}
return URL(string: urlString)
}
let ctaText:String
private let shortUrl:String?
}
//
// InMobiShortsSourceTests.swift
// InMobiShortsSourceTests
//
// Created by Dmitry Stepanets on 07.06.2021.
//
import XCTest
@testable import InMobiShortsSource
class InMobiShortsSourceTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() throws {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
...@@ -112,6 +112,15 @@ public enum AnalyticsEvent: String { ...@@ -112,6 +112,15 @@ public enum AnalyticsEvent: String {
case ANALYTICS_WIDGET_PRECIP_REMOVE = "IOS_PRECIP_FORECAST_WIDGET_REMOVED" case ANALYTICS_WIDGET_PRECIP_REMOVE = "IOS_PRECIP_FORECAST_WIDGET_REMOVED"
case ANALYTICS_WIDGET_WIND_REMOVE = "IOS_WIND_FORECAST_WIDGET_REMOVED" case ANALYTICS_WIDGET_WIND_REMOVE = "IOS_WIND_FORECAST_WIDGET_REMOVED"
case ANALYTICS_WIDGET_RADAR_REMOVE = "IOS_RADAR_WIDGET_REMOVED" case ANALYTICS_WIDGET_RADAR_REMOVE = "IOS_RADAR_WIDGET_REMOVED"
case ANALYTICS_SHORTS_SECTION_VIEW = "SHORTS_SECTION_VIEW"
case ANALYTICS_SHORTS_CARD_VIEW = "SHORTS_CARD_VIEW"
case ANALYTICS_SHORTS_CARD_CLICK = "SHORTS_CARD_CLICK"
case ANALYTICS_SHORTS_CARD_BINGE_VIEW = "SHORTS_CARD_BINGE_VIEW"
case ANALYTICS_SHORTS_VIEW_SHORTS = "VIEW_SHORTS"
case ANALYTICS_SHORTS_READ_MORE_CLICK = "READ_MORE_CLICK"
case ANALYTICS_SHORTS_LIKE_BUTTON_CLICK = "LIKE_BUTTON_CLICK"
case ANALYTICS_SHORTS_EXIT_SHORTS_VIEW = "EXIT_SHORTS_VIEW"
case ANALYTICS_SHORTS_NUDGE_VIEW = "NUDGE_VIEW"
/// FTUE Funnel: User has saved his first city after installing the app. /// FTUE Funnel: User has saved his first city after installing the app.
case ANALYTICS_USER_QUALIFIED = "USER_QUALIFIED" case ANALYTICS_USER_QUALIFIED = "USER_QUALIFIED"
......
...@@ -9,6 +9,7 @@ import Foundation ...@@ -9,6 +9,7 @@ import Foundation
public enum AnalyticsParameter: String { public enum AnalyticsParameter: String {
//TODO: rename to Swifty names. This is a legacy from the old app. //TODO: rename to Swifty names. This is a legacy from the old app.
case ANALYTICS_KEY_SOURCE = "source"
case ANALYTICS_KEY_CCPA_TODAY_DISMISS_ACTION = "ACTION" case ANALYTICS_KEY_CCPA_TODAY_DISMISS_ACTION = "ACTION"
case ANALYTICS_KEY_CCPA_TODAY_CCPA_EXPT_3_VERSION = "CCPA_EXPT_3_VERSION" case ANALYTICS_KEY_CCPA_TODAY_CCPA_EXPT_3_VERSION = "CCPA_EXPT_3_VERSION"
case ANALYTICS_KEY_LAST_SEEN_CITY_CITY_ID = "cityId" case ANALYTICS_KEY_LAST_SEEN_CITY_CITY_ID = "cityId"
...@@ -18,8 +19,12 @@ public enum AnalyticsParameter: String { ...@@ -18,8 +19,12 @@ public enum AnalyticsParameter: String {
case ANALYTICS_KEY_PLACEMENT_NAME = "placement_name" case ANALYTICS_KEY_PLACEMENT_NAME = "placement_name"
case ANALYTICS_KEY_AD_UNIT_ID = "AD_PLACEMENT_ID" case ANALYTICS_KEY_AD_UNIT_ID = "AD_PLACEMENT_ID"
case ANALYTICS_KEY_AD_ADAPTER = "AD_ADAPTER" case ANALYTICS_KEY_AD_ADAPTER = "AD_ADAPTER"
case ANALYTICS_KEY_PUSH_NOTIFICATION_SOURCE = "source"
case ANALYTICS_KEY_THEME_CHANGE_NAME = "themeName" case ANALYTICS_KEY_THEME_CHANGE_NAME = "themeName"
case ANALYTICS_KEY_FIST_OPEN_SOURCE = "Source" case ANALYTICS_KEY_FIST_OPEN_SOURCE = "Source"
case ANALYTICS_KEY_WIDGET_NAME = "widget_name" case ANALYTICS_KEY_WIDGET_NAME = "widget_name"
case ANALYTICS_KEY_SHORTS_CARD_POSITION = "position"
case ANALYTICS_KEY_SHORTS_CARD_ID = "card_id"
case ANALYTICS_KEY_SHORTS_TIME_SPENT = "time_spent"
case ANALYTICS_KEY_SHORTS_READ_MORE_VIEW = "view"
case ANALYTICS_KEY_SHORTS_NUDGE_VIEW_TYPE = "type"
} }
...@@ -10,7 +10,12 @@ import AppsFlyerLib ...@@ -10,7 +10,12 @@ import AppsFlyerLib
internal struct AppsFlyerAnalyticsService: AnalyticsService { internal struct AppsFlyerAnalyticsService: AnalyticsService {
public let name: String = "AppsFlyer" public let name: String = "AppsFlyer"
let eventsWhitelist: Set<AnalyticsEvent>? = [.ANALYTICS_APP_OPEN, .ANALYTICS_USER_QUALIFIED, .ANALYTICS_D3_RETAINED, .ANALYTICS_FIRST_OPEN, .ANALYTICS_ATT_ACCEPTED] let eventsWhitelist: Set<AnalyticsEvent>? = [.ANALYTICS_APP_OPEN,
.ANALYTICS_USER_QUALIFIED,
.ANALYTICS_D3_RETAINED,
.ANALYTICS_FIRST_OPEN,
.ANALYTICS_ATT_ACCEPTED,
.ANALYTICS_SHORTS_CARD_BINGE_VIEW]
let attributesWhitelist: Set<AnalyticsAttribute>? = [] // block all let attributesWhitelist: Set<AnalyticsAttribute>? = [] // block all
func log(event: AnalyticsEvent, params: [AnalyticsParameter : Any]?) { func log(event: AnalyticsEvent, params: [AnalyticsParameter : Any]?) {
......
//
// UIColor+Highlight.swift
// 1Weather
//
// Created by Dmitry Stepanets on 28.11.2020.
//
import UIKit
public extension UIColor {
private var isDarkColor: Bool {
var r, g, b, a: CGFloat
(r, g, b, a) = (0, 0, 0, 0)
self.getRed(&r, green: &g, blue: &b, alpha: &a)
let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b
return lum < 0.60 ? true : false
}
var highlighted:UIColor {
return self.isDarkColor ? self.adjust(by: 30) : self.adjust(by: -30)
}
func lighten(by fraction:CGFloat) -> UIColor {
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
var alpha: CGFloat = 0
getRed(&red, green: &green, blue: &blue, alpha: &alpha)
red = lightenColor(color: red, fraction: fraction)
green = lightenColor(color: green, fraction: fraction)
blue = lightenColor(color: blue, fraction: fraction)
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
func darken(by fraction:CGFloat) -> UIColor {
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
var alpha: CGFloat = 0
getRed(&red, green: &green, blue: &blue, alpha: &alpha)
red = darkenColor(color: red, fraction: fraction)
green = darkenColor(color: green, fraction: fraction)
blue = darkenColor(color: blue, fraction: fraction)
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
private func lightenColor(color: CGFloat, fraction: CGFloat) -> CGFloat {
return min(color + (1 - color) * fraction, 1)
}
private func darkenColor(color: CGFloat, fraction: CGFloat) -> CGFloat {
return max(color - color * fraction, 0)
}
private func adjust(by percentage:CGFloat=30.0) -> UIColor {
var r:CGFloat=0, g:CGFloat=0, b:CGFloat=0, a:CGFloat=0;
if(self.getRed(&r, green: &g, blue: &b, alpha: &a)){
return UIColor(red: min(r + percentage/100, 1.0),
green: min(g + percentage/100, 1.0),
blue: min(b + percentage/100, 1.0),
alpha: a)
}
else{
return self
}
}
}
...@@ -102,6 +102,15 @@ internal class DeviceLocationMonitor: NSObject { ...@@ -102,6 +102,15 @@ internal class DeviceLocationMonitor: NSObject {
public let timeZoneIdentifier: String? = nil public let timeZoneIdentifier: String? = nil
} }
public override init() {
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(handleReturnFromBackground), name: UIApplication.didBecomeActiveNotification, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
public typealias CurrentLocationCompletion = (LocationRequestResult) -> () public typealias CurrentLocationCompletion = (LocationRequestResult) -> ()
public func useCurrentLocation(requestedFrom viewController: UIViewController, completion: @escaping CurrentLocationCompletion) { public func useCurrentLocation(requestedFrom viewController: UIViewController, completion: @escaping CurrentLocationCompletion) {
...@@ -178,6 +187,18 @@ internal class DeviceLocationMonitor: NSObject { ...@@ -178,6 +187,18 @@ internal class DeviceLocationMonitor: NSObject {
viewController.present(alertGoToSettings, animated: true, completion: nil) viewController.present(alertGoToSettings, animated: true, completion: nil)
} }
@objc
private func handleReturnFromBackground() {
// If the device is locked while the location permission dialog is shown, it disappears, and the app may end up in an unexpected state, waiting for a callback from DeviceLocationMonitor.
// So, if we were showing a permission dialog when the device was sent to background, we need to show it again when the app becomes active.
// This variable is not nil, if a permission dialog is shown.
if authorizationStatusChangeHandler != nil {
if CLLocationManager.authorizationStatus() == .notDetermined {
locationManager.requestWhenInUseAuthorization()
}
}
}
} }
// MARK: - CLLocationManagerDelegate // MARK: - CLLocationManagerDelegate
......
...@@ -161,6 +161,8 @@ public class LocationManager { ...@@ -161,6 +161,8 @@ public class LocationManager {
self.selectedLocationIndex = nil self.selectedLocationIndex = nil
return return
} }
if let index = locations.firstIndex(of: location) { if let index = locations.firstIndex(of: location) {
self.selectedLocationIndex = index self.selectedLocationIndex = index
locations[index] = location locations[index] = location
......
//
// ShortsItem.swift
// OneWeatherCore
//
// Created by Dmitry Stepanets on 08.06.2021.
//
import UIKit
public struct ShortsItemImage {
public let width:Int
public let height:Int
public let url:URL
public init(width: Int, height: Int, url: URL) {
self.width = width
self.height = height
self.url = url
}
}
public struct ShortsItem {
public let id: String
public let images: [ShortsItemImage]
public let overlayImages: [ShortsItemImage]
public let updatedAtInSecs: TimeInterval
public let startsAtInSecs: TimeInterval
public let endsAtInSecs: TimeInterval
public let shareURL: URL?
public let title: String
public let summaryText: String
public let sourceName: String
public let heartCount: Int
public let shortURL: URL?
public let ctaText: String
public let ctaURL: URL?
public let likeCount: Int
public let shareCount: Int
public var isLiked: Bool = false
public var viewDate: Date?
public init(id: String, images: [ShortsItemImage], overlayImages: [ShortsItemImage], updatedAtInSecs: TimeInterval, startsAtInSecs: TimeInterval, endsAtInSecs: TimeInterval, shareURL: URL?, title: String, summaryText: String, sourceName: String, heartCount: Int, shortURL: URL?, ctaText: String, ctaURL: URL?, likeCount: Int, shareCount: Int) {
self.id = id
self.images = images
self.overlayImages = overlayImages
self.updatedAtInSecs = updatedAtInSecs
self.startsAtInSecs = startsAtInSecs
self.endsAtInSecs = endsAtInSecs
self.shareURL = shareURL
self.title = title
self.summaryText = summaryText
self.sourceName = sourceName
self.heartCount = heartCount
self.shortURL = shortURL
self.ctaText = ctaText
self.ctaURL = ctaURL
self.likeCount = likeCount
self.shareCount = shareCount
}
public func getImage(for width:CGFloat) -> ShortsItemImage? {
guard !self.images.isEmpty && width > 0 else {
return nil
}
var image = images.first
for itemImage in images {
if CGFloat(itemImage.width) / UIScreen.main.scale >= width {
image = itemImage
break
}
}
return image
}
public func getOverlayImage(for width:CGFloat) -> ShortsItemImage? {
guard !self.overlayImages.isEmpty && width > 0 else {
return nil
}
var image = overlayImages.first
for itemImage in overlayImages {
if CGFloat(itemImage.width) / UIScreen.main.scale >= width {
image = itemImage
break
}
}
return image
}
public mutating func like() {
isLiked = true
}
public mutating func dislike() {
isLiked = false
}
public mutating func markAsViewed() {
viewDate = Date()
}
}
...@@ -39,7 +39,7 @@ public struct CurrentWeather: Equatable, Hashable { ...@@ -39,7 +39,7 @@ public struct CurrentWeather: Equatable, Hashable {
public var moonState: CelestialState? = .normal public var moonState: CelestialState? = .normal
public var moonPhase: MoonPhase? = .unknown public var moonPhase: MoonPhase? = .unknown
public init(lastTimeUpdated: Date, date: Date, timeZone: TimeZone, weekDay: WeekDay, type: WeatherType = .unknown, isDay: Bool, uv: Int? = nil, minTemp: Temperature? = nil, maxTemp: Temperature? = nil, windSpeed: WindSpeed? = nil, windDirection: WindDirection? = nil, precipitationProbability: Percent? = nil, temp: Temperature? = nil, dewPoint: Temperature? = nil, apparentTemp: Temperature? = nil, humidity: Percent? = nil, visibility: Visibility? = nil, pressure: Pressure? = nil, sunrise: Date? = nil, sunset: Date? = nil, sunState: CelestialState? = .normal, moonrise: Date? = nil, moonset: Date? = nil, approximateMoonrise: Date? = nil, moonState: CelestialState? = .normal, moonPhase: MoonPhase? = .unknown) { public init(lastTimeUpdated: Date, date: Date, timeZone: TimeZone, weekDay: WeekDay, type: WeatherType = .unknown, isDay: Bool, uv: Int? = nil, minTemp: Temperature? = nil, maxTemp: Temperature? = nil, windSpeed: WindSpeed? = nil, windDirection: WindDirection? = nil, precipitationProbability: Percent? = nil, temp: Temperature? = nil, dewPoint: Temperature? = nil, apparentTemp: Temperature? = nil, humidity: Percent? = nil, visibility: Visibility? = nil, pressure: Pressure? = nil, sunrise: Date? = nil, sunset: Date? = nil, sunState: CelestialState? = .normal, moonrise: Date? = nil, moonset: Date? = nil, approximateMoonrise: Date? = nil, moonState: CelestialState? = .normal, moonPhase: MoonPhase? = nil) {
self.lastTimeUpdated = lastTimeUpdated self.lastTimeUpdated = lastTimeUpdated
self.date = date self.date = date
self.timeZone = timeZone self.timeZone = timeZone
......
...@@ -98,6 +98,9 @@ public class Settings { ...@@ -98,6 +98,9 @@ public class Settings {
@UserDefaultsOptionalValue("widgetPromotionTriggerCount") @UserDefaultsOptionalValue("widgetPromotionTriggerCount")
public var widgetPromotionTriggerCount: Int? public var widgetPromotionTriggerCount: Int?
@UserDefaultsBasicValue(key: "shorts_showed_swipeUp_count")
public var shortsSwipeUpNudgeShowedCount = 0
#warning("Not implemented!") #warning("Not implemented!")
//TODO: implement store in UserDefaults and configure via UI in debug builds. //TODO: implement store in UserDefaults and configure via UI in debug builds.
public var adLogging: Bool = true public var adLogging: Bool = true
......
//
// ShortsSource.swift
// OneWeatherCore
//
// Created by Dmitry Stepanets on 08.06.2021.
//
import Foundation
public typealias ShortsSourceCompletion = ([ShortsItem]?, Error?) -> ()
public protocol ShortSource {
func updateShorts(completion: @escaping ShortsSourceCompletion)
}
...@@ -13,10 +13,10 @@ extension OneWeatherUI { ...@@ -13,10 +13,10 @@ extension OneWeatherUI {
static let frameworkBundle = Bundle(for: OneWeatherUIClass.self) static let frameworkBundle = Bundle(for: OneWeatherUIClass.self)
static let loadFonts: () = { static let loadFonts: () = {
// loadFontWith(name: "SF-Pro-Display-Regular", fileExtension: "otf") loadFontWith(name: "SF-Pro-Display-Regular", fileExtension: "otf")
// loadFontWith(name: "SF-Pro-Display-Light", fileExtension: "otf") loadFontWith(name: "SF-Pro-Display-Light", fileExtension: "otf")
loadFontWith(name: "SF-Pro-Display-Bold", fileExtension: "otf") loadFontWith(name: "SF-Pro-Display-Bold", fileExtension: "otf")
// loadFontWith(name: "SF-Pro-Display-Thin", fileExtension: "otf") loadFontWith(name: "SF-Pro-Display-Thin", fileExtension: "otf")
}() }()
internal static func loadFontWith(name: String, fileExtension: String) { internal static func loadFontWith(name: String, fileExtension: String) {
......
...@@ -30,7 +30,7 @@ end ...@@ -30,7 +30,7 @@ end
#Analytics #Analytics
def analytics_pods def analytics_pods
pod 'MoEngage-iOS-SDK' pod 'MoEngage-iOS-SDK'
pod 'AppsFlyerFramework' pod 'AppsFlyerFramework', '~> 6.3.2'
pod 'Flurry-iOS-SDK/FlurrySDK' pod 'Flurry-iOS-SDK/FlurrySDK'
# Recommended: Add the Firebase pod for Google Analytics # Recommended: Add the Firebase pod for Google Analytics
pod 'Firebase/Analytics' pod 'Firebase/Analytics'
...@@ -44,6 +44,9 @@ def application_pods ...@@ -44,6 +44,9 @@ def application_pods
pod 'Cirque', :git => 'https://github.com/StepanetsDmtry/Cirque.git' pod 'Cirque', :git => 'https://github.com/StepanetsDmtry/Cirque.git'
pod 'AlgoliaSearchClient', '~> 8.2' pod 'AlgoliaSearchClient', '~> 8.2'
pod 'PKHUD', '~> 5.0' pod 'PKHUD', '~> 5.0'
pod 'Nuke'
pod 'Nuke-WebP-Plugin'
pod 'pop', :git => 'https://github.com/facebook/pop.git'
end end
#-------Targets------- #-------Targets-------
......
...@@ -145,6 +145,15 @@ PODS: ...@@ -145,6 +145,15 @@ PODS:
- GoogleUtilities/MethodSwizzler - GoogleUtilities/MethodSwizzler
- GoogleUtilities/UserDefaults (7.5.0): - GoogleUtilities/UserDefaults (7.5.0):
- GoogleUtilities/Logger - GoogleUtilities/Logger
- libwebp (1.1.0):
- libwebp/demux (= 1.1.0)
- libwebp/mux (= 1.1.0)
- libwebp/webp (= 1.1.0)
- libwebp/demux (1.1.0):
- libwebp/webp
- libwebp/mux (1.1.0):
- libwebp/demux
- libwebp/webp (1.1.0)
- Localize-Swift (3.2.0): - Localize-Swift (3.2.0):
- Localize-Swift/LocalizeSwiftCore (= 3.2.0) - Localize-Swift/LocalizeSwiftCore (= 3.2.0)
- Localize-Swift/UIKit (= 3.2.0) - Localize-Swift/UIKit (= 3.2.0)
...@@ -169,7 +178,12 @@ PODS: ...@@ -169,7 +178,12 @@ PODS:
- nanopb/encode (= 2.30908.0) - nanopb/encode (= 2.30908.0)
- nanopb/decode (2.30908.0) - nanopb/decode (2.30908.0)
- nanopb/encode (2.30908.0) - nanopb/encode (2.30908.0)
- Nuke (9.5.0)
- Nuke-WebP-Plugin (5.0.0):
- libwebp (= 1.1.0)
- Nuke (~> 9.0)
- PKHUD (5.3.0) - PKHUD (5.3.0)
- pop (1.0.11)
- PromisesObjC (2.0.0) - PromisesObjC (2.0.0)
- SnapKit (5.0.1) - SnapKit (5.0.1)
- Swarm (1.0.7) - Swarm (1.0.7)
...@@ -178,7 +192,7 @@ PODS: ...@@ -178,7 +192,7 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- AlgoliaSearchClient (~> 8.2) - AlgoliaSearchClient (~> 8.2)
- AmazonPublisherServicesSDK - AmazonPublisherServicesSDK
- AppsFlyerFramework - AppsFlyerFramework (~> 6.3.2)
- BezierKit - BezierKit
- Cirque (from `https://github.com/StepanetsDmtry/Cirque.git`) - Cirque (from `https://github.com/StepanetsDmtry/Cirque.git`)
- Firebase/Analytics - Firebase/Analytics
...@@ -194,7 +208,10 @@ DEPENDENCIES: ...@@ -194,7 +208,10 @@ DEPENDENCIES:
- lottie-ios - lottie-ios
- MoEngage-iOS-SDK - MoEngage-iOS-SDK
- MORichNotification - MORichNotification
- Nuke
- Nuke-WebP-Plugin
- PKHUD (~> 5.0) - PKHUD (~> 5.0)
- pop (from `https://github.com/facebook/pop.git`)
- SnapKit - SnapKit
- "Swarm (from `git@gitlab.pinsightmedia.com:oneweather/wdt-skywisetilekit-ios.git`, branch `develop`)" - "Swarm (from `git@gitlab.pinsightmedia.com:oneweather/wdt-skywisetilekit-ios.git`, branch `develop`)"
- XMLCoder (~> 0.12.0) - XMLCoder (~> 0.12.0)
...@@ -225,6 +242,7 @@ SPEC REPOS: ...@@ -225,6 +242,7 @@ SPEC REPOS:
- GoogleMobileAdsMediationMoPub - GoogleMobileAdsMediationMoPub
- GoogleUserMessagingPlatform - GoogleUserMessagingPlatform
- GoogleUtilities - GoogleUtilities
- libwebp
- Localize-Swift - Localize-Swift
- Logging - Logging
- lottie-ios - lottie-ios
...@@ -232,6 +250,8 @@ SPEC REPOS: ...@@ -232,6 +250,8 @@ SPEC REPOS:
- mopub-ios-sdk - mopub-ios-sdk
- MORichNotification - MORichNotification
- nanopb - nanopb
- Nuke
- Nuke-WebP-Plugin
- PKHUD - PKHUD
- PromisesObjC - PromisesObjC
- SnapKit - SnapKit
...@@ -240,6 +260,8 @@ SPEC REPOS: ...@@ -240,6 +260,8 @@ SPEC REPOS:
EXTERNAL SOURCES: EXTERNAL SOURCES:
Cirque: Cirque:
:git: https://github.com/StepanetsDmtry/Cirque.git :git: https://github.com/StepanetsDmtry/Cirque.git
pop:
:git: https://github.com/facebook/pop.git
Swarm: Swarm:
:branch: develop :branch: develop
:git: "git@gitlab.pinsightmedia.com:oneweather/wdt-skywisetilekit-ios.git" :git: "git@gitlab.pinsightmedia.com:oneweather/wdt-skywisetilekit-ios.git"
...@@ -248,6 +270,9 @@ CHECKOUT OPTIONS: ...@@ -248,6 +270,9 @@ CHECKOUT OPTIONS:
Cirque: Cirque:
:commit: ceb7ba910a35973cbcd41c73a62be6305aed4d13 :commit: ceb7ba910a35973cbcd41c73a62be6305aed4d13
:git: https://github.com/StepanetsDmtry/Cirque.git :git: https://github.com/StepanetsDmtry/Cirque.git
pop:
:commit: 87d1f8b74cdaa4699d7a9f6be1ff5202014c581f
:git: https://github.com/facebook/pop.git
Swarm: Swarm:
:commit: 5bb474b6a838977167c19beaabfb6bc85e995135 :commit: 5bb474b6a838977167c19beaabfb6bc85e995135
:git: "git@gitlab.pinsightmedia.com:oneweather/wdt-skywisetilekit-ios.git" :git: "git@gitlab.pinsightmedia.com:oneweather/wdt-skywisetilekit-ios.git"
...@@ -278,6 +303,7 @@ SPEC CHECKSUMS: ...@@ -278,6 +303,7 @@ SPEC CHECKSUMS:
GoogleMobileAdsMediationMoPub: 5c17c4e8553e1aa46376e08ac3a9f3ccf8f62698 GoogleMobileAdsMediationMoPub: 5c17c4e8553e1aa46376e08ac3a9f3ccf8f62698
GoogleUserMessagingPlatform: ab890ce5f6620f293a21b6bdd82e416a2c73aeca GoogleUserMessagingPlatform: ab890ce5f6620f293a21b6bdd82e416a2c73aeca
GoogleUtilities: eea970f4a389963963bffe8d8fabe43540678b9c GoogleUtilities: eea970f4a389963963bffe8d8fabe43540678b9c
libwebp: 946cb3063cea9236285f7e9a8505d806d30e07f3
Localize-Swift: 6f4475136bdb0d7b2882ea3d4ea919d70142b232 Localize-Swift: 6f4475136bdb0d7b2882ea3d4ea919d70142b232
Logging: beeb016c9c80cf77042d62e83495816847ef108b Logging: beeb016c9c80cf77042d62e83495816847ef108b
lottie-ios: c058aeafa76daa4cf64d773554bccc8385d0150e lottie-ios: c058aeafa76daa4cf64d773554bccc8385d0150e
...@@ -285,12 +311,15 @@ SPEC CHECKSUMS: ...@@ -285,12 +311,15 @@ SPEC CHECKSUMS:
mopub-ios-sdk: 36d322902674b79b0560a1cb8d15af0548d364ea mopub-ios-sdk: 36d322902674b79b0560a1cb8d15af0548d364ea
MORichNotification: d4cef22d783e9bba2bc33225176d19a96ac5f1e8 MORichNotification: d4cef22d783e9bba2bc33225176d19a96ac5f1e8
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
Nuke: 6f400a4ea957e09149ec335a3c6acdcc814d89e4
Nuke-WebP-Plugin: a79a97be508453ce5c36b78989595cbbc19c2deb
PKHUD: 98f3e4bc904b9c916f1c5bb6d765365b5357291b PKHUD: 98f3e4bc904b9c916f1c5bb6d765365b5357291b
pop: ae3ae187018759968252242e175c21f7f9be5dd2
PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58 PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58
SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb
Swarm: 95393cd52715744c94e3a8475bc20b4de5d79f35 Swarm: 95393cd52715744c94e3a8475bc20b4de5d79f35
XMLCoder: f884dfa894a6f8b7dce465e4f6c02963bf17e028 XMLCoder: f884dfa894a6f8b7dce465e4f6c02963bf17e028
PODFILE CHECKSUM: 62fca2be289770da54f757489617473cd28ef5b6 PODFILE CHECKSUM: 6b610cb290828c2f023d33cbffc4c28fdf79c46b
COCOAPODS: 1.10.2 COCOAPODS: 1.10.2
...@@ -264,7 +264,7 @@ extension BezierCurve { ...@@ -264,7 +264,7 @@ extension BezierCurve {
} }
public let defaultIntersectionAccuracy = CGFloat(0.5) public let defaultIntersectionAccuracy = CGFloat(0.5)
public let reduceStepSize: CGFloat = 0.01 internal let reduceStepSize: CGFloat = 0.01
public func == (left: BezierCurve, right: BezierCurve) -> Bool { public func == (left: BezierCurve, right: BezierCurve) -> Bool {
return left.points == right.points return left.points == right.points
......
Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
You are hereby granted a non-exclusive, worldwide, royalty-free license to use,
copy, modify, and distribute this software in source code or binary form for use
in connection with the web services and APIs provided by Facebook.
As with any software that integrates with the Facebook platform, your use of
this software is subject to the Facebook Developer Principles and Policies
[http://developers.facebook.com/policy/]. This copyright notice shall be
included in all copies or substantial portions of the software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# Facebook SDK for iOS
[![Platforms](https://img.shields.io/cocoapods/p/FBSDKCoreKit.svg)](https://cocoapods.org/pods/FBSDKCoreKit)
[![circleci](https://circleci.com/gh/facebook/facebook-ios-sdk/tree/master.svg?style=shield)](https://circleci.com/gh/facebook/facebook-ios-sdk/tree/master)
[![CocoaPods](https://img.shields.io/cocoapods/v/FBSDKCoreKit.svg)](https://cocoapods.org/pods/FBSDKCoreKit)
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
This open-source library allows you to integrate Facebook into your iOS app.
Learn more about the provided samples, documentation, integrating the SDK into your app, accessing source code, and more
at https://developers.facebook.com/docs/ios
Please take a moment and [subscribe to releases](https://docs.github.com/en/enterprise/2.15/user/articles/watching-and-unwatching-repositories) so that you can be notified about new features, deprecations, and critical fixes. To see information about the latest release, consult our [changelog](CHANGELOG.md).
## TRY IT OUT
### Swift Package Manager (available Xcode 11.2 and forward)
1. In Xcode, select File > Swift Packages > Add Package Dependency.
2. Follow the prompts using the URL for this repository and a minimum semantic version of v5.10.0
3. Check-out the tutorials available online at: <https://developers.facebook.com/docs/ios/getting-started>
4. Start coding! Visit <https://developers.facebook.com/docs/ios> for tutorials and reference documentation.
**Note for Swift Package Manager Users:**
If you explicitly **DO NOT** want to include Swift, import `FBSDKCoreKit` `FBSDKLoginKit` and `FBSDKShareKit`
</br>For projects that include Swift, use `FacebookCore`, `FacebookLogin`, and `FacebookShare`
### CocoaPods
1. Add the following to your `Podfile`:
pod 'FBSDKCoreKit'
pod 'FBSDKLoginKit'
pod 'FBSDKShareKit'
2. Test your install by adding `import FBSDKCoreKit` to your `AppDelegate`
3. Check-out the tutorials available online at: <https://developers.facebook.com/docs/ios/getting-started>
4. Start coding! Visit <https://developers.facebook.com/docs/ios> for tutorials and reference documentation.
## iOS 14 CHANGES
### Data Disclosure
Due to the release of iOS 14, tracking events that your app collects and sends to Facebook may require you to disclosed these data types in the App Store Connect questionnaire. It is your responsibility to ensure this is reflected in your application’s privacy policy. Visit our blogpost for information on affected Facebook SDKs, APIs, and products and the Apple App Store Privacy Details article to learn more about the data types you will need to disclose.
link to FB blogpost https://developers.facebook.com/blog/post/2020/10/22/preparing-for-apple-app-store-data-disclosure-requirements/
apple store details https://developer.apple.com/app-store/app-privacy-details/
## FEATURES
- Login - <https://developers.facebook.com/docs/facebook-login>
- Sharing - <https://developers.facebook.com/docs/sharing>
- App Links - <https://developers.facebook.com/docs/applinks>
- Graph API - <https://developers.facebook.com/docs/ios/graph>
- Analytics - <https://developers.facebook.com/docs/analytics>
## GIVE FEEDBACK
Please report bugs or issues to our designated developer support team -- <https://developers.facebook.com/support/bugs/> -- as this will help us resolve them more quickly.
You can also visit our [Facebook Developer Community Forum](https://developers.facebook.com/community/),
join the [Facebook Developers Group on Facebook](https://www.facebook.com/groups/fbdevelopers/),
ask questions on [Stack Overflow](http://facebook.stackoverflow.com),
or open an issue in this repository.
## LICENSE
See the [LICENSE](LICENSE) file.
## SECURITY POLICY
See the [SECURITY POLICY](SECURITY.md) for more info on our bug bounty program.
## DEVELOPER TERMS
- By enabling Facebook integrations, including through this SDK, you can share information with Facebook, including
information about people’s use of your app. Facebook will use information received in accordance with our
[Data Use Policy](https://www.facebook.com/about/privacy/), including to provide you with insights about the
effectiveness of your ads and the use of your app. These integrations also enable us and our partners to serve ads on
and off Facebook.
- You may limit your sharing of information with us by updating the Insights control in the developer tool
`https://developers.facebook.com/apps/{app_id}/settings/advanced`.
- If you use a Facebook integration, including to share information with us, you agree and confirm that you have
provided appropriate and sufficiently prominent notice to and obtained the appropriate consent from your users
regarding such collection, use, and disclosure (including, at a minimum, through your privacy policy). You further
agree that you will not share information with us about children under the age of 13.
- You agree to comply with all applicable laws and regulations and also agree to our Terms
<https://www.facebook.com/policies/>, including our Platform Policies <https://developers.facebook.com/policy/>.and
Advertising Guidelines, as applicable <https://www.facebook.com/ad_guidelines.php>.
By using the Facebook SDK for iOS you agree to these terms.
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
//
// You are hereby granted a non-exclusive, worldwide, royalty-free license to use,
// copy, modify, and distribute this software in source code or binary form for use
// in connection with the web services and APIs provided by Facebook.
//
// As with any software that integrates with the Facebook platform, your use of
// this software is subject to the Facebook Developer Principles and Policies
// [http://developers.facebook.com/policy/]. This copyright notice shall be
// included in all copies or substantial portions of the software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#import "FBSDKJSONValue.h"
#import <Foundation/Foundation.h>
#import "FBSDKBasicUtility.h"
#import "FBSDKSafeCast.h"
#import "FBSDKTypeUtility.h"
@interface FBSDKJSONField ()
- (instancetype)initWithPotentialJSONField:(id)obj;
@end
static NSArray<FBSDKJSONField *> *createArray(id obj)
{
NSArray *const original = FBSDK_CAST_TO_CLASS_OR_NIL(obj, NSArray);
if (!original) {
return @[];
}
NSMutableArray<FBSDKJSONField *> *const fields =
[[NSMutableArray alloc] initWithCapacity:original.count];
for (id field in original) {
FBSDKJSONField *const f = [[FBSDKJSONField alloc] initWithPotentialJSONField:field];
if (f) {
[fields addObject:f];
}
}
return fields;
}
static NSDictionary<NSString *, FBSDKJSONField *> *createDictionary(id obj)
{
NSDictionary *const original = FBSDK_CAST_TO_CLASS_OR_NIL(obj, NSDictionary);
if (!original) {
return @{};
}
NSMutableDictionary<NSString *, FBSDKJSONField *> *const fields =
[[NSMutableDictionary alloc] initWithCapacity:original.count];
for (id key in original) {
// This is just a sanity check. Apple should only give us string keys
// anyway.
if (![key respondsToSelector:@selector(isKindOfClass:)] || ![key isKindOfClass:NSString.class]) {
continue;
}
NSString *const stringKey = (NSString *)key;
FBSDKJSONField *const typedField = [[FBSDKJSONField alloc] initWithPotentialJSONField:original[key]];
if (typedField) {
fields[stringKey] = typedField;
}
}
return fields;
}
@implementation FBSDKJSONValue
- (instancetype)initWithPotentialJSONObject:(id)obj
{
// If this isn't a real JSON object, dump it.
if (![FBSDKTypeUtility isValidJSONObject:obj]) {
return nil;
}
_rawObject = obj;
return self;
}
- (void)matchArray:(void (^)(NSArray<FBSDKJSONField *> *))arrayMatcher
dictionary:(void (^)(NSDictionary<NSString *, FBSDKJSONField *> *))dictMatcher
{
if (arrayMatcher && [_rawObject isKindOfClass:[NSArray class]]) {
arrayMatcher(createArray(_rawObject));
} else if (dictMatcher && [_rawObject isKindOfClass:[NSDictionary class]]) {
dictMatcher(createDictionary(_rawObject));
}
}
- (NSDictionary<NSString *, FBSDKJSONField *> *_Nullable)matchDictionaryOrNil
{
__block NSDictionary<NSString *, FBSDKJSONField *> *result = nil;
[self matchArray:nil dictionary:^(NSDictionary<NSString *, FBSDKJSONField *> *_Nonnull value) {
result = value;
}];
return result;
}
- (NSDictionary<NSString *, id> *_Nullable)unsafe_matchDictionaryOrNil
{
return [_rawObject isKindOfClass:NSDictionary.class] ? _rawObject : nil;
}
- (NSArray<FBSDKJSONField *> *_Nullable)matchArrayOrNil
{
__block NSArray<FBSDKJSONField *> *result = nil;
[self matchArray:^(NSArray<FBSDKJSONField *> *_Nonnull value) {
result = value;
} dictionary:nil];
return result;
}
- (NSArray *_Nullable)unsafe_matchArrayOrNil
{
__block BOOL isArray = NO;
[self matchArray:^(NSArray<FBSDKJSONField *> *_Nonnull _) {
isArray = YES;
} dictionary:nil];
return [_rawObject isKindOfClass:NSArray.class] ? _rawObject : nil;
}
@end
@implementation FBSDKJSONField
- (instancetype)initWithPotentialJSONField:(id)obj
{
// If this is nil, don't wrap it.
if (obj == nil) {
return nil;
}
// Per Apple's Docs, these are the only types FBSDKTypeUtility can return.
if (
![obj isKindOfClass:NSString.class]
&& ![obj isKindOfClass:NSNumber.class]
&& ![obj isKindOfClass:NSNull.class]
&& ![obj isKindOfClass:NSDictionary.class]
&& ![obj isKindOfClass:NSArray.class]) {
return nil;
}
if (self = [super init]) {
_rawObject = obj;
}
return self;
}
- (void)matchArray:(void (^)(NSArray<FBSDKJSONField *> *))arrayMatcher
dictionary:(void (^)(NSDictionary<NSString *, FBSDKJSONField *> *_Nonnull))dictionaryMatcher
string:(void (^)(NSString *_Nonnull))stringMatcher
number:(void (^)(NSNumber *_Nonnull))numberMatcher
null:(void (^)(void))nullMatcher
{
if (nullMatcher && [_rawObject isKindOfClass:NSNull.class]) {
nullMatcher();
} else if (numberMatcher && [_rawObject isKindOfClass:NSNumber.class]) {
numberMatcher(_rawObject);
} else if (stringMatcher && [_rawObject isKindOfClass:NSString.class]) {
stringMatcher(_rawObject);
} else if (arrayMatcher && [_rawObject isKindOfClass:NSArray.class]) {
arrayMatcher(createArray(_rawObject));
} else if (dictionaryMatcher && [_rawObject isKindOfClass:NSDictionary.class]) {
dictionaryMatcher(createDictionary(_rawObject));
}
}
- (NSArray<FBSDKJSONField *> *_Nullable)arrayOrNil
{
__block NSArray<FBSDKJSONField *> *result = nil;
[self matchArray:^(NSArray<FBSDKJSONField *> *_Nonnull a) {
result = [a copy];
} dictionary:nil string:nil number:nil null:nil];
return result;
}
- (NSDictionary<NSString *, FBSDKJSONField *> *_Nullable)dictionaryOrNil
{
__block NSDictionary<NSString *, FBSDKJSONField *> *result = nil;
[self matchArray:nil dictionary:^(NSDictionary<NSString *, FBSDKJSONField *> *_Nonnull d) {
result = [d copy];
} string:nil number:nil null:nil];
return result;
}
- (NSString *_Nullable)stringOrNil
{
__block NSString *result = nil;
[self matchArray:nil dictionary:nil string:^(NSString *_Nonnull s) {
result = [s copy];
} number:nil null:nil];
return result;
}
- (NSNumber *_Nullable)numberOrNil
{
__block NSNumber *result = nil;
[self matchArray:nil dictionary:nil string:nil number:^(NSNumber *_Nonnull n) {
result = n;
} null:nil];
return result;
}
- (NSNull *_Nullable)nullOrNil
{
__block NSNull *result = nil;
[self matchArray:nil dictionary:nil string:nil number:nil null:^{
result = [NSNull null];
}];
return result;
}
@end
FBSDKJSONValue *_Nullable FBSDKCreateJSONFromString(NSString *_Nullable string, NSError *__autoreleasing *errorRef)
{
return
[[FBSDKJSONValue alloc] initWithPotentialJSONObject:[FBSDKBasicUtility objectForJSONString:string error:errorRef]];
}
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
//
// You are hereby granted a non-exclusive, worldwide, royalty-free license to use,
// copy, modify, and distribute this software in source code or binary form for use
// in connection with the web services and APIs provided by Facebook.
//
// As with any software that integrates with the Facebook platform, your use of
// this software is subject to the Facebook Developer Principles and Policies
// [http://developers.facebook.com/policy/]. This copyright notice shall be
// included in all copies or substantial portions of the software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#import "FBSDKLibAnalyzer.h"
#import <objc/runtime.h>
#import "FBSDKTypeUtility.h"
@implementation FBSDKLibAnalyzer
static NSMutableDictionary<NSString *, NSString *> *_methodMapping;
+ (void)initialize
{
_methodMapping = [NSMutableDictionary dictionary];
}
+ (NSDictionary<NSString *, NSString *> *)getMethodsTable:(NSArray<NSString *> *)prefixes
frameworks:(NSArray<NSString *> *)frameworks
{
NSArray<NSString *> *allClasses = [self _getClassNames:prefixes frameworks:frameworks];
for (NSString *className in allClasses) {
Class class = NSClassFromString(className);
if (class) {
[self _addClass:class isClassMethod:NO];
[self _addClass:object_getClass(class) isClassMethod:YES];
}
}
@synchronized(_methodMapping) {
return [_methodMapping copy];
}
}
+ (nullable NSArray<NSString *> *)symbolicateCallstack:(NSArray<NSString *> *)callstack
methodMapping:(NSDictionary<NSString *, id> *)methodMapping
{
if (!callstack || !methodMapping) {
return nil;
}
NSArray<NSString *> *sortedAllAddress = [methodMapping.allKeys sortedArrayUsingComparator:^NSComparisonResult (id _Nonnull obj1, id _Nonnull obj2) {
return [obj1 compare:obj2];
}];
BOOL containsFBSDKFunction = NO;
NSInteger nonSDKMethodCount = 0;
NSMutableArray<NSString *> *symbolicatedCallstack = [NSMutableArray array];
for (NSUInteger i = 0; i < callstack.count; i++) {
NSString *rawAddress = [self _getAddress:[FBSDKTypeUtility array:callstack objectAtIndex:i]];
if (rawAddress.length < 10) {
continue;
}
NSString *addressString = [NSString stringWithFormat:@"0x%@", [rawAddress substringWithRange:NSMakeRange(rawAddress.length - 10, 10)]];
NSString *methodAddress = [self _searchMethod:addressString sortedAllAddress:sortedAllAddress];
if (methodAddress) {
containsFBSDKFunction = YES;
nonSDKMethodCount == 0 ?: [FBSDKTypeUtility array:symbolicatedCallstack addObject:[NSString stringWithFormat:@"(%ld DEV METHODS)", (long)nonSDKMethodCount]];
nonSDKMethodCount = 0;
NSString *methodName = [FBSDKTypeUtility dictionary:methodMapping objectForKey:methodAddress ofType:NSObject.class];
// filter out cxx_destruct
if ([methodName containsString:@".cxx_destruct"]) {
return nil;
}
[FBSDKTypeUtility array:symbolicatedCallstack addObject:[NSString stringWithFormat:@"%@%@", methodName, [self _getOffset:addressString secondString:methodAddress]]];
} else {
nonSDKMethodCount++;
}
}
nonSDKMethodCount == 0 ?: [FBSDKTypeUtility array:symbolicatedCallstack addObject:[NSString stringWithFormat:@"(%ld DEV METHODS)", (long)nonSDKMethodCount]];
return containsFBSDKFunction ? symbolicatedCallstack : nil;
}
#pragma mark - Private Methods
+ (NSArray<NSString *> *)_getClassNames:(NSArray<NSString *> *)prefixes
frameworks:(NSArray<NSString *> *)frameworks
{
NSMutableArray<NSString *> *classNames = [NSMutableArray new];
// from main bundle
[classNames addObjectsFromArray:[self _getClassesFrom:[[NSBundle mainBundle] executablePath]
prefixes:prefixes]];
// from dynamic libraries
if (frameworks.count > 0) {
unsigned int count = 0;
const char **images = objc_copyImageNames(&count);
for (int i = 0; i < count; i++) {
NSString *image = [NSString stringWithUTF8String:images[i]];
for (NSString *framework in frameworks) {
if ([image containsString:framework]) {
[classNames addObjectsFromArray:[self _getClassesFrom:image
prefixes:nil]];
}
}
}
free(images);
}
return [classNames copy];
}
+ (NSArray<NSString *> *)_getClassesFrom:(NSString *)image
prefixes:(NSArray<NSString *> *)prefixes
{
NSMutableArray<NSString *> *classNames = [NSMutableArray array];
unsigned int count = 0;
const char **classes = objc_copyClassNamesForImage([image UTF8String], &count);
for (unsigned int i = 0; i < count; i++) {
NSString *className = [NSString stringWithUTF8String:classes[i]];
if (prefixes.count > 0) {
for (NSString *prefix in prefixes) {
if ([className hasPrefix:prefix]) {
[FBSDKTypeUtility array:classNames addObject:className];
break;
}
}
} else {
[FBSDKTypeUtility array:classNames addObject:className];
}
}
free(classes);
return [classNames copy];
}
+ (void)_addClass:(Class)class
isClassMethod:(BOOL)isClassMethod
{
unsigned int methodsCount = 0;
Method *methods = class_copyMethodList(class, &methodsCount);
NSString *methodType = isClassMethod ? @"+" : @"-";
for (unsigned int i = 0; i < methodsCount; i++) {
Method method = methods[i];
if (method) {
SEL selector = method_getName(method);
IMP methodImplementation = class_getMethodImplementation(class, selector);
NSString *methodAddress = [NSString stringWithFormat:@"0x%010lx", (unsigned long)methodImplementation];
NSString *methodName = [NSString stringWithFormat:@"%@[%@ %@]",
methodType,
NSStringFromClass(class),
NSStringFromSelector(selector)];
if (methodAddress && methodName) {
@synchronized(_methodMapping) {
[FBSDKTypeUtility dictionary:_methodMapping setObject:methodName forKey:methodAddress];
}
}
}
}
free(methods);
}
+ (nullable NSString *)_getAddress:(nullable NSString *)callstackEntry
{
if ([callstackEntry isKindOfClass:[NSString class]]) {
NSArray<NSString *> *components = [callstackEntry componentsSeparatedByString:@" "];
for (NSString *component in components) {
if ([component containsString:@"0x"]) {
return component;
}
}
}
return nil;
}
+ (NSString *)_getOffset:(NSString *)firstString
secondString:(NSString *)secondString
{
unsigned long long first = 0, second = 0;
NSScanner *scanner = [NSScanner scannerWithString:firstString];
[scanner scanHexLongLong:&first];
scanner = [NSScanner scannerWithString:secondString];
[scanner scanHexLongLong:&second];
unsigned long long difference = first - second;
return [NSString stringWithFormat:@"+%llu", difference];
}
+ (nullable NSString *)_searchMethod:(NSString *)address
sortedAllAddress:(NSArray<NSString *> *)sortedAllAddress
{
if (0 == sortedAllAddress.count) {
return nil;
}
NSString *lowestAddress = [FBSDKTypeUtility array:sortedAllAddress objectAtIndex:0];
NSString *highestAddress = [FBSDKTypeUtility array:sortedAllAddress objectAtIndex:sortedAllAddress.count - 1];
if ([address compare:lowestAddress] == NSOrderedAscending || [address compare:highestAddress] == NSOrderedDescending) {
return nil;
}
if ([address compare:lowestAddress] == NSOrderedSame) {
return lowestAddress;
}
if ([address compare:highestAddress] == NSOrderedSame) {
return highestAddress;
}
NSUInteger index = [sortedAllAddress indexOfObject:address
inSortedRange:NSMakeRange(0, sortedAllAddress.count - 1)
options:NSBinarySearchingInsertionIndex
usingComparator:^NSComparisonResult (id _Nonnull obj1, id _Nonnull obj2) {
return [obj1 compare:obj2];
}];
return [FBSDKTypeUtility array:sortedAllAddress objectAtIndex:index - 1];
}
@end
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
//
// You are hereby granted a non-exclusive, worldwide, royalty-free license to use,
// copy, modify, and distribute this software in source code or binary form for use
// in connection with the web services and APIs provided by Facebook.
//
// As with any software that integrates with the Facebook platform, your use of
// this software is subject to the Facebook Developer Principles and Policies
// [http://developers.facebook.com/policy/]. This copyright notice shall be
// included in all copies or substantial portions of the software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#import "FBSDKSafeCast.h"
id _FBSDKCastToClassOrNilUnsafeInternal(id object, Class klass)
{
return [(NSObject *)object isKindOfClass:klass] ? object : nil;
}
id _FBSDKCastToProtocolOrNilUnsafeInternal(id object, Protocol *protocol)
{
return [(NSObject *)object conformsToProtocol:protocol] ? object : nil;
}
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
//
// You are hereby granted a non-exclusive, worldwide, royalty-free license to use,
// copy, modify, and distribute this software in source code or binary form for use
// in connection with the web services and APIs provided by Facebook.
//
// As with any software that integrates with the Facebook platform, your use of
// this software is subject to the Facebook Developer Principles and Policies
// [http://developers.facebook.com/policy/]. This copyright notice shall be
// included in all copies or substantial portions of the software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#import "FBSDKTypeUtility.h"
@implementation FBSDKTypeUtility
#pragma mark - Class Methods
+ (NSArray *)arrayValue:(id)object
{
return (NSArray *)[self _objectValue:object ofClass:[NSArray class]];
}
+ (nullable id)array:(NSArray *)array objectAtIndex:(NSUInteger)index
{
if ([self arrayValue:array] && index < array.count) {
return [array objectAtIndex:index];
}
return nil;
}
+ (void)array:(NSMutableArray *)array addObject:(id)object
{
if (object && [array isKindOfClass:NSMutableArray.class]) {
[array addObject:object];
}
}
+ (void)array:(NSMutableArray *)array addObject:(nullable id)object atIndex:(NSUInteger)index
{
if (object && [array isKindOfClass:NSMutableArray.class]) {
if (index < array.count) {
[array insertObject:object atIndex:index];
} else if (index == array.count) {
[array addObject:object];
}
}
}
+ (BOOL)boolValue:(id)object
{
if ([object isKindOfClass:[NSNumber class]]) {
// @0 or @NO returns NO, otherwise YES
return ((NSNumber *)object).boolValue;
} else if ([object isKindOfClass:[NSString class]]) {
// Returns YES on encountering one of "Y", "y", "T", "t", or a digit 1-9, otherwise NO
return ((NSString *)object).boolValue;
} else {
return ([self objectValue:object] != nil);
}
}
+ (NSDictionary *)dictionaryValue:(id)object
{
return (NSDictionary *)[self _objectValue:object ofClass:[NSDictionary class]];
}
+ (id)dictionary:(NSDictionary *)dictionary objectForKey:(NSString *)key ofType:(Class)type
{
id potentialValue = [[self dictionaryValue:dictionary] objectForKey:key];
if ([potentialValue isKindOfClass:type]) {
return potentialValue;
} else {
return nil;
}
}
+ (void)dictionary:(NSMutableDictionary *)dictionary setObject:(id)object forKey:(id<NSCopying>)key
{
if (object && key) {
dictionary[key] = object;
}
}
+ (void)dictionary:(NSDictionary *)dictionary enumerateKeysAndObjectsUsingBlock:(void(NS_NOESCAPE ^)(id key, id obj, BOOL *stop))block
{
NSDictionary *validDictionary = [self dictionaryValue:dictionary];
if (validDictionary) {
[validDictionary enumerateKeysAndObjectsUsingBlock:block];
}
}
+ (NSNumber *)numberValue:(id)object
{
return [self _objectValue:object ofClass:NSNumber.class];
}
+ (NSInteger)integerValue:(id)object
{
if ([object isKindOfClass:[NSNumber class]]) {
return ((NSNumber *)object).integerValue;
} else if ([object isKindOfClass:[NSString class]]) {
return ((NSString *)object).integerValue;
} else {
return 0;
}
}
+ (NSString *)stringValueOrNil:(id)object
{
return [self _objectValue:object ofClass:NSString.class];
}
+ (id)objectValue:(id)object
{
return ([object isKindOfClass:[NSNull class]] ? nil : object);
}
+ (NSString *)coercedToStringValue:(id)object
{
if ([object isKindOfClass:[NSString class]]) {
return (NSString *)object;
} else if ([object isKindOfClass:[NSNumber class]]) {
return ((NSNumber *)object).stringValue;
} else if ([object isKindOfClass:[NSURL class]]) {
return ((NSURL *)object).absoluteString;
} else {
return nil;
}
}
+ (NSTimeInterval)timeIntervalValue:(id)object
{
if ([object isKindOfClass:[NSNumber class]]) {
return ((NSNumber *)object).doubleValue;
} else if ([object isKindOfClass:[NSString class]]) {
return ((NSString *)object).doubleValue;
} else {
return 0;
}
}
+ (NSUInteger)unsignedIntegerValue:(id)object
{
if ([object isKindOfClass:[NSNumber class]]) {
return ((NSNumber *)object).unsignedIntegerValue;
} else {
// there is no direct support for strings containing unsigned values > NSIntegerMax - not worth writing ourselves
// right now, so just cap unsigned values at NSIntegerMax until we have a need for larger
NSInteger integerValue = [self integerValue:object];
if (integerValue < 0) {
integerValue = 0;
}
return (NSUInteger)integerValue;
}
}
+ (NSURL *)URLValue:(id)object
{
if ([object isKindOfClass:[NSURL class]]) {
return (NSURL *)object;
} else if ([object isKindOfClass:[NSString class]]) {
return [NSURL URLWithString:(NSString *)object];
} else {
return nil;
}
}
+ (BOOL)isValidJSONObject:(id)obj
{
return [NSJSONSerialization isValidJSONObject:obj];
}
+ (NSData *)dataWithJSONObject:(id)obj options:(NSJSONWritingOptions)opt error:(NSError *__autoreleasing _Nullable *)error
{
NSData *data;
@try {
data = [NSJSONSerialization dataWithJSONObject:obj options:opt error:error];
} @catch (NSException *exception) {
NSLog(@"FBSDKJSONSerialization - dataWithJSONObject:options:error failed: %@", exception.reason);
}
return data;
}
+ (id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError *__autoreleasing _Nullable *)error
{
if (![data isKindOfClass:NSData.class]) {
return nil;
}
id object;
@try {
object = [NSJSONSerialization JSONObjectWithData:data options:opt error:error];
} @catch (NSException *exception) {
NSLog(@"FBSDKJSONSerialization - JSONObjectWithData:options:error failed: %@", exception.reason);
}
return object;
}
#pragma mark - Helper Methods
+ (id)_objectValue:(id)object ofClass:(Class)expectedClass
{
return ([object isKindOfClass:expectedClass] ? object : nil);
}
@end
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
//
// You are hereby granted a non-exclusive, worldwide, royalty-free license to use,
// copy, modify, and distribute this software in source code or binary form for use
// in connection with the web services and APIs provided by Facebook.
//
// As with any software that integrates with the Facebook platform, your use of
// this software is subject to the Facebook Developer Principles and Policies
// [http://developers.facebook.com/policy/]. This copyright notice shall be
// included in all copies or substantial portions of the software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#import "FBSDKURLSession.h"
#import <Foundation/Foundation.h>
#import "FBSDKBasicUtility.h"
#import "FBSDKURLSessionTask.h"
// At some point this default conformance declaration needs to be moved out of
// this class and treated like the dependency it is.
@interface NSURLSession (SessionProviding) <FBSDKSessionProviding>
@end
@implementation FBSDKURLSession
- (instancetype)initWithDelegate:(id<NSURLSessionDataDelegate>)delegate
delegateQueue:(NSOperationQueue *)queue
{
if ((self = [super init])) {
self.delegate = delegate;
self.delegateQueue = queue;
}
return self;
}
- (void)executeURLRequest:(NSURLRequest *)request
completionHandler:(FBSDKURLSessionTaskBlock)handler
{
if (!self.valid) {
[self updateSessionWithBlock:^{
FBSDKURLSessionTask *task = [[FBSDKURLSessionTask alloc] initWithRequest:request fromSession:self.session completionHandler:handler];
[task start];
}];
} else {
FBSDKURLSessionTask *task = [[FBSDKURLSessionTask alloc] initWithRequest:request fromSession:self.session completionHandler:handler];
[task start];
}
}
- (void)updateSessionWithBlock:(dispatch_block_t)block
{
if (!self.valid) {
self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
delegate:_delegate
delegateQueue:_delegateQueue];
}
block();
}
- (void)invalidateAndCancel
{
[self.session invalidateAndCancel];
self.session = nil;
}
- (BOOL)valid
{
return self.session != nil;
}
@end
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
//
// You are hereby granted a non-exclusive, worldwide, royalty-free license to use,
// copy, modify, and distribute this software in source code or binary form for use
// in connection with the web services and APIs provided by Facebook.
//
// As with any software that integrates with the Facebook platform, your use of
// this software is subject to the Facebook Developer Principles and Policies
// [http://developers.facebook.com/policy/]. This copyright notice shall be
// included in all copies or substantial portions of the software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#import "FBSDKURLSessionTask.h"
#import "FBSDKSessionProviding.h"
@implementation FBSDKURLSessionTask
- (instancetype)init
{
if ((self = [super init])) {
_requestStartDate = [NSDate date];
}
return self;
}
- (instancetype)initWithRequest:(NSURLRequest *)request
fromSession:(id<FBSDKSessionProviding>)session
completionHandler:(FBSDKURLSessionTaskBlock)handler
{
if ((self = [self init])) {
self.requestStartTime = (uint64_t)([self.requestStartDate timeIntervalSince1970] * 1000);
self.task = [session dataTaskWithRequest:request completionHandler:handler];
}
return self;
}
- (NSURLSessionTaskState)state
{
return self.task.state;
}
#pragma mark - Task State
- (void)start
{
[self.task resume];
}
- (void)cancel
{
[self.task cancel];
self.handler = nil;
}
@end
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
//
// You are hereby granted a non-exclusive, worldwide, royalty-free license to use,
// copy, modify, and distribute this software in source code or binary form for use
// in connection with the web services and APIs provided by Facebook.
//
// As with any software that integrates with the Facebook platform, your use of
// this software is subject to the Facebook Developer Principles and Policies
// [http://developers.facebook.com/policy/]. This copyright notice shall be
// included in all copies or substantial portions of the software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
Dispatches the specified block on the main thread.
@param block the block to dispatch
*/
extern void fb_dispatch_on_main_thread(dispatch_block_t block);
/**
Dispatches the specified block on the default thread.
@param block the block to dispatch
*/
extern void fb_dispatch_on_default_thread(dispatch_block_t block);
/**
Describes the callback for appLinkFromURLInBackground.
@param object the FBSDKAppLink representing the deferred App Link
@param stop the error during the request, if any
*/
typedef id _Nullable (^FBSDKInvalidObjectHandler)(id object, BOOL *stop)
NS_SWIFT_NAME(InvalidObjectHandler);
@interface FBSDKBasicUtility : NSObject
/**
Converts an object into a JSON string.
@param object The object to convert to JSON.
@param errorRef If an error occurs, upon return contains an NSError object that describes the problem.
@param invalidObjectHandler Handles objects that are invalid, returning a replacement value or nil to ignore.
@return A JSON string or nil if the object cannot be converted to JSON.
*/
+ (nullable NSString *)JSONStringForObject:(id)object
error:(NSError *__autoreleasing *)errorRef
invalidObjectHandler:(nullable FBSDKInvalidObjectHandler)invalidObjectHandler;
/**
Sets an object for a key in a dictionary if it is not nil.
@param dictionary The dictionary to set the value for.
@param object The value to set after serializing to JSON.
@param key The key to set the value for.
@param errorRef If an error occurs, upon return contains an NSError object that describes the problem.
@return NO if an error occurred while serializing the object, otherwise YES.
*/
+ (BOOL)dictionary:(NSMutableDictionary<id, id> *)dictionary
setJSONStringForObject:(id)object
forKey:(id<NSCopying>)key
error:(NSError *__autoreleasing *)errorRef;
/**
Converts a JSON string into an object
@param string The JSON string to convert.
@param errorRef If an error occurs, upon return contains an NSError object that describes the problem.
@return An NSDictionary, NSArray, NSString or NSNumber containing the object representation, or nil if the string
cannot be converted.
*/
+ (nullable id)objectForJSONString:(NSString *)string error:(NSError *__autoreleasing *)errorRef;
/**
Constructs a query string from a dictionary.
@param dictionary The dictionary with key/value pairs for the query string.
@param errorRef If an error occurs, upon return contains an NSError object that describes the problem.
@param invalidObjectHandler Handles objects that are invalid, returning a replacement value or nil to ignore.
@return Query string representation of the parameters.
*/
+ (nullable NSString *)queryStringWithDictionary:(NSDictionary<NSString *, id> *)dictionary
error:(NSError *__autoreleasing *)errorRef
invalidObjectHandler:(nullable FBSDKInvalidObjectHandler)invalidObjectHandler;
/**
Converts simple value types to the string equivalent for serializing to a request query or body.
@param value The value to be converted.
@return The value that may have been converted if able (otherwise the input param).
*/
+ (id)convertRequestValue:(id)value;
/**
Encodes a value for an URL.
@param value The value to encode.
@return The encoded value.
*/
+ (NSString *)URLEncode:(NSString *)value;
/**
Parses a query string into a dictionary.
@param queryString The query string value.
@return A dictionary with the key/value pairs.
*/
+ (NSDictionary<NSString *, NSString *> *)dictionaryWithQueryString:(NSString *)queryString;
/**
Decodes a value from an URL.
@param value The value to decode.
@return The decoded value.
*/
+ (NSString *)URLDecode:(NSString *)value;
/**
Gzip data with default compression level if possible.
@param data The raw data.
@return nil if unable to gzip the data, otherwise gzipped data.
*/
+ (nullable NSData *)gzip:(NSData *)data;
+ (NSString *)anonymousID;
+ (NSString *)persistenceFilePath:(NSString *)filename;
+ (nullable NSString *)SHA256Hash:(nullable NSObject *)input;
@end
NS_ASSUME_NONNULL_END
This source diff could not be displayed because it is too large. You can view the blob instead.
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