Skip to content

Commit

Permalink
♻️ Extract delegate swizzling to single implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
mmaatttt committed May 6, 2024
1 parent cde4394 commit 83b5789
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 182 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand All @@ -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
Expand Down
64 changes: 4 additions & 60 deletions Sources/AppcuesKit/Push/UIApplication+AutoConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
66 changes: 6 additions & 60 deletions Sources/AppcuesKit/Push/UNUserNotificationCenter+AutoConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,79 +99,25 @@ 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)
)

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,
Expand Down
82 changes: 82 additions & 0 deletions Sources/AppcuesKit/Utilities/Swizzler.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 83b5789

Please sign in to comment.