4

I have a container that has clipToBounds set to false and a view outside of its bounds. Touch events are not recognized for out of bounds views.

Adrian
  • 59
  • 3

3 Answers3

6

just add this class to your view

class MyView: UIView {

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        for subview in subviews as [UIView] {
            if !subview.isHidden 
               && subview.alpha > 0 
               && subview.isUserInteractionEnabled 
               && subview.point(inside: convert(point, to: subview), with: event) {
                 return true
            }
        }
        return false
    }
}
Lukas Würzburger
  • 6,543
  • 7
  • 41
  • 75
Sandeep Baddula
  • 181
  • 1
  • 9
  • 1
    Thank, but I was looking for solution that does not involve subclassing, since i have a reusable view that will that has an outside of bounds components. It would be a wrong approach to subclass every view for each viewcontroller where I want to add the the view containing out of bounds componenet. – Adrian Jun 18 '18 at 06:59
  • That's wrong , Adrian. This is the way to go. It's completely commonplace that, regarding UI, you simply can't compose - you just subclass to add the concept needed. – Fattie Apr 06 '23 at 00:22
0

Here I fixed formatting & swizzling method of the previous answer

extension UIView {

    private enum ExtendedTouchAssociatedKey {
        static var outsideOfBounds = "viewExtensionAllowTouchesOutsideOfBounds"
    }

    /// This propery is set on the parent of the view that first clips the content you want to be touchable outside of the bounds
    var allowTouchesOfViewsOutsideBounds: Bool {
        get {
            objc_getAssociatedObject(self, &ExtendedTouchAssociatedKey.outsideOfBounds) as? Bool ?? false
        }
        set {
            UIView.swizzlePointInsideIfNeeded()
            subviews.forEach {
                $0.allowTouchesOfViewsOutsideBounds = newValue
            }
            objc_setAssociatedObject(self, &ExtendedTouchAssociatedKey.outsideOfBounds, newValue, .OBJC_ASSOCIATION_RETAIN)
        }
    }

    func hasSubview(at point: CGPoint) -> Bool {
        if subviews.isEmpty {
            return bounds.contains(point)
        }
        return subviews.contains { subview in
            let converted = self.convert(point, to: subview)
            return subview.hasSubview(at: converted)
        }
    }

    private static var swizzledMethods = false

    @objc
    func _point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        if allowTouchesOfViewsOutsideBounds {
            return originalPoint(inside: point, with: event) || hasSubview(at: point)
        }
        return originalPoint(inside: point, with: event)
    }

    @objc
    func originalPoint(inside point: CGPoint, with event: UIEvent?) -> Bool {
        fatalError("this should be swizzled")
    }

    private static func swizzlePointInsideIfNeeded() {
        if swizzledMethods {
            return
        }
        
        swizzledMethods = true
        
        let aClass = UIView.self
        
        let originalSelector = class_getInstanceMethod(
            aClass,
            #selector(point(inside:with:))
        )!
        
        let swizzledSelector = class_getInstanceMethod(
            aClass,
            #selector(_point(inside:with:))
        )!
        
        let backupSelector = class_getInstanceMethod(
            aClass,
            #selector(originalPoint(inside:with:))
        )!
        
        let nativeImplementation = method_getImplementation(originalSelector)
        let customImplementation = method_getImplementation(swizzledSelector)
        
        method_setImplementation(originalSelector, customImplementation)
        method_setImplementation(backupSelector, nativeImplementation)
    }
}
kubacizek
  • 578
  • 4
  • 9
-1

Here is an extension that will enable you to allow touches of clipped subviews in a container. Paste this file in your project and set containerView.allowTouchesOfViewsOutsideBounds = true

public extension UIView {

    private struct ExtendedTouchAssociatedKey {
        static var outsideOfBounds = "viewExtensionAllowTouchesOutsideOfBounds"
    }

    /// This propery is set on the parent of the view that first clips the content you want to be touchable
    /// outside of the bounds
    var allowTouchesOfViewsOutsideBounds:Bool {
        get {
            return objc_getAssociatedObject(self, &ExtendedTouchAssociatedKey.outsideOfBounds) as? Bool ?? false
        }
        set {
            UIView.swizzlePointInsideIfNeeded()
            subviews.forEach({$0.allowTouchesOfViewsOutsideBounds = newValue})
            objc_setAssociatedObject(self, &ExtendedTouchAssociatedKey.outsideOfBounds, newValue, .OBJC_ASSOCIATION_RETAIN)
        }
    }

    func hasSubview(at point:CGPoint) -> Bool {

        if subviews.count == 0 {
            return self.bounds.contains(point)
        }
        return subviews.contains(where: { (subview) -> Bool in
            let converted = self.convert(point, to: subview)
            return subview.hasSubview(at: converted)
        })

    }

    static private var swizzledMethods:Bool = false

    @objc func _point(inside point: CGPoint, with event: UIEvent?) -> Bool {

        if allowTouchesOfViewsOutsideBounds {
            return  _point(inside:point,with:event) || hasSubview(at: point)
        }
        return _point(inside:point,with:event)
    }

    static private func swizzlePointInsideIfNeeded() {
        if swizzledMethods {
            return
        }
        swizzledMethods = true
        let aClass: AnyClass! = UIView.self
        let originalSelector = #selector(point(inside:with:))
        let swizzledSelector = #selector(_point(inside:with:))
        swizzle(forClass: aClass, originalSelector: originalSelector, swizzledSelector: swizzledSelector)
    }
}
Efraim
  • 338
  • 3
  • 9