1

I am trying to swizzle the UITouch lifecycle events by creating a UIResponder extension. Following is an excerpt depicting the implementation for touchesBegan.

 public extension UIResponder {

   private enum TouchPhase: String {
    case begin
    case move
    case end
    case cancel
    //computed property
    var phase: String {
        return self.rawValue
    }
}

    @objc func swizztouchesBegan(_ touches: Set<UITouch>, with event: 
            UIEvent?) {
         print("swizztouchesBegan event called!")
         let touch = touches.first! as UITouch
         self.createTouchesDict(for: touch, event: event, phase: .begin)
         self.swizztouchesBegan(touches, with: event)
     }

    static func swizzleTouchesBegan() {
       let _: () = {
            let originalSelector = 
        #selector(UIResponder.touchesBegan(_:with:))
            let swizzledSelector = 
        #selector(UIResponder.swizztouchesBegan(_:with:))
             let originalMethod = class_getInstanceMethod(UIResponder.self, originalSelector)
             let swizzledMethod = class_getInstanceMethod(UIResponder.self, swizzledSelector)
    method_exchangeImplementations(originalMethod!, swizzledMethod!)
    }()
 }
}

I am then invoking it in my app like so-

 [UIResponder swizzleTouchesBegan]; //my app is a mix of Objective-C and Swift

What I see is- "swizztouchesBegan event called!" gets printed and I get to create the dictionary of the touch metadata. However, it looks like it does not call the original implementation, e.g. let's say somewhere in my code, inside a table view controller, I write the following-

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesBegan(touches, with: event)
    print("AM I GETTING CALLED?")
} 

this overridden touchesBegan is not getting called- what could be the issue? I am already calling the original implementation from inside the swizzled one by calling the swizzled one again (thereby making sure the original one gets called.)

PS/ Addendum: I have been able to get it working by just overriding the touch methods in an UIView extension, like following. Can someone point out if there's any disadvantage using this approach over the swizzling one?

public extension UIView {

private enum TouchPhase: String {
    case begin
    case move
    case end
    case cancel
    //computed property
    var phase: String {
        return self.rawValue
    }
}
open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

    print("overriding touches began event!")
    let touch = touches.first! as UITouch
    self.createTouchesDict(for: touch, event: event, phase: .begin)
    super.touchesBegan(touches, with: event)
}

open override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    print("overriding touches moved event!")
    let touch = touches.first! as UITouch
    self.createTouchesDict(for: touch, event: event, phase: .move)
    super.touchesMoved(touches, with: event)

}

open override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    print("overriding touches ended event!")
    let touch = touches.first! as UITouch
    self.createTouchesDict(for: touch, event: event, phase: .end)
    super.touchesEnded(touches, with: event)

}

  open override func touchesCancelled(_ touches: Set<UITouch>, with 
 event: UIEvent?) {
    print("overriding touches canceled event!")
    let touch = touches.first! as UITouch
    self.createTouchesDict(for: touch, event: event, phase: .cancel)
    super.touchesCancelled(touches, with: event)

    }
  }
Subzero
  • 841
  • 6
  • 20

2 Answers2

0

It seems to me that you're swizzling the selector always without checking if that was already done.

here is a sample on how you can do it.

extension UIResponder {

    static let swizzleIfNeeded: () = {
        swizzle(originalSelector: #selector(touchesBegan(_:with:)), to: #selector(swizztouchesBegan(_:with:)))
    }()

    static func swizzle(originalSelector: Selector, to swizzledSelector: Selector) {
        let originalMethod = class_getInstanceMethod(self, originalSelector)
        let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
        let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!))
        if didAddMethod {
            class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!))
        } else {
            method_exchangeImplementations(originalMethod!, swizzledMethod!);
        }
    }

    @objc func swizztouchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("swizztouchesBegan event called!")
        let touch = touches.first! as UITouch
        self.createTouchesDict(for: touch, event: event, phase: .begin)
        self.swizztouchesBegan(touches, with: event)
    }
}

then in you app delegate did finish (for example)

UIResponder.swizzleIfNeeded
zombie
  • 5,069
  • 3
  • 25
  • 54
  • 1
    I tried your solution- I think it's an optimized one but it still did not solve the issue. The original impl is still not getting called. Because of this, let's say, if I tap on a cell in a UICollectionView, it's didSelectItemAt delegate is not getting called. Not sure why's that happening. – Subzero Jul 09 '18 at 23:15
0

It seems method swizzle is not necessary for swift, because there are extensions.

So if you want to swizzle touch for any class you just override touch. like below I wanted to give bounce effect to all Bounce

extension UIView {
    func highlight(color: UIColor) {
        let anim = CABasicAnimation.init(keyPath: #keyPath(CALayer.backgroundColor))
        anim.fromValue = UIColor.clear.cgColor
        anim.toValue = color.cgColor
        anim.duration = 0.5
        anim.autoreverses = true
        anim.repeatCount = 0
        anim.fillMode = kCAFillModeForwards
        anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)

        self.layer.add(anim, forKey: "blink")
    }

    func bounce() {
        let endTransform = CGAffineTransform(scaleX: 1.0, y: 1.0)
        let prepareTransform = CGAffineTransform(scaleX: 0.95, y: 0.95)
        UIView.animateKeyframes(
            withDuration: 0.2, delay: 0.0, options: UIView.KeyframeAnimationOptions.calculationModeLinear, animations: {
                UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.1) {
                    self.transform = prepareTransform
                }
                UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.1) {
                    self.transform = endTransform
                }
        }, completion: nil)
    }
}

extension BounceButton {
    open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.bounce()
        super.touchesBegan(touches, with: event)
    }
}

But in your case you can do for all UIView extensions, so everything can be bounceable.

mert
  • 1,090
  • 13
  • 26