@dynamicReplacement causes infinite recursion?

Hello everyone!

For testing purposes, I am trying to replace the implementation of UIViewController's present method using the @_dynamicReplacement annotation.

The first thing this replacement does is call the original, then it does its own work.

extension UIViewController {
    @_dynamicReplacement(for: present(_:animated:completion:))
    func testablePresent(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
        self.present(viewControllerToPresent, animated: flag, completion: completion)
        // testing stuff...
    }
}

I thought that when the replacement implementation calls the method being replaced, it's supposed to call the original implementation. Not itself. That's what the original pitch says should happen anyway.

But with the code above, I'm definitely seeing infinite recursion. The call stack starts off like this:

...repeat...
in @nonobjc UIViewController.present(_:animated:completion:) ()
in UIViewController.mockedPresent(_:animated:completion:) at MyTests.swift:190
in @objc UIViewController.mockedPresent(_:animated:completion:) ()
in <source code method that calls present()>
...

It seems to looping through a few intermediate methods. For whatever reason @nonobjc UIViewController.present calls back into @objc UIViewController.mockedPresent.

Maybe it has something to do with present being an objc method? I'm not really sure what the problem is.

So far I've tried:

  • using some compiler flags that I read about but which didn't seem to actually be supported anymore. -Xfrontend -enable-dynamic-replacement-chaining
  • adding @nonobjc to the declarations.

Any help or advice would be appreciated! Thanks!

1 Like

I'm trying to do smth like this @_dynamicReplacement infinite recursion · GitHub

And getting the same issue at least in setViewControllers ;-;

Both of the mentioned UIVieController API calls are not marked with “dynamic” and thus are not eligible to “swift native swizzling” (done via the not so well known @_dynamicReplacement attribute). You can swizzle them the old Objective-C way (even in Swift) with an explicit “method_exchangeImplementation”, and if you need to call the original implementation from your replacement methods you would be calling the “new” (replacement) methods themselves (which does look like recursion but it is not).

3 Likes

I did a quick experiment using two UICollectionView methods as a guinea pig.

The first one:

    // UICollectionViewCell's
    @objc(_bridgedUpdateConfigurationUsingState:)
    dynamic open func updateConfiguration(using state: UICellConfigurationState)

Note "dynamic" keyword, it is important and allows me to write:

extension UICollectionViewCell {
    @_dynamicReplacement(for: updateConfiguration(using:))
    func myUpdateConfiguration(using state: UICellConfigurationState) {
        print("enter myUpdateConfiguration")
        updateConfiguration(using: state)
        print("exit myUpdateConfiguration")
    }
}

and without doing anything else myUpdateConfiguration will be called instead of the original method. Note that within it I call the original method via the original name. This is new in Swift 5.something "native swift swizzling".


Now to another dude:

    // UICollectionViewCell's
    open func dragStateDidChange(_ dragState: UICollectionViewCell.DragState)

If you try the same approach as above:

extension UICollectionViewCell {
    // MARK: wrong. don't do that ❌
    @_dynamicReplacement(for: dragStateDidChange(_:))
    func wrong_dragStateDidChange(_ dragState: UICollectionViewCell.DragState) {
        print("enter myDragStateDidChange")
        dragStateDidChange(dragState)
        print("exit myDragStateDidChange")
    }
}

This won't work (won't be called). The reason is – the method is not mark as dynamic. Old Obj-C swizzling to the rescue:

extension UICollectionViewCell {
    // MARK: âś…
    @objc func correct_dragStateDidChange(_ dragState: UICollectionViewCell.DragState) {
        print("enter myDragStateDidChange")
        correct_dragStateDidChange(dragState)
        print("exit myDragStateDidChange")
    }
}
...
// call this somewhere once, early in the app lifecycle:
    let original = class_getInstanceMethod(UICollectionViewCell.self, #selector(UICollectionViewCell.dragStateDidChange(_:)))!
    let swizzled = class_getInstanceMethod(UICollectionViewCell.self, #selector(UICollectionViewCell.correct_dragStateDidChange(_:)))!
    method_exchangeImplementations(original, swizzled)

Note that within the implementation I'm calling the original method via the new name: which looks like recursion but it is not. If you were to call the original method via the original name – you'll get the infinite recursion you are talking about.

Hope this helps.

1 Like

Ohhh interesting. That makes sense! Thanks for looking into this!