How to perform action on RealityKit animation completion - swift

In WWDC19 talk they showed how to animate Entity and perform action on animation completion (go to minute 12 for reference) like this:
let animationController = move(to: flipUpTransform, relativeTo: parent, duration: 0.25, timingFunction: .easeInOut)
animationController.completionHandler {
// Perform some action
}
But it looks like that they removed or never added this completionHandler. Instead they have isCompleted boolean property.
I am new to swift so I have no idea to use this isCompleted to perform some actions upon animation completion. How can I solve this?

The documentation says:
Look for one of the events in AnimationEvents if you want to be alerted when certain aspects of animation occur.
So you can use Combine to get notified when the animation finishes. so
let animation = entity.move(to: targetTransform, relativeTo: nil, duration: 1)
arView.scene.publisher(for: AnimationEvents.PlaybackCompleted.self)
.filter { $0.playbackController == animation }
.sink(receiveValue: { event in
/ * your completion handler */
}).store(in: &subscriptions)

Related

RealityKit – move(to:) methods work only without duration parameter

Using move(to:) method to update Entity's position works only if I don't use the the initialiser with duration parameter.
sphere.move(to: newTransform, relativeTo: nil, duration: 0.75) // Absolutely no effect
sphere.move(to: newTransform, relativeTo: nil) // Instant effect
Both called from the Main thread. I don't understand what may cause this strange behaviour.
Implement move(...) method after arView.scene.anchors.append(scene), not before. When you run the following code you'll see that model moves along the +X axis during 2 sec, as expected.
let scene = try! Experience.loadBox()
guard let model = scene.steelBox?.children[0] as? ModelEntity
else { return }
var transform = Transform()
transform.translation.x = 0.5
arView.scene.anchors.append(scene)
DispatchQueue.main.async {
model.move(to: transform, relativeTo: nil, duration: 2.0)
}
However, if you do not use the duration parameter (i.e. duration = 0.0 sec), the model will instantly move 0.5 m along the +X axis.
DispatchQueue.main.async {
model.move(to: transform, relativeTo: nil)
}
I've just realised that marking the init method with #MainActor did not cause move(to:relativeTo:duration:) to be called on the main thread, executing it within DispatchQueue.main.async{} closure did the trick.

UIView.animate() issue

I have a code, where UI elements are changed after user interaction. I use UIView.animate() to refresh UI. The code is pretty simple:
UIView.animate(withDuration: 1.0) {
self.fadeIn()
} completion: { _ in
UIView.animate(withDuration: 1.0) {
self.fadeOut()
}
}
Very important point is that total animation duration from beginning to the end should be 2 seconds. But sometimes, there are no UI changes in fadeIn(), and UIView.animate() just get immediately to completion block. But in that option I want to have a delay for duration time (1 sec), and only after that get to completion block to fadeOut. How I can force animate with duration even if it's nothing to animate in animation block? Thanks in advance!
Maybe don't call fadeOut() in the completion block, and just make sure that it's called with one second delay after the fadeIn() is called?
UIView.animate(withDuration: 1.0) {
self.fadeIn()
}
UIView.animate(withDuration: 1.0, delay: 1.0) {
self.fadeOut()
}

Update Showing Frame of AVPlayer When Using AVPlayer.seek() Prior to AVPlayer.play()

I have an AVPlayer and when I get to the end of the duration, I want to go back to the beginning, but I don't want to restart the AVPlayer.
//Do something when video ended
NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying(note:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
#objc func playerDidFinishPlaying(note: Notification) {
self.restartVideo()
}
func restartVideo() {
self.player.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero)
}
From here, I can simply .play() the AVPlayer and it will play from the beginning. But, prior to me .play(), the .seek() function does not indicate to the user that it has been reset back to the beginning. The AVPlayer frame does not actually transition back to the first frame until AFTER I execute .play().
Question:
How can I get the AVPlayer to change the actual frame it shows to the user when using the .seek() functionality?
This part of your code is right. It could be something else calling it to seek to end??
You can use the completion handler to see if the seek was interrupted. And also set a break point to see what queue you're on, just to make sure you're on the main thread. If a second seek operation happens you will get a false result in the completion handler. Also, try setting a break point after the seek to check if the UI responded.
self.player.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero) { success in
print("seek finished success = ", success)
}
set like this
self.player.seek(to: .zero) // it will update the player slider to 0:00
if you want to set on specific time than use it like
// Seek to the 2 minute mark
let time = CMTime(value: 120, timescale: 1)
self.player.seek(to: time)

Swift Dynamic animation followed by property animator

I have an animation where I use a push animation, then a snap animation using UIDynamicBehavior, and then I finish with a property behavior:
for card in selectedCards {
removeCard(card: card)
}
private func removeCard(card: Card) {
guard let subView = cardsContainer.subviews.first(where: { ($0 as? PlayingCardView)?.card == card }) else {
return
}
if let card = subView as? PlayingCardView { card.selected = false }
let matchedCardsFrame = matchedCards.convert(matchedCards.frame, to: view)
view.addSubview(subView)
cardBehavior.addItem(subView) // here I add the push behavior
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.cardBehavior.removeItem(subView) // here I remove the push behavior
UIViewPropertyAnimator.runningPropertyAnimator(
withDuration: 0.3,
delay: 0,
options: [],
animations: {
self.cardBehavior.addSnapBehavior(subView, frame: matchedCardsFrame) // here I add the snap behavior
}, completion: { finished in
self.animator.removeAllBehaviors()
subView.frame.size = CGSize(width: matchedCardsFrame.height, height: matchedCardsFrame.width)
subView.transform = CGAffineTransform.identity.rotated(by: CGFloat.pi / 2)
subView.setNeedsDisplay()
})
}
}
Essentially the above code does the following:
Add push behavior
Remove push behavior
Add snap behavior
Remove all behaviors
Add property transform
What I want is for the push action to execute, then after a second or so, have the snap behavior execute, and after the snap execution is finished, to perform a transform. However, if I removeAllBehaviors() before I execute the property transform then the snap behavior doesn't finish. But if I leave the snap behavior and try to execute the property transform then it has no effect since it appears that the snap behavior acts on the object indefinitely, putting it at odds with the property transform.
How can I programmatically say finish the snap behavior and then perform the transform?

UIViewPropertyAnimator AutoLayout Completion Issue

I'm using UIViewPropertyAnimator to run an array interactive animations, and one issue I'm having is that whenever the I reverse the animations I can't run the animations back forward again.
I'm using three functions to handle the animations in conjunction with a pan gesture recognizer.
private var runningAnimations = [UIViewPropertyAnimator]()
private func startInteractiveTransition(gestureRecognizer: UIPanGestureRecognizer, state: ForegroundState, duration: TimeInterval) {
if runningAnimations.isEmpty {
animateTransitionIfNeeded(gestureRecognizer: gestureRecognizer, state: state, duration: duration)
}
for animator in runningAnimations {
animator.pauseAnimation()
animationProgressWhenInterrupted = animator.fractionComplete
}
}
private func animateTransitionIfNeeded(gestureRecognizer: UIPanGestureRecognizer, state: ForegroundState, duration: TimeInterval) {
guard runningAnimations.isEmpty else {
return
}
let frameAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1) {
switch state {
case .expanded:
// change frame
case .collapsed:
// change frame
}
}
frameAnimator.isReversed = false
frameAnimator.addCompletion { _ in
print("remove all animations")
self.runningAnimations.removeAll()
}
self.runningAnimations.append(frameAnimator)
for animator in runningAnimations {
animator.startAnimation()
}
}
private func updateInteractiveTransition(gestureRecognizer: UIPanGestureRecognizer, fractionComplete: CGFloat) {
if runningAnimations.isEmpty {
print("empty")
}
for animator in runningAnimations {
animator.fractionComplete = fractionComplete + animationProgressWhenInterrupted
}
}
What I've noticed is after I reverse the animations and then call animateTransitionIfNeeded, frameAnimator is appended to running animations however when I call updateInteractiveTransition immediately after and check runningAnimations, it's empty.
So I'm led to believe that this may have to do with how swift handles memory possibly or how UIViewAnimating completes animations.
Any suggestions?
I've come to realize the issue I was having the result of how UIViewPropertyAnimator handles layout constraints upon reversal.
I couldn't find much detail on it online or in the official documentation, but I did find this which helped a lot.
Animator just animates views into new frames. However, reversed or not, the new constraints still hold regardless of whether you reversed the animator or not. Therefore after the animator finishes, if later autolayout again lays out views, I would expect the views to go into places set by currently active constraints. Simply said: The animator animates frame changes, but not constraints themselves. That means reversing animator reverses frames, but it does not reverse constraints - as soon as autolayout does another layout cycle, they will be again applied.
Like normal you set your constraints and call view.layoutIfNeeded()
animator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1) {
[unowned self] in
switch state {
case .expanded:
self.constraintA.isActive = false
self.constraintB.isActive = true
self.view.layoutIfNeeded()
case .collapsed:
self.constraintB.isActive = false
self.constraintA.isActive = true
self.view.layoutIfNeeded()
}
}
And now, since our animator has the ability to reverse, we add a completion handler to ensure that the correct constraints are active upon completion by using the finishing position.
animator.addCompletion { [weak self] (position) in
if position == .start {
switch state {
case .collapsed:
self?.constraintA.isActive = false
self?.constraintB.isActive = true
self?.view.layoutIfNeeded()
case .expanded:
self?.constraintA.isActive = false
self?.constraintB.isActive = true
self?.view.layoutIfNeeded()
}
}
}
The animator operates on animatable properties of views, such as the frame, center, alpha, and transform properties, creating the needed animations from the blocks you provide.
This is the crucial part of the documentation.
You can properly animate:
frame, center, alpha and transform, so you would not be able to animate properly NSConstraints.
You should modify frames of views inside of addAnimations block