2

So I have quite a simple task: animate a view to appear (height goes from zero to full height), then it has to disappear animatedly (collapse, height goes to zero). It looks like this: enter image description here

Implementation is very simple: we give the view a fixed height constraint, then animate it 0->fullHeight, fullHeight->0.

    notificationView.heightConstraint.constant = intrinsicHeight
    UIView.animateWithDuration(0.25, animations: {
        view.layoutIfNeeded()
    }) { (finished) in
        guard finished else { return }
        notificationView.heightConstraint.constant = 0
        UIView.animateWithDuration(0.25, delay: 3, options: .AllowUserInteraction, animations: {
            notificationView.superview!.layoutIfNeeded()
        }, completion: nil)
    }

The trouble is that a UIButton contained in the notification doesn't receive any touches because notification's height is set to 0. I guess, an alternative would be to use NSTimer or dispatch_after, but that would make things a little bit more complicated and I don't want that.

The question is: can I preserve user interaction using current approach?

xinatanil
  • 1,085
  • 2
  • 13
  • 23

2 Answers2

1

You are correct. The animation is updating the model values for your button's frame as soon as you call animateWithDuration so even though your button is still on screen it is only responding to hits in an area that is off screen. What you want is to do hit testing on the presentation layer of the button, which will have the value of the button's frame mid-flight.

One option is to subclass UIButton and override hitTest like:

override func hitTest(point: CGPoint, withEvent event: UIEvent!) -> UIView! 
{
    let superviewPoint = convertPoint(point, toView: superview)
    let point = layer.presentationLayer.convertPoint(superviewPoint,
                                           fromLayer: superview.layer)
    return super.hitTest(point, withEvent: event)
}

This is what Apple suggests in this video.

Another suggestion here, is to override touchesEnded or touchesBegan in your view controller and explicitly call hitTest on the presentationLayer of your button like so:

    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?)
    {
        guard let touch = touches.first else{ return }
        let point = touch.locationInView(self.notificationView)
        if ((self.button.layer.presentationLayer()?.hitTest(point)) != nil)
        {
            // We're touching where the button is presented, so call your action
            self.buttonPress(self.button) 
        }
    }

This has the drawback of your button not highlighting on touch down.

For these to work you need the .AllowUserInteraction option enabled on your animation, which you are already doing.

Community
  • 1
  • 1
beyowulf
  • 15,101
  • 2
  • 34
  • 40
0

Another option is to add the 3 second delay using Grand Central Dispatch, instead of specifying it in the animation:

notificationView.heightConstraint.constant = intrinsicHeight
UIView.animateWithDuration(0.25, animations: {
    view.layoutIfNeeded()
}) { (finished) in
    guard finished else { return }
    notificationView.heightConstraint.constant = 0
    delay(3) {
        UIView.animateWithDuration(0.25, delay: 0, options: .AllowUserInteraction, animations: {
            notificationView.superview!.layoutIfNeeded()
        }, completion: nil)
    }
}

where delay is a helper function defined as:

func delay(delay:NSTimeInterval, _ f:()->()) {
    dispatch_after(
        dispatch_time(
            DISPATCH_TIME_NOW,
            Int64(delay * Double(NSEC_PER_SEC))
        ),
        dispatch_get_main_queue(), f)
}
John Gibb
  • 10,603
  • 2
  • 37
  • 48