10

While it is possible to replace setMyProperty: method in obj-c, I'm wondering how to do it in swift?

For example I want to replace UIScrollView::setContentOffset::

let originalSelector: Selector = #selector(UIScrollView.setContentOffset)
let replaceSelector: Selector = #selector(UIScrollView.setContentOffsetHacked)
...

...but after execution originalSelector contains setContentOffset:animaed. So, how to pass setter method of property to selector?

brigadir
  • 6,874
  • 6
  • 46
  • 81
  • This article could help you http://nshipster.com/swift-objc-runtime/ – Victor Sigler Aug 04 '16 at 22:28
  • 1
    And if you want to chase the down the rabbit-hole: http://stackoverflow.com/questions/25651081/method-swizzling-in-swift?rq=1 – BaseZen Aug 04 '16 at 22:33
  • @VictorSigler that article doesn't cover this special case with property setter. – brigadir Aug 04 '16 at 22:51
  • @BaseZen, the same for question & answers mentioned by you. I'm looking how to deal with property setter, especially I want to know why wrong method is picked from `#selector(UIScrollView.setContentOffset)`. – brigadir Aug 04 '16 at 22:55
  • Answer has been redone, so I'm curious if that fits your needs now. – BaseZen Aug 07 '16 at 18:06

2 Answers2

13

Starting from Swift 2.3 (XCode 8) it's possible to assign setter and getter to selector variable:

The Objective-C selectors for the getter or setter of a property can now be referenced with #selector. For example:

let sel: Selector = #selector(setter: UIScrollView.contentOffset)

More details here

brigadir
  • 6,874
  • 6
  • 46
  • 81
10

[REWRITTEN after further research]

Here's an elaborate workaround based on the below

http://nshipster.com/swift-objc-runtime/

[WARNING from the authors]

In closing, remember that tinkering with the Objective-C runtime should be much more of a last resort than a place to start. Modifying the frameworks that your code is based upon, as well as any third-party code you run, is a quick way to destabilize the whole stack. Tread softly!

So here it is, all accessors and mutators have to be covered, so it's a lot. Plus, since you need to intercede with the values but must re-use the original stored property since you can't introduce any new storage here, you have some bizarre looking functions that appear to be recursive but aren't because of runtime swizzling. This is the first time the compiler has generated a warning for my code that I know will be wrong at runtime.

Oh well, it is an interesting academic exercise.

extension UIScrollView {
    struct StaticVars {
        static var token: dispatch_once_t = 0
    }

    public override class func initialize() {
        dispatch_once(&StaticVars.token) {
            guard self == UIScrollView.self else {
                return
            }
            // Accessor
            method_exchangeImplementations(
                class_getInstanceMethod(self, Selector("swizzledContentOffset")),
                class_getInstanceMethod(self, Selector("contentOffset"))
            )
            // Two-param setter
            method_exchangeImplementations(
                class_getInstanceMethod(self, #selector(UIScrollView.setContentOffset(_:animated:))),
                class_getInstanceMethod(self, #selector(UIScrollView.swizzledSetContentOffset(_:animated:)))
            )
            // One-param setter
            method_exchangeImplementations(
                class_getInstanceMethod(self, #selector(UIScrollView.swizzledSetContentOffset(_:))),
                class_getInstanceMethod(self, Selector("setContentOffset:")))
        }
    }

    func swizzledSetContentOffset(inContentOffset: CGPoint, animated: Bool) {
        print("Some interceding code for the swizzled 2-param setter with \(inContentOffset)")
        // This is not recursive. The method implementations have been exchanged by runtime. This is the
        // original setter that will run.
        swizzledSetContentOffset(inContentOffset, animated: animated)
    }


    func swizzledSetContentOffset(inContentOffset: CGPoint) {
        print("Some interceding code for the swizzled 1-param setter with \(inContentOffset)")
        swizzledSetContentOffset(inContentOffset) // not recursive
    }


    var swizzledContentOffset: CGPoint {
        get {
            print("Some interceding code for the swizzled accessor: \(swizzledContentOffset)") // false warning
            return swizzledContentOffset // not recursive, false warning
        }
    }
}
BaseZen
  • 8,650
  • 3
  • 35
  • 47
  • Thanks, but I really need to replace `setContentOffset` for `UIScrollView` and its subclasses. The reason is - I'm implementing custom pull-to-refresh control. – brigadir Aug 04 '16 at 22:50
  • I see `public var contentOffset: CGPoint` in `UIScrollView` interface - so it's read-write, not readonly. `setContentOffset(animated:)` is standalone method. When dragging scroll-view, its `contentOffset` is set via setter, not via `setContentOffset:animated`. Looks like your answer doesn't deal with my issue. – brigadir Aug 05 '16 at 17:25
  • You're right, I misread how it showed up as a 'V' in Xcode, thought that meant read-only. Redone. Anyway, see my new comments. It does all appear to work. – BaseZen Aug 05 '16 at 23:51