Assigning to a pointer causes EXC_BAD_ACCESS through class_addMethod

56 views Asked by At

I have an internal class that adoptsUIScrollViewDelegate, but does not implement scrollViewWillEndDragging(_:withVelocity:targetContentOffset:). I want to add this method to the class using class_addMethod, but unfortunately run into a EXC_BAD_ACCESS error when trying to assign a new value to targetContentOffset.

Here is my code:

func extendDelegate(scrollViewDelegte: UIScrollViewDelegate) {
   let block : @convention(block) (UIScrollView, CGPoint, UnsafeMutablePointer<CGPoint>) -> Void = { scrollView, velocity, targetContentOffset  in
      // EXC_BAD_ACCESS here. 
      // I can read the pointee and it does contain a valid CGPoint value, 
      // however assigning to it gives a crash.
      targetContentOffset.pointee = .zero
   }

   class_addMethod(
      type(of: scrollViewDelegte),
      #selector(UIScrollViewDelegate.scrollViewWillEndDragging(_:withVelocity:targetContentOffset:)),
      imp_implementationWithBlock(block),
      "v@:@{CGPoint=dd}^{CGPoint=dd}"
   )
}

Any helps is greatly appreciated!

2

There are 2 answers

1
Cy-4AH On BEST ANSWER

From imp_implementationWithBlock's documentation:

The signature of block should be method_return_type ^(id self, method_args ...)

I think you are missing self in parameters.

2
Alexander On

Rather than generating your own IMP and type encoding, I'd suggest you let the compiler do it for you, by creating a method normally, and transplanting it over:

import Foundation
import ObjectiveC

@objc class ScrollViewPatch: NSObject {
    @objc func scrollViewWillEndDragging(
        _ scrollView: UIScrollView,
        withVelocity velocity: CGPoint,
        targetContentOffset: UnsafeMutablePointer<CGPoint>
    ) {
        targetContentOffset.pointee = .zero
    }
}

func extendDelegate(scrollViewDelegte: UIScrollViewDelegate) {
    let patchedMethod = class_getInstanceMethod(
        ScrollViewPatch.self,
        #selector(ScrollViewPatch.scrollViewWillEndDragging(_:withVelocity:targetContentOffset:))
    )!

    class_addMethod(
        type(of: scrollViewDelegte),
        #selector(UIScrollViewDelegate.scrollViewWillEndDragging(_:withVelocity:targetContentOffset:)),
        method_getImplementation(patchedMethod),
        method_getTypeEncoding(patchedMethod) // Much more reliable than hard-coding "v@:@{CGPoint=dd}^{CGPoint=dd}"
    )
}

Here's a simpler standalone proof-of-concept:

import Foundation
import ObjectiveC

@objc class MethodSource: NSObject {
    @objc func myMethod(_ arg: NSString) {
        print("Called \(#function) on \(self) with \"\(arg)\"")
    }
}

@objc class TargetClass: NSObject {
    // Doens't natively respond to `-myMethod`
}

let targetObject = TargetClass()

// The target object doesn't initially respond to `-myMethod`
print(targetObject.responds(to: #selector(MethodSource.myMethod))) // false

let patchedMethod = class_getInstanceMethod(
    MethodSource.self,
    #selector(MethodSource.myMethod)
)!

class_addMethod(
    type(of: targetObject),
    #selector(MethodSource.myMethod),
    method_getImplementation(patchedMethod),
    method_getTypeEncoding(patchedMethod)
)

// Now it's true
print(targetObject.responds(to: #selector(MethodSource.myMethod)))

// Called myMethod(_:) on <main.TargetClass: 0x6000016b0040> with "some string"
targetObject.perform(#selector(MethodSource.myMethod), with: "some string")