Search code examples
iosswiftcocoa-touchframecalayer

iOS: Unable to detect touch outside the bounds of CALayer


I'm creating a reusable multi state switch extends from UIControl that looks similar to the default UISwitch, with more than two states. The implementation is like adding CALayers for each of the state and one additional Layer for highlighting the selected state on the UIView.

The UI looks like this

multi state switch

The problem is that I could not recognize touch of a state when the user tapped just outside the squared border as indicated in the image. There is a simple convenience method (getIndexForLocation) I have added to return the index of selected state, given the touch point. With the returned selected index I displace the highlight indicator's position to move on center of the selected state.

func getIndexForLocation(_ point: CGPoint) -> Int {
    var index = selectedIndex
    let sLayers = self.controlLayer.sublayers
    for i in 0..<totalStates {
        if sLayers![i].frame.contains(point) {
            index = i
        }
    }
    return index
}

The above method is called from endTracking(touch:for event) method of UIControl with the last touch point.

How can I change this method, so that I can get index of the touched/selected state, even If the user has touched around the image. The touch area can be approximately the area of highlight circle placed on top of the center of the state image.

self.controlLayer is the container Layer whose sublayers are all the states and the highlight indicator.

The method that adds does position animation with selected index is provided below

func performSwitch(to index: Int) -> () {
    guard selectedIndex != index else {
        return
    }

    let offsetDiff = CGFloat((index - selectedIndex) * (stateSize + 2))
    let oldPosition = indicatorPosition
    indicatorPosition.x += offsetDiff
    let newPosition = indicatorPosition

    let animation: CABasicAnimation = CABasicAnimation(keyPath: "position")
    animation.timingFunction = CAMediaTimingFunction(controlPoints: 0.785, 0.135, 0.15, 0.86)
    animation.duration = 0.6
    animation.fromValue = NSValue(cgPoint: oldPosition!)
    animation.toValue = NSValue(cgPoint: newPosition!)
    animation.autoreverses = false
    animation.delegate = self
    animation.isRemovedOnCompletion = false
    animation.fillMode = kCAFillModeForwards


    self.selectedIndex = index
    self.stateIndicator.add(animation, forKey: "position")
}

Any help would be greatly appreciated.


Solution

  • You are testing by saying if sLayers![i].frame.contains(point). The simplest solution, therefore, is make the frame of each "button" much bigger — big enough to encompass the whole area you wish to be touchable as this "button". Remember, the drawing can be small even if the frame is big.

    Also, just as an aside, your code is silly because you are basically performing hit-testing. Hit-testing for layers is built-in, so once you have your frames right, all you have to do is call hitTest(_:) on the superlayer. That will tell you which layer was tapped.

    (Of course, it would be even better to use subviews instead of layers. Views can detect touches; layers can't.)