Touch Handling in Non-Rectangular Views.

Making a UIView look non-rectangular is pretty straightforward, but UIKit’s standard behavior is to treat all views as rectangular when it comes to touch event delivery.

In this example, we want our view to look like an oval inscribed in its bounds rectangle. And only respond to touches that fall in said oval.

We could override hitTest(), but the only thing we want to change is how it determines whether a touch event falls inside the self view or not.

func hitTest(_ point: CGPoint, withEvent event: UIEvent?) -> UIView?

Returns the farthest descendant of the receiver in the view hierarchy (including itself) that contains a specified point.

Luckily, the documentation for hitTest() states that it accomplishes its task by calling another public method - pointInside() - which is the perfect override point as its only responsibility is what we want to modify.

func pointInside(_ point: CGPoint, withEvent event: UIEvent?) -> Bool

Returns a Boolean value indicating whether the receiver contains the specified point.

All we need to do now is write the code that determines whether the given point is inside our oval shape. Again, we’re in luck: UIBezierPath has a containsPoint() method that will let you save your amazing trigonometry skills for another day.

func containsPoint(_ point: CGPoint) -> Bool

Returns a Boolean value indicating whether the area enclosed by the receiver contains the specified point.

All this luck turns our override into a one-liner:

public override func pointInside(point: CGPoint, withEvent _: UIEvent?) -> Bool {
  return UIBezierPath(ovalInRect: bounds).containsPoint(point)
}

This can easily be adapted to any shape that UIBezierPath can express.

Overriding pointInside() is also useful in other circumstances where the touch target you desire doesn’t exactly match your view’s bounds.

Consider a small view that acts like a handle that can be dragged around, like UISlider’s thumb. If the touch target was the same size as the handle as it appears, it would be pretty hard to grab. You might be tempted to increase the view’s size and only draw the small handle in its center, but adding some “touch padding” by overriding pointInside() is a cleaner solution:

override func pointInside(point: CGPoint, withEvent _: UIEvent?) -> Bool {
  return bounds.insetBy(dx: -25, dy: -25).contains(point)
}

Seriously, how awesome is Swift’s version of CGGeometry?

Here’s the full class from our oval example, designable and inspectable, ready to be dropped into your project.

import UIKit


@IBDesignable public class OvalBackgroundView: UIView {
  @IBInspectable public var shadowOffset: CGSize = .zero { didSet { setNeedsDisplay() } }
  @IBInspectable public var shadowOpacity: Float = 0.15 { didSet { setNeedsDisplay() } }
  @IBInspectable public var ovalColor: UIColor = .whiteColor() { didSet { setNeedsDisplay() } }
  
  private var ovalBackgroundLayer: CAShapeLayer { return layer as! CAShapeLayer }
  
  public static override func layerClass() -> AnyClass { return CAShapeLayer.self }
  
  public override func drawRect(rect: CGRect) {
    ovalBackgroundLayer.shadowOffset = shadowOffset
    ovalBackgroundLayer.shadowOpacity = shadowOpacity
    ovalBackgroundLayer.fillColor = ovalColor.CGColor
    
    super.drawRect(rect)
  }
  
  public override func layoutSubviews() {
    ovalBackgroundLayer.path = UIBezierPath(ovalInRect: ovalBackgroundLayer.bounds).CGPath
    ovalBackgroundLayer.shadowPath = ovalBackgroundLayer.path
    
    super.layoutSubviews()
  }
  
  public override func pointInside(point: CGPoint, withEvent _: UIEvent?) -> Bool {
    return UIBezierPath(ovalInRect: ovalBackgroundLayer.bounds).containsPoint(point)
  }
}

Questions? Let me know, glad to help.