How to do interactive transition + ending animation? - ios

Normally, we can do an interactive transitioning with animateTransition of UIViewControllerAnimatedTransitioning and updating progress via UIPercentDrivenInteractiveTransition.
Question:
How to have the interactive transitioning at first, then as we pass a certain threshold, perform a different ending animation?
What I want to achieve here is something like dismissing App store's Today card (https://gph.is/2qgcGHd). We can interactively shrink the card by panning a left edge of the screen. Then when it reaches the point, the card animates back to home page without any interactivity. It seems like a combination of interactive + animate transition to me.
What I've tried:
I tried doing this in UIView.animateWithKeyFrames by dividing into two parts of animation with 0.5 relative times for each. Then as the progress reach 0.5, I call finish() (of UIPercentDrivenInteractiveTransition) to have the second animation performing. It has some glitches there and it's like a hack. Want to know if there's a better way to do this.

In the end, I use UIView.animateKeyFramesand dividing the interactive transition into two-part animation (as explained in the question):
let progressUntilDismissing = 0.4
UIView.animateKeyframes(withDuration: 0.5, delay: 0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0,
relativeDuration: progressUntilDismissing,
animations: {
// interactive dismissing animation...
})
UIView.addKeyframe(withRelativeStartTime: progressUntilDismissing,
relativeDuration: (1 - progressUntilDismissing),
animations: {
// closing dismissing animation...
})
}) { (finished) in
//...
}
Then in the pan gesture recognizer, I calculate the pan progress and determine if it passes progressUntilDismissing or not.
If yes, call finish() on UIPercentDrivenInteractiveTransition subclass, it will animate the closing dismissing animation automatically.
In case anyone is curious, this is what I'm playing with:
AppStoreTodayInteractiveTransition

Related

Haptic feedback during animation - mimic UIPickerView

I created the circle with deck of cards, that user can spin to select one. After the paning ends, it snaps to designated angle with a nice deceleration animation. In future there will some kind of indication that the card at 45 degrees is the selected one.
I would like to indication that the selection changed with haptic feedback, just like in UIPickerView. For now I am trying to add haptic feedback to the deceleration animation. My idea was to make feedback generator and call .selectionChanged() in animation as many times as number of cards that were skipped. But for now I decided to just simply call it 3 times. Unfortunately, none of my ideas work - even creating a separate UIViewPropertyAnimator does not work. I suppose I should only put animatable properties in animation closure. The animator itself works right - the deceleration animation works.
animator.addAnimations {
UIView.animateKeyframes(withDuration: 5.0, delay: 0.0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 1.0/3.0, relativeDuration: 0.0, animations: {
self.selectionGenerator.selectionChanged()
})
UIView.addKeyframe(withRelativeStartTime: 2.0/3.0, relativeDuration: 0.0, animations: {
self.selectionGenerator.selectionChanged()
})
UIView.addKeyframe(withRelativeStartTime: 3.0/3.0, relativeDuration: 0.0, animations: {
self.selectionGenerator.selectionChanged()
})
})
}
animator.startAnimation()
How can I mimic the haptic feedback behaviour of for example DatePicker , which vibrates when selection changes?
you can add haptic feedback using
HAPTICA https://github.com/efremidze/Haptica
and add this feedback in animation loop i hope it will work... :)

Building Card-Highlighting-Animation as in the App-Store

In the AppStore (iOS 11) on the left "today"-tab, there are several card views. If you highlight one, it shrinks a little bit. How can I rebuild this animation?
I guess changing the constraints of the card view during an animation will not be what we need, since you would also have to adapt all the other constraints (e.g. of the labels) to match the new size.
Is there an easier way to shrink a view with all its subviews?
Also, when you click the card, it increases to fullscreen with an animation. Do you have any ideas how to achieve this effect?
For tapping and shrinking card, I also wrote about this in detail. Here's the idea:
Use a scaling transform to animate shrinking (like in accepted answer)
Disable delaysContentTouch to make it shrink faster upon touch
(scrollView.delaysContentTouch = false)
Always allow users to scroll using .allowUserInteraction animation option:
UIView.animate(withDuration: 1.0,
delay: 0.0,
options: [.allowUserInteraction],
animations: ...,
completion: ...)
(By default when you use transform, it disables the interaction a bit. User can't scroll successively without doing that)
About the expanding to full screen with animation, I have tried to replicate it with the native's transition APIs which you can check out here: https://github.com/aunnnn/AppStoreiOS11InteractiveTransition
In short, I use UIViewControllerAnimatedTransitioning to do custom animation. Hide the original card and create a new dummy card view just for animation. Then setup AutoLayout constraints of that card, including 4 to each of the screen edges. Then animate those constraints to make it fill the screen.
After everything is done, hide that dummy view and show the destination detail page.
Note: The exact implementation detail is a bit different and involved.
You can get an easy scale animation using transform:
UIView.animate(withDuration: 0.2) {
view.transform = CGAffineTransform.identity.scaledBy(x: 0.9, y: 0.9)
}
As to the fullscreen animation, you want to check out some tutorials on how to create custom transition animations.
If you are interested in a more complete functionality you can use this library:
https://github.com/PaoloCuscela/Cards
this is also a good rebuild of that animation when you press a card:
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: .beginFromCurrentState, animations: {
self.transform = .init(scaleX: 0.95, y: 0.95)
}, completion: nil)

UIView constraints won't animate if layoutIfNeeded() is called

I'm trying to animate a view that is constrained (iconVerticalConstraint) to be at center Y (+100) of a superview, specifically reaching its right (0) position after an animation.
Here's my code:
self.view.layoutIfNeeded()
UIView.animateKeyframes(withDuration: 2, delay: 0, animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.25, animations:{
self.iconVerticalConstraint.constant -= 20
})
UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.75, animations:{
self.iconVerticalConstraint.constant -= 80
})
})
I tried following this question's answer but that didn't help, because in my case my view contains more subviews, intrinsically linked, and putting layoutIfNeeded() in my animation block would animate the whole view, and not just that constraint. Otherwise this approach does no animation at all.
changing constraints such as
self.iconVerticalConstraint.constant -= 20
should be OUTSIDE the animation block and inside you just call
self.view.layoutIfNeeded()
I think because you are trying to do 2, and calling
self.view.layoutIfNeeded()
They all happen once, consider changing to:
self.iconVerticalConstraint.constant -= 20
UIView.animate(withDuration: 0.25, animations: {
self.view.layoutIfNeeded()
}) { (finished) in
self.iconVerticalConstraint.constant -= 80
UIView.animate(withDuration: 0.75, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
Edit: Explanation
When you change a constraint, the view will not update until the next layout cycle, usually when you exit your current scope. So when you want to animate a constraint, you change the constant BEFORE the animation block, then inside the animation block you call layoutIfNeeded() This tells it to animate the now forced layout cycle.
Now in your example, you where calling layoutIfNeeded() which would do nothing at that point, then changing 2 constants, without telling the layout to animate, so when the scope exited, the UI would layout without animation.
In your case you want to chain the changing of 2 constants, but you dont have a way of changing the second constant (80) after the first animation completes, and if you set both, both constants will be adjusted on the FIRST layoutIfNeeded() hence, the chaining of animations, which will still work with your other animations that you omitted, just add then in the first animation block if they are in the first keyframe and the second if they were in the second.
Final Edit:
Also you can change your layoutIfNeeded() currently applied to self.view to actually on the views it affects, in this case iconVerticalConstraint if it was a view that is attached to another view say a top attached to a view bottom, if you wanna animate, you must call layoutIfNeeded() on each view it affects
After hours of time spent to understand the basic mechanism, I can provide my own answer, for all of you who will find themselves in my situation.
layoutIfNeeded() tells to the view to adjust position of all of its subviews.
animate or animateKeyframes or every other alternative you're using, instead, tells to the view to perform any of the instructions inside the animation block in the way you specify in the animate function (by default, with a options: .easeInOut)
Back to the question: the problem was in the fact that animate and layoutIfNeeded are called asynchronously so that, in fact, every instruction inside the animation block was executed just before the layoutIfNeeded instruction, making it useless. The solution isn't putting the layoutIfNeeded instruction inside the animation block, as suggested by SeanLintern88 because in this way we would tell the view to adjust the position of its subviews according to the animation options. That's the key point.
The answer is somewhat simple. Move the animation part on the subview itself, specifically in a point when you know that the superview has already been "autolayouted" (e.g. viewDidAppear), so that the constraint is already defined and executed, and you can now change it. In that point, code should look like this:
UIView.animateKeyframes(withDuration: 2, delay: 0, animations: {
self.view.layoutIfNeeded()
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.25, animations:{
self.iconVerticalConstraint.constant -= 20
})
UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.75, animations:{
self.iconVerticalConstraint.constant -= 80
})
})
Because you want to animate the autolayout.
Another option (for instance, if you haven't/don't want to create a class for your subview) is to change bounds/center of the subview itself in the viewDidAppear function of the parent view. This is because, if you are in the parent view, you know that at that point auto layout has already been registered but not executed, in the more precise sense that bounds and center have been already changed according to the auto layout.
If you then change them, you'll obtain your desired animation (but remember: auto layout is still on, so you must change the constraints in a way that reflects the final position of your animation; if you wish, you can do that in the completion part of animate function).

Can I force an element to finish it's animation immediately?

Here I have some code to show a UIView with a label as a notification.
self.not1cons.constant = 0
self.notificationLbl1.text = self.notification1
UIView.animate(withDuration: 2.5, delay: 0.3, options: .allowAnimatedContent, animations: {
self.view.layoutIfNeeded()
}, completion: { finsihed in
self.not1cons.constant = -100
UIView.animate(withDuration: 2.5, delay: 2.0, options: .allowAnimatedContent, animations: {
self.view.layoutIfNeeded()
}, completion: { finshed in
})
})
It start off-screen and descends in to view. It stays in place for a few seconds and returns to its original position off-screen. I need some code to make these chained animations happen instantly. Is this possible?
You could probably accomplish this by manipulating the CAAnimations the system generates behind the scenes, but that is fairly tricky business, and not a great idea since it relies on undocumented details of the current implemention, which is risky.
Instead I'd suggest reworking your animations to use the new-to-iOS 10
UIViewPropertyAnimator, which has support for pausing and resuming animations, as well as scrubbing them back and forth to arbitrary points.
I have a demo project on Gitub called UIViewPropertyAnimator-test that lets you scrub an animation back and forth using a slider. It is more complex than your need, but it should give you the idea.

Difficulty allowing user interaction during UIView animation

I'm struggling to figure out how to allow user interaction with a view as it's being animated.
Here's the situation: I have a UIView cardView which holds card subviews. The cards are draggable tiles, similar to how the cards in Tinder are draggable/swipeable.
I am trying to fade out the card using animateWithDuration by animating to cardView.alpha = 0. Logically, this will also fade out all of the subviews (card objects). In this specific case, I am only targeting one card subview. However, during the animation, I am unable to drag/interact with the card.
Here is the code I'm using:
UIView.animateWithDuration(
duration,
delay: 0,
options: UIViewAnimationOptions.AllowUserInteraction,
animations: {self.cardView.alpha = 0}
) {
_ in
println("Card faded out")
card.removeFromSuperview()
}
Why doesn't this work? Any help will be appreciated. Thank you!!
I think you can find the answer in this previous post.
The interesting bit of the post is:
UIView's block animation by default blocks user interaction, and to get around it you need to pass UIViewAnimationOptionAllowUserInteraction as one of the options.
I fixed this problem by setting alpha to 0.1 instead of 0.0. I'm not sure if that will work in your case, but it shows that the event handling code thought that the view was not visible and disabled interaction even with the UIViewAnimationOptionAllowUserInteraction flag set. Oddly, setting the alpha to 0.01 did not work, so there is a threshold of visibility you have to stay above.
Swift 5
UIView.animateKeyframes(withDuration: 0.5, delay: 0, options: [.repeat, .autoreverse, .allowUserInteraction], animations: {
self.customButton.backgroundColor = .none
}, completion: nil)
The issue is with the Alpha value of 0. Alpha values of a certain proximity to Zero will remove the view from the view responder hierarchy. The fix here is to make the alpha setting to this:
self.cardView.alpha = 0.011
The view will still be invisible but not removed from the responder chain. From my testing the minimum amount is the following:
extension CGFloat {
static let minAlphaForTouchInput: CGFloat = 0.010000001
}

Resources