Adrian
Adrian

Reputation: 69

How to enable touches of a view outside bounds of its superview?

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.

Upvotes: 5

Views: 2294

Answers (3)

kubacizek
kubacizek

Reputation: 577

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)
    }
}

Upvotes: 0

Efraim
Efraim

Reputation: 338

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)
    }
}

Upvotes: -1

Sandeep Baddula
Sandeep Baddula

Reputation: 181

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
    }
}

Upvotes: 6

Related Questions