diff --git a/Sources/AppcuesKit/Presentation/Extensions/UIScrollView+ScrollObserver.swift b/Sources/AppcuesKit/Presentation/Extensions/UIScrollView+ScrollObserver.swift index a9ded2645..fa5ee3016 100644 --- a/Sources/AppcuesKit/Presentation/Extensions/UIScrollView+ScrollObserver.swift +++ b/Sources/AppcuesKit/Presentation/Extensions/UIScrollView+ScrollObserver.swift @@ -109,23 +109,26 @@ extension UIScrollView { shouldSetDelegate = true } - swizzle( - delegate, + Swizzler.swizzle( + targetInstance: delegate, targetSelector: NSSelectorFromString("scrollViewWillBeginDragging:"), + replacementOwner: UIScrollView.self, placeholderSelector: #selector(appcues__placeholderScrollViewWillBeginDragging), swizzleSelector: #selector(appcues__scrollViewWillBeginDragging) ) - swizzle( - delegate, + Swizzler.swizzle( + targetInstance: delegate, targetSelector: NSSelectorFromString("scrollViewDidEndDecelerating:"), + replacementOwner: UIScrollView.self, placeholderSelector: #selector(appcues__placeholderScrollViewDidEndDecelerating), swizzleSelector: #selector(appcues__scrollViewDidEndDecelerating) ) - swizzle( - delegate, + Swizzler.swizzle( + targetInstance: delegate, targetSelector: NSSelectorFromString("scrollViewDidEndDragging:willDecelerate:"), + replacementOwner: UIScrollView.self, placeholderSelector: #selector(appcues__placeholderScrollViewDidEndDragging), swizzleSelector: #selector(appcues__scrollViewDidEndDragging) ) @@ -143,62 +146,6 @@ extension UIScrollView { return delegate } - private func swizzle( - _ delegate: UIScrollViewDelegate, - targetSelector: Selector, - placeholderSelector: Selector, - swizzleSelector: Selector - ) { - // see if the currently assigned delegate has an implementation for the target selector already. - // these are optional methods in the protocol, and if they are not there already, we'll need to add - // a placeholder implementation so that we can consistently swap it with our override, which will attempt - // to call back into it, in case there was an implementation already - if we don't do this, we'll - // get invalid selector errors in these cases. - let originalMethod = class_getInstanceMethod(type(of: delegate), targetSelector) - - if originalMethod == nil { - // this is the case where the existing delegate does not have an implementation for the target selector - - guard let placeholderMethod = class_getInstanceMethod(UIScrollView.self, placeholderSelector) else { - // this really shouldn't ever be nil, as that would mean the function defined a few lines below is no - // longer there, but we must nil check this call - return - } - - // add the placeholder, so it can be swizzled uniformly - class_addMethod( - type(of: delegate), - targetSelector, - method_getImplementation(placeholderMethod), - method_getTypeEncoding(placeholderMethod) - ) - } - - // swizzle the new implementation to inject our own custom logic - - // this should never be nil, as it would mean the function defined a few lines below is no longer there, - // but we must nil check this call. - guard let swizzleMethod = class_getInstanceMethod(UIScrollView.self, swizzleSelector) else { return } - - // add the swizzled version - this will only succeed once for this instance, if its already there, we've already - // swizzled, and we can exit early in the next guard - let addMethodResult = class_addMethod( - type(of: delegate), - swizzleSelector, - method_getImplementation(swizzleMethod), - method_getTypeEncoding(swizzleMethod) - ) - - guard addMethodResult, - let originalMethod = originalMethod ?? class_getInstanceMethod(type(of: delegate), targetSelector), - let swizzledMethod = class_getInstanceMethod(type(of: delegate), swizzleSelector) else { - return - } - - // finally, here is where we swizzle in our custom implementation - method_exchangeImplementations(originalMethod, swizzledMethod) - } - @objc func appcues__placeholderScrollViewWillBeginDragging(_ scrollView: UIScrollView) { // this gives swizzling something to replace, if the existing delegate doesn't already diff --git a/Sources/AppcuesKit/Push/UIApplication+AutoConfig.swift b/Sources/AppcuesKit/Push/UIApplication+AutoConfig.swift index 2bf2f16d7..4c100f204 100644 --- a/Sources/AppcuesKit/Push/UIApplication+AutoConfig.swift +++ b/Sources/AppcuesKit/Push/UIApplication+AutoConfig.swift @@ -10,73 +10,17 @@ import UIKit extension UIApplication { static func swizzleDidRegisterForDeviceToken() { - guard let appDelegate = UIApplication.shared.delegate else { return } + guard let appDelegateInstance = UIApplication.shared.delegate else { return } - swizzle( - appDelegate, + Swizzler.swizzle( + targetInstance: appDelegateInstance, targetSelector: NSSelectorFromString("application:didRegisterForRemoteNotificationsWithDeviceToken:"), + replacementOwner: UIApplication.self, placeholderSelector: #selector(appcues__placeholderApplicationDidRegisterForRemoteNotificationsWithDeviceToken), swizzleSelector: #selector(appcues__applicationDidRegisterForRemoteNotificationsWithDeviceToken) ) } - private static func swizzle( - _ delegate: UIApplicationDelegate, - targetSelector: Selector, - placeholderSelector: Selector, - swizzleSelector: Selector - ) { - // see if the currently assigned delegate has an implementation for the target selector already. - // these are optional methods in the protocol, and if they are not there already, we'll need to add - // a placeholder implementation so that we can consistently swap it with our override, which will attempt - // to call back into it, in case there was an implementation already - if we don't do this, we'll - // get invalid selector errors in these cases. - let originalMethod = class_getInstanceMethod(type(of: delegate), targetSelector) - - if originalMethod == nil { - // this is the case where the existing delegate does not have an implementation for the target selector - - guard let placeholderMethod = class_getInstanceMethod(UIApplication.self, placeholderSelector) else { - // this really shouldn't ever be nil, as that would mean the function defined a few lines below is no - // longer there, but we must nil check this call - return - } - - // add the placeholder, so it can be swizzled uniformly - class_addMethod( - type(of: delegate), - targetSelector, - method_getImplementation(placeholderMethod), - method_getTypeEncoding(placeholderMethod) - ) - } - - // swizzle the new implementation to inject our own custom logic - - // this should never be nil, as it would mean the function defined a few lines below is no longer there, - // but we must nil check this call. - guard let swizzleMethod = class_getInstanceMethod(UIApplication.self, swizzleSelector) else { return } - - // add the swizzled version - this will only succeed once for this instance, if its already there, we've already - // swizzled, and we can exit early in the next guard - let addMethodResult = class_addMethod( - type(of: delegate), - swizzleSelector, - method_getImplementation(swizzleMethod), - method_getTypeEncoding(swizzleMethod) - ) - - guard addMethodResult, - let originalMethod = originalMethod ?? class_getInstanceMethod(type(of: delegate), targetSelector), - let swizzledMethod = class_getInstanceMethod(type(of: delegate), swizzleSelector) else { - return - } - - // finally, here is where we swizzle in our custom implementation - method_exchangeImplementations(originalMethod, swizzledMethod) - } - - @objc func appcues__placeholderApplicationDidRegisterForRemoteNotificationsWithDeviceToken( _ application: UIApplication, diff --git a/Sources/AppcuesKit/Push/UNUserNotificationCenter+AutoConfig.swift b/Sources/AppcuesKit/Push/UNUserNotificationCenter+AutoConfig.swift index 2337ed296..cfe5dc10a 100644 --- a/Sources/AppcuesKit/Push/UNUserNotificationCenter+AutoConfig.swift +++ b/Sources/AppcuesKit/Push/UNUserNotificationCenter+AutoConfig.swift @@ -99,16 +99,18 @@ extension UNUserNotificationCenter { self.delegate = delegate } - swizzle( - delegate, + Swizzler.swizzle( + targetInstance: delegate, targetSelector: NSSelectorFromString("userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:"), + replacementOwner: UNUserNotificationCenter.self, placeholderSelector: #selector(appcues__placeholderUserNotificationCenterDidReceive), swizzleSelector: #selector(appcues__userNotificationCenterDidReceive) ) - swizzle( - delegate, + Swizzler.swizzle( + targetInstance: delegate, targetSelector: NSSelectorFromString("userNotificationCenter:willPresentNotification:withCompletionHandler:"), + replacementOwner: UNUserNotificationCenter.self, placeholderSelector: #selector(appcues__placeholderUserNotificationCenterWillPresent), swizzleSelector: #selector(appcues__userNotificationCenterWillPresent) ) @@ -116,62 +118,6 @@ extension UNUserNotificationCenter { return delegate } - private func swizzle( - _ delegate: UNUserNotificationCenterDelegate, - targetSelector: Selector, - placeholderSelector: Selector, - swizzleSelector: Selector - ) { - // see if the currently assigned delegate has an implementation for the target selector already. - // these are optional methods in the protocol, and if they are not there already, we'll need to add - // a placeholder implementation so that we can consistently swap it with our override, which will attempt - // to call back into it, in case there was an implementation already - if we don't do this, we'll - // get invalid selector errors in these cases. - let originalMethod = class_getInstanceMethod(type(of: delegate), targetSelector) - - if originalMethod == nil { - // this is the case where the existing delegate does not have an implementation for the target selector - - guard let placeholderMethod = class_getInstanceMethod(UNUserNotificationCenter.self, placeholderSelector) else { - // this really shouldn't ever be nil, as that would mean the function defined a few lines below is no - // longer there, but we must nil check this call - return - } - - // add the placeholder, so it can be swizzled uniformly - class_addMethod( - type(of: delegate), - targetSelector, - method_getImplementation(placeholderMethod), - method_getTypeEncoding(placeholderMethod) - ) - } - - // swizzle the new implementation to inject our own custom logic - - // this should never be nil, as it would mean the function defined a few lines below is no longer there, - // but we must nil check this call. - guard let swizzleMethod = class_getInstanceMethod(UNUserNotificationCenter.self, swizzleSelector) else { return } - - // add the swizzled version - this will only succeed once for this instance, if its already there, we've already - // swizzled, and we can exit early in the next guard - let addMethodResult = class_addMethod( - type(of: delegate), - swizzleSelector, - method_getImplementation(swizzleMethod), - method_getTypeEncoding(swizzleMethod) - ) - - guard addMethodResult, - let originalMethod = originalMethod ?? class_getInstanceMethod(type(of: delegate), targetSelector), - let swizzledMethod = class_getInstanceMethod(type(of: delegate), swizzleSelector) else { - return - } - - // finally, here is where we swizzle in our custom implementation - method_exchangeImplementations(originalMethod, swizzledMethod) - } - @objc func appcues__placeholderUserNotificationCenterDidReceive( _ center: UNUserNotificationCenter, diff --git a/Sources/AppcuesKit/Utilities/Swizzler.swift b/Sources/AppcuesKit/Utilities/Swizzler.swift new file mode 100644 index 000000000..c595921f6 --- /dev/null +++ b/Sources/AppcuesKit/Utilities/Swizzler.swift @@ -0,0 +1,82 @@ +// +// Swizzler.swift +// AppcuesKit +// +// Created by Matt on 2024-04-11. +// Copyright © 2024 Appcues. All rights reserved. +// + +import Foundation + +internal enum Swizzler { + /// Swizzling for delegate objects. + /// + /// This is unique because, + /// 1. We aren't certain of the class type that implements the delegate protocol at compile time. + /// This is the reason why this function takes an instance of the delegate instead of the delegate type. + /// 2. Delegate methods are frequently optional, so we can't rely on the implementation being there to swizzle. + /// If this is the case, we add an empty placeholder implementation and then swizzle that. + /// + /// - Parameters: + /// - targetInstance: Instance of the class to replace the method in. + /// - targetSelector: Selector of the method to replace. + /// - replacementOwner: Class containing the methods selected by `swizzleSelector` and `placeholderSelector`. + /// - placeholderSelector: Selector of the method to use the `targetSelector` method is not implemented. + /// This should be an empty function. + /// - swizzleSelector: Selector of the method to use as the replacement. + static func swizzle( + targetInstance: AnyObject, + targetSelector: Selector, + replacementOwner: AnyClass, + placeholderSelector: Selector, + swizzleSelector: Selector + ) { + // see if the currently assigned delegate has an implementation for the target selector already. + // these are optional methods in the protocol, and if they are not there already, we'll need to add + // a placeholder implementation so that we can consistently swap it with our override, which will attempt + // to call back into it, in case there was an implementation already - if we don't do this, we'll + // get invalid selector errors in these cases. + let targetClass: AnyClass = type(of: targetInstance) + let originalMethod = class_getInstanceMethod(targetClass, targetSelector) + + if originalMethod == nil { + // this is the case where the existing delegate does not have an implementation for the target selector + + guard let placeholderMethod = class_getInstanceMethod(replacementOwner, placeholderSelector) else { + // this should never be nil as it would be a developer error, but we must nil check this call + return + } + + // add the placeholder, so it can be swizzled uniformly + class_addMethod( + targetClass, + targetSelector, + method_getImplementation(placeholderMethod), + method_getTypeEncoding(placeholderMethod) + ) + } + + // swizzle the new implementation to inject our own custom logic + + // this should never be nil as it would be a developer error, but we must nil check this call + guard let swizzleMethod = class_getInstanceMethod(replacementOwner, swizzleSelector) else { return } + + // add the swizzled version - this will only succeed once for this instance, if its already there, we've already + // swizzled, and we can exit early in the next guard + let addMethodResult = class_addMethod( + targetClass, + swizzleSelector, + method_getImplementation(swizzleMethod), + method_getTypeEncoding(swizzleMethod) + ) + + guard addMethodResult, + let originalMethod = originalMethod ?? class_getInstanceMethod(targetClass, targetSelector), + let swizzledMethod = class_getInstanceMethod(targetClass, swizzleSelector) else { + return + } + + // finally, here is where we swizzle in our custom implementation + method_exchangeImplementations(originalMethod, swizzledMethod) + } +}