Skip to content

Commit

Permalink
♻️ Update push auto config to be static
Browse files Browse the repository at this point in the history
  • Loading branch information
mmaatttt committed May 6, 2024
1 parent 0509db7 commit 9bd3a32
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 98 deletions.
38 changes: 19 additions & 19 deletions Sources/AppcuesKit/Appcues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,25 @@ public class Appcues: NSObject {
config.logger.info("Appcues SDK %{public}@ initialized", version())
}

/// Enables automatic push notification management.
///
/// This should be called in `UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:)`
/// to ensure no incoming notifications are missed.
///
/// The following will automatically be handled:
/// 1. Calling `UIApplication.registerForRemoteNotifications()`
/// 2. Implementing `UIApplicationDelegate.application(_:didRegisterForRemoteNotificationsWithDeviceToken:)`
/// to call ``setPushToken(_:)``
/// 3. Ensuring `UNUserNotificationCenter.current().delegate` is set
/// 4. Implementing `UNUserNotificationCenterDelegate.userNotificationCenter(_:didReceive:withCompletionHandler:)`
/// to call ``didReceiveNotification(response:completionHandler:)``
/// 5. Implementing `UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:withCompletionHandler:)`
/// to show notification while the app is in the foreground
@objc
public static func enableAutomaticPushConfig() {
PushAutoConfig.configureAutomatically()
}

/// Get the current version of the Appcues SDK.
/// - Returns: Current version of the Appcues SDK.
@objc(sdkVersion)
Expand Down Expand Up @@ -312,25 +331,6 @@ public class Appcues: NSObject {
_ = container.resolve(UIKitScreenTracker.self)
}

/// Enables automatic push notification management.
///
/// This should be called in `UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:)` to ensure no incoming notifications are missed.
///
/// The following will automatically be handled:
/// 1. Calling `UIApplication.registerForRemoteNotifications()`
/// 2. Implementing `UIApplicationDelegate.application(_:didRegisterForRemoteNotificationsWithDeviceToken:)`
/// to call ``setPushToken(_:)``
/// 3. Ensuring `UNUserNotificationCenter.current().delegate` is set
/// 4. Implementing `UNUserNotificationCenterDelegate.userNotificationCenter(_:didReceive:withCompletionHandler:)`
/// to call ``didReceiveNotification(response:completionHandler:)``
/// 5. Implementing `UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:withCompletionHandler:)`
/// to show notification while the app is in the foreground
@objc
public func enableAutomaticPushConfig() {
let pushMonitor = container.resolve(PushMonitoring.self)
pushMonitor.configureAutomatically()
}

/// Verifies if an incoming URL is intended for the Appcues SDK.
/// - Parameter url: The URL being opened.
/// - Returns: `true` if the URL matches the Appcues URL Scheme or `false` if the URL is not known by the Appcues SDK.
Expand Down
73 changes: 73 additions & 0 deletions Sources/AppcuesKit/Push/PushAutoConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// PushAutoConfig.swift
// AppcuesKit
//
// Created by Matt on 2024-04-15.
// Copyright © 2024 Appcues. All rights reserved.
//

import UIKit

internal enum PushAutoConfig {
// This is an array to support the (rare) case of multiple SDK instances supporting push
private static var pushMonitors: [WeakPushMonitoring] = []

static func register(observer: PushMonitoring) {
pushMonitors.append(WeakPushMonitoring(observer))
}

static func remove(observer: PushMonitoring) {
pushMonitors.removeAll { $0.value == nil || $0.value === observer }
}

static func configureAutomatically() {
UIApplication.swizzleDidRegisterForDeviceToken()
UIApplication.shared.registerForRemoteNotifications()

UNUserNotificationCenter.swizzleNotificationCenterGetDelegate()
}

static func didRegister(deviceToken: Data) {
// Pass device token to all observing PushMonitor instances
pushMonitors.forEach { weakPushMonitor in
if let pushMonitor = weakPushMonitor.value {
pushMonitor.setPushToken(deviceToken)
}
}
}

// Shared instance is called from the swizzled method
static func didReceive(
_ response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
// Stop at the first PushMonitor that successfully handles the notification
_ = pushMonitors.first { weakPushMonitor in
if let pushMonitor = weakPushMonitor.value {
return pushMonitor.didReceiveNotification(response: response, completionHandler: completionHandler)
}
return false
}
}

// Shared instance is called from the swizzled method
static func willPresent(
_ parsedNotification: ParsedNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
// Behavior for all Appcues notification
if #available(iOS 14.0, *) {
completionHandler([.banner, .list])
} else {
completionHandler(.alert)
}
}
}

extension PushAutoConfig {
class WeakPushMonitoring {
weak var value: PushMonitoring?

init (_ wrapping: PushMonitoring) { self.value = wrapping }
}
}
22 changes: 3 additions & 19 deletions Sources/AppcuesKit/Push/PushMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ internal protocol PushMonitoring: AnyObject {
var pushBackgroundEnabled: Bool { get }
var pushPrimerEligible: Bool { get }

func configureAutomatically()

func setPushToken(_ deviceToken: Data?)

func refreshPushStatus(publishChange: Bool, completion: ((UNAuthorizationStatus) -> Void)?)
Expand All @@ -32,10 +30,6 @@ internal class PushMonitor: PushMonitoring {
private let storage: DataStoring
private let analyticsPublisher: AnalyticsPublishing

// Store this value to know if we need to remove the notification center observer.
// Calling remove every time would result in inadvertently initializing the shared instance.
private var configuredAutomatically = false

private(set) var pushAuthorizationStatus: UNAuthorizationStatus = .notDetermined

var pushEnabled: Bool {
Expand Down Expand Up @@ -66,23 +60,15 @@ internal class PushMonitor: PushMonitoring {
name: UIApplication.willEnterForegroundNotification,
object: nil
)

PushAutoConfig.register(observer: self)
}

@objc
private func applicationWillEnterForeground(notification: Notification) {
refreshPushStatus(publishChange: true)
}

func configureAutomatically() {
UIApplication.swizzleDidRegisterForDeviceToken()
UIApplication.shared.registerForRemoteNotifications()

UNUserNotificationCenter.swizzleNotificationCenterGetDelegate()
AppcuesUNUserNotificationCenterDelegate.shared.register(observer: self)

configuredAutomatically = true
}

func setPushToken(_ deviceToken: Data?) {
storage.pushToken = deviceToken?.map { String(format: "%02x", $0) }.joined()

Expand Down Expand Up @@ -232,8 +218,6 @@ internal class PushMonitor: PushMonitoring {
#endif

deinit {
if configuredAutomatically {
AppcuesUNUserNotificationCenterDelegate.shared.remove(observer: self)
}
PushAutoConfig.remove(observer: self)
}
}
8 changes: 5 additions & 3 deletions Sources/AppcuesKit/Push/UIApplication+AutoConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ extension UIApplication {
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
AppcuesUNUserNotificationCenterDelegate.shared.didRegister(deviceToken: deviceToken)
PushAutoConfig.didRegister(deviceToken: deviceToken)

// Also call the original implementation
appcues__applicationDidRegisterForRemoteNotificationsWithDeviceToken(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)

appcues__applicationDidRegisterForRemoteNotificationsWithDeviceToken(
application,
didRegisterForRemoteNotificationsWithDeviceToken: deviceToken
)
}
}
60 changes: 3 additions & 57 deletions Sources/AppcuesKit/Push/UNUserNotificationCenter+AutoConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,63 +9,9 @@
import Foundation
import UserNotifications

// This is a placeholder delegate implementation in case there's no UNUserNotificationCenter.delegate set in the app
internal class AppcuesUNUserNotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate {
static var shared = AppcuesUNUserNotificationCenterDelegate()

// This is an array to support the (rare) case of multiple SDK instances supporting push
private var pushMonitors: [WeakPushMonitoring] = []

func register(observer: PushMonitoring) {
pushMonitors.append(WeakPushMonitoring(observer))
}

func remove(observer: PushMonitoring) {
pushMonitors.removeAll { $0.value === observer }
}

func didRegister(deviceToken: Data) {
// Pass device token to all observing PushMonitor instances
pushMonitors.forEach { weakPushMonitor in
if let pushMonitor = weakPushMonitor.value {
pushMonitor.setPushToken(deviceToken)
}
}
}

// Shared instance is called from the swizzled method
func didReceive(
_ response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
// Stop at the first PushMonitor that successfully handles the notification
_ = pushMonitors.first { weakPushMonitor in
if let pushMonitor = weakPushMonitor.value {
return pushMonitor.didReceiveNotification(response: response, completionHandler: completionHandler)
}
return false
}
}

// Shared instance is called from the swizzled method
func willPresent(
_ parsedNotification: ParsedNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
// Behavior for all Appcues notification
if #available(iOS 14.0, *) {
completionHandler([.banner, .list])
} else {
completionHandler(.alert)
}
}
}

extension AppcuesUNUserNotificationCenterDelegate {
class WeakPushMonitoring {
weak var value: PushMonitoring?

init (_ wrapping: PushMonitoring) { self.value = wrapping }
}
}

extension UNUserNotificationCenter {
Expand Down Expand Up @@ -145,7 +91,7 @@ extension UNUserNotificationCenter {
withCompletionHandler completionHandler: @escaping () -> Void
) {
if ParsedNotification(userInfo: response.notification.request.content.userInfo) != nil {
AppcuesUNUserNotificationCenterDelegate.shared.didReceive(response, withCompletionHandler: completionHandler)
PushAutoConfig.didReceive(response, withCompletionHandler: completionHandler)
} else {
// Not an Appcues push, so pass to the original implementation
appcues__userNotificationCenterDidReceive(center, didReceive: response, withCompletionHandler: completionHandler)
Expand All @@ -159,7 +105,7 @@ extension UNUserNotificationCenter {
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
if let parsedNotification = ParsedNotification(userInfo: notification.request.content.userInfo) {
AppcuesUNUserNotificationCenterDelegate.shared.willPresent(parsedNotification, withCompletionHandler: completionHandler)
PushAutoConfig.willPresent(parsedNotification, withCompletionHandler: completionHandler)
} else {
// Not an Appcues push, so pass to the original implementation
appcues__userNotificationCenterWillPresent(center, willPresent: notification, withCompletionHandler: completionHandler)
Expand Down

0 comments on commit 9bd3a32

Please sign in to comment.