Setting Slider Value to Set SeekToTime in AVPlayer - ios

I am using Player library, that is using AVPlayer & AVFoundation, which is quiet convenient for my case. I successfully managed to play the video and add a slider. I set the slider's min to 0 and max to duration of the video..
At this point, in order to connect slider to current playtime, I used this answer, on StackOverflow. I setup a protocol and used addPeriodicTimeObserverForInterval, so slider is linked to the currentTime and moving as video moves along successfully.
Now, here is my problem. I want to do the reverse and make - when slider moves, set currentTime of the video (forward,backward).
I made some research and I found out that it's seekToTime that I should link the slider.value (sender.value in my case) but, I couldn't understand how to apply it to Player. How can I make the slider to control the currentTime?
I am not sure if I am right, but as far as I understand, the first step should be:
protocol UserSetsTheTimeDelegate
{
func userSetsTheTime(result: Int)
}
class ViewController: UIViewController, PlayerDelegate {
...
func sliderValueDidChange(sender:UISlider!) {
print("value--\(sender.value)")
self.delegate?.userSetsTheTime(sender.value)
}
}
And add, I think I should add the UserSetsTheTimeDelegate in Player.swift, but I got confused here.
Update:
I realised that I can send the data to the Player.swift by using an instance in ViewController and adding a method in Player class. This is the first step. But now, as currentTime is getting updated (with modifying the slider) so quickly, it doesn't let me manipulate the slider value.

It is preferable from an UX point of view to call the seek() method on your Player after a touchUp on your slider, rather than whenever the value changes.
As you noticed, you had poor experience with the sliderValueDidChange.
Instead, try by adding a target/action on UIControlEvents.TouchUpInside (for instance) on your slider, then only seek. The experience should be better.
var slider : UISlider! {
didSet {
slider.addTarget(self, action: Selector("touchUp:"), forControlEvents: [UIControlEvents.TouchUpInside])
}
}
func touchUp(sender:AnyObject?) {
// player.seek(slider.currentValue)
}

I solved my problem. I created an instance of Player so I was able to send the data from the view controller back to Player class. Then I simply applied seekToTime(). At this point, visually, it was seeming poor but then I noticed that I can use stop(), setCurrentTime() and then play() in order to make it look nicer.

Related

What's the difference between UIView.animate() and UIButton.animate()?

I want to change the opacity of my button to half when pressed, and animate that change. So I was searching for how to do that and found this piece of code:
#IBAction func keyPressed(_ sender: UIButton) {
UIView.animate(withDuration: 0.3)
{
sender.alpha = 0.5
}
I got curious as to why we called the animate function on a UIView not on a UIButton, as what we want to animate is, specifically, a UIButton. So I tried UIButton.animate() and to my eyes it gives the same result with the animation.
So what's the difference? Is there a reason the person posting this code preferred using UIView.animate() over UIButton.animate()?
The animate function is a class level function of UIView so it is common to use it as UIView.animate....
Since UIButton ultimately extends UIView, using UIButton.animate... also works. You could even use UIScrollView.animate..., for example, in your UIButton code. Obviously that would be confusing but it would work.
In any code within a UIView subclass, using Self.animate... also works.
But the basic answer to your question is that people use UIView.animate... because animate is defined in the UIView class.

Would setting a UIView property explicitly stop/finish an existing animation which was started using UIViewPropertyAnimator on the same property

My current level of iOS dev knowledge is that I am still very new to it and in the learning stage. Giving this info so that experts can answer accordingly :)
As per the Stanford's course for iOS development it was said that an animation for a UIView property would be interrupted if another animation starts for the same property (more specifically the new animation starts working on one of the properties of the previous animation).
Link to the exact point in the video where this was taught https://youtu.be/84ZhYhAwYqo?t=209
The same rather similar thing was also said as an answer to a question on stackoverflow also. Direct link to answer https://stackoverflow.com/a/3076451/8503233
But when I tried this it didn't happen.
To test this I made a simple app where a button click started animating a view using UIViewPropertyAnimator.runningPropertyAnimator, where it changed the alpha to 0. Duration was set to 10 seconds so that I had plenty of time to trigger another animation.
Then another button will start an animation changing the alpha of the same view to 1. I expected that when the second animation will start, it will cause the first animation to stop and its completion handler to be called with the value of UIViewAnimatingPosition as .current.
But what I found that even when the second animation started, the first animation was still running and after running for its full duration, the completion handler was called with UIViewAnimatingPosition as .end.
This is absolutely opposite to what I read in the sources I gave above. Please help. What exactly is happening here. Will share the app code if asked for. Thanks!!!
EDIT1:
View controller code
class MyViewController: UIViewController {
#IBOutlet weak var viewToAnimate: UIView!
#IBOutlet weak var statusView: UIView!
#IBAction func buttonAnimate1(_ sender: UIButton) {
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 5, delay: 0, options: [], animations: {
self.viewToAnimate.alpha = 0.1
}, completion: {position in
switch position {
case .start:
print("In animation 1 completion block with position = start")
case .current:
print("In animation 1 completion block with position = current")
case .end:
print("In animation 1 completion block with position = end")
}
self.statusView.backgroundColor = UIColor.brown
})
}
#IBAction func buttonAnimate2(_ sender: UIButton) {
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 5, delay: 0, options: [.beginFromCurrentState], animations: {
self.viewToAnimate.alpha = 1
}, completion: {position in
switch position {
case .start:
print("In animation 2 completion block with position = start")
case .current:
print("In animation 2 completion block with position = current")
case .end:
print("In animation 2 completion block with position = end")
}
self.statusView.backgroundColor = UIColor.green
})
}
#IBAction func buttonRestore(_ sender: UIButton) {
viewToAnimate.alpha = 1
statusView.backgroundColor = UIColor.yellow
}
}
Test Steps:
In the simulator I press Animate1 button and then after about 1 second, I press Animate2 button.
Output on console:
In animation 1 completion block with position = end
In animation 2 completion block with position = end
In the comments from #matt, what's told is that animations are now additive. This clarifies things a lot but Apple's documentation for addAnimations(_:delayFactor:) says and I quote
If the animation block modifies a property that is being modified by a different property animator, then the animators combine their changes in the most appropriate way. For many properties, the changes from each animator are added together to yield a new intermediate value. If a property cannot be modified in this additive manner, the new animations take over as if the beginFromCurrentState option had been specified for a view-based animation.
So if a new animation takes over as beginFromCurrentState either implicitly or explicitly, then shouldn't the first animation stop with the state as .current when the new animation takes over?
You are consulting ancient history (the answer you cite is from 2010!). It used to be true that with view animation, ordering an animation on a view property already being animated would cancel the existing animation suddenly. But in more recent versions of iOS, view animations have been additive, meaning that a new animation will, by default, be combined smoothly with an existing in-flight animation.

Is there a way to observe changes to fractionComplete in UIViewPropertyAnimator

I've been looking at the very cool new UIViewPropertyAnimator class in iOS 10. It lets you easily do things like pause, resume, and reverse in-flight UIView animations. It used to be that you had to manipulate the underlying CAAnimations the system created in order to do that with UIView animations.
The new UIViewPropertyAnimator class has a property fractionComplete. It varies from 0.0 (beginning of animation) to 1.0 (end of animation.) If you change the value, you can "scrub" an animation from beginning to end.
I have a demo project on Github called KeyframeViewAnimations that lets you use a slider to scrub UIView and CAAnimations back and forth using fairly arcane CAAnimation tricks. As the animation runs, the slider moves from beginning to end to show the progress of the animation. That project was written in Objective-C before Swift was released, but the underlying techniques are the same in Objective-C or Swift.
I decided to create an equivalent project using UIViewPropertyAnimator. There are some quirks, but it's fairly straightforward. One thing I could not figure out how to do cleanly was to observe the progress of the animation's fractionComplete property so that I could update the slider as the animation progresses. I tried setting up KVO (key-value-observing) on the fractionComplete property, but it doesn't trigger a KVO notification until the animation is complete.
What I ended up doing is setting up a timer that runs on a very small interval and queries the UIViewPropertyAnimator's fractionComplete property repeatedly and updating the slider as it progresses. It works, but it's not a very clean way of doing things. It would be much better if there was a way to get notified about the progress of the animation.
I'm hoping there's something I'm missing.
You can see the UIViewPropertyAnimator based project on Github at this link: UIViewPropertyAnimator-test.
The code that gets the progress value from the animator is this code:
func startTimer(_ start: Bool) {
if start {
timer = Timer.scheduledTimer(withTimeInterval: 0.02,
repeats: true) {
timer in
var value = Float(self.viewAnimator.fractionComplete)
if self.animatingBackwards {
value = 1 - value
}
self.animationSlider.value = value
}
}
else {
self.timer?.invalidate()
}
}
I should probably convert the code above to use a CADisplayLink timer instead of a vanilla Timer (NSTimer) but I'm hoping there's a better way.
FYI, after researching this, the answer appears to be that no, there is no way to get updates as your animation progresses. You have to set up a timer (Either a Timer or a CADisplayLink and update your progress value each time it fires.
Did you try subclassing UIViewPropertyAnimator and then overriding the fractionComplete property?
class MyPropertyAnimator : UIViewPropertyAnimator {
override var fractionComplete: CGFloat {
didSet {
print("didSetFraction")
}
}
}

Get update function speed

I'm making a game that uses the update func to move an SKSpriteNode's position.y down by 6 pixels every time it runs.
It works correctly for the first game, but then I go to the app's shop in another view controller. When I try to go back to the game after visiting the shop, it appears that the update func is running twice as fast.
Heres the code:
override func update(currentTime: CFTimeInterval) {
roadStraight.position.y -= 6.0
}
Is there a way to get the firing speed of the update func so I can keep it constant? Thanks
A few things:
Use the time difference between the last calling of the function as a coefficient for your movement
Use a global speed constant which lets you easily adjust the speed of all animations without needing to search within the functions
I would do something like this (I hope you're using Swift 2.0, it's much more beautiful like this):
let speed = 0.01
var prev : CFTimeInterval!
override func update(currentTime: CFTimeInterval) {
defer { prev = currentTime }
guard prev != nil else { return }
let dt = currentTime - prev
roadStraight.position.y -= speed * dt
}
Well, as you can see the update: function takes an argument of the type CFTimeInterval. This gives the time that elapsed from the start. Thus, you could create a property that you could use within update: to check the time difference each time update: is called. However, update: function is an integral part of the Sprite-kit's frame rendering so you can't simply put limits on it. Sprite-kit runs in this sequence to render every frame:
And, as you probably already know the frames-per-second or fps counter on the bottom right of the screen is far from constant. If you need to execute or run things at a constant rate your best choice is to do it out of the update: method or, if it is absolutely necessary then you could get the time difference like I described before and multiply it by a certain constant to vary the magnitude of whatever movement or positioning you are performing.

Loop activated on mouse movement

I'm just going to explain the context so it is clearer.
I made this menu : my menu
I am looking to make an improved and more advanced version of the same menu.
I made an animation of waves on the cofee's surface and am looking to make it loop when the mouse is moving and to stop looping when it's not.
Sorry for the lack of specifications as I am quite new to actionscript, but I hope somebody will be able to help me. :)
Thanks,
Mathieu
Well, you said it - leverage MouseEvent.MOUSE_MOVE to set a conditional in your looping routine.
private var _isMoving:Boolean = false;
stage.addEventListener(MouseEvent.MOUSE_MOVE, checkMouse);
this.addEventListener(Event.ENTER_FRAME, doLoop);
private function checkMouse(e:MouseEvent):void
{
_isMoving = true;
}
private function doLoop(e:Event):void
{
trace("moving =" + _isMoving);
if(_isMoving)
{
// loop animation
}
_isMoving = false;
}
depending on how you want it to work I would do this as follows:
create an animation of wavy coffee
ensure the animation loops
note that clips loop by default, so all you have to do is match the first and last frames!
place the clip at the edge of your current coffee graphic
double click on the graphic to edit it
drag an instance of the looping animation from the library onto the "edge" of the graphic
OR just replace your entire light brown graphic with an animated one that loops
when the mouse is moving, call play on the animated loop clip
when the mouse stops, call stop on the animated loop clip
Some example code would be along the lines of:
public function init():void {
menuClip.addEventListener(MouseEvent.MOUSE_OVER, onMenuRollOver);
menuClip.addEventListener(MouseEvent.MOUSE_OUT, onMenuRollOut);
}
public function onMenuRollOver(event:MouseEvent):void {
stage.addEventListener(MouseEvent.MOUSE_MOVE, onMove);
/* do the stuff you're currently doing to animate the clip here.
something like: coffee graphic height = ease to mouseHeight */
}
public function onMenuRollOut(event:MouseEvent):void {
/* do the stuff you're currently doing to stop the clip here. */
stage.removeEventListener(MouseEvent.MOUSE_MOVE, onMove);
coffeeClip.stop();
}
public function onMove(event:MouseEvent):void {
resetTimer();
coffeeClip.play(); //note: play has no effect when movie is playing (that's ideal in this case)
}
public function resetTimer():void {
if(mouseMovementTimer == null) createTimer();
mouseMovementTimer.reset();
mouseMovementTimer.start();
}
public function createTimer():Timer {
mouseMovementTimer = new Timer(DELAY, 1); //fiddle with the delay variable. Try 500, at first
mouseMovementTimer.addEventListener(TimerEvent.TIMER, stopAnimationLoop);
}
public function stopAnimationLoop(event:TimerEvent):void {
mouseMovementTimer.removeEventListener(TimerEvent.TIMER, stopAnimationLoop); //optional but recommended
mouseMovementTimer = null;
coffeClip.stop();
}
Of course, you would need to do things like call init() and import flash.utils.Timer and initialize variables like mouseMovementTimer, menuClip, coffeeClip and DELAY.
Warning: This code is off the top of my head and untested. So there's likely to be small bugs in it but you should get the general idea:
add a mouse listener when the user mouses over the menu
remove that listener if the user mouses out of the menu
have that listener play the looping movie clip
trigger an event that will stop the looping clip if movement hasn't been detected in a while
once the trigger goes of, stop the clip
The key is in detecting when the mouse stops moving. Flash detects interaction well but does not detect NON-INTERACTION for obvious reasons. One easy way to solve that is to trigger a timer that will go off once too much time has elapsed since the last activity. Then, when the timer triggers, you know action has stopped!
I think that's the key piece to solving your problem. I hope that helps someone in some way.
~gmale

Resources