Currently I am writing a looping animation method. This method generates a new arc4random_uniform and then converts it to a CGFloat. These values are then inserted into the animateWithDuration function as so:
func randomAnimationForPostPackets() {
while true {
let randomCoordinatesXInt: UInt32 = arc4random_uniform(300)
let randCoordsX = CGFloat(randomCoordinatesXInt)
let randomCoordinatesYInt: UInt32 = arc4random_uniform(300)
let randCoordsY = CGFloat(randomCoordinatesYInt)
UIView.animateWithDuration(5, delay: 0, options: [.AllowUserInteraction], animations: {
self.postPacketView.center.x = randCoordsX
self.postPacketView.center.y = randCoordsY
}, completion: nil)
}
}
When attempting to loop this function in a while loop, the application crashes due to the constant generation of random numbers. How would I be able to implement a looped animation based on a new set of random coordinates each time?
Using a while loop will just call animations as fast as it can using all the CPU and you will eventually run out of memory.
animateWithDuration has a completion closure that is called when the animation is complete. Use this to set up the next animation.
func randomAnimationForPostPackets() {
let randomCoordinatesXInt: UInt32 = arc4random_uniform(300)
let randCoordsX = CGFloat(randomCoordinatesXInt)
let randomCoordinatesYInt: UInt32 = arc4random_uniform(300)
let randCoordsY = CGFloat(randomCoordinatesYInt)
UIView.animateWithDuration(5, delay: 0, options: [.AllowUserInteraction], animations: {
self.postPacketView.center.x = randCoordsX
self.postPacketView.center.y = randCoordsY
}) { (finished) in
self.randomAnimationsPostPacket()
}
}
#Paulw11 basically nailed it in the comments: Your call to animateWithDuration is async, i.e. it isn't "blocking" your loop for 5 seconds. Your loop is spinning wicked fast, like, a bajillion times before that first animate even gets a chance. In fact, I suspect your loop is taking ALL the resources such that the animate never even tries.
Related
I try to implement simple player with UISlider to indicate at what time is current audio file.
In code I have added two observers:
slider.rx.value.subscribe(onNext: { value in
let totalTime = Float(CMTimeGetSeconds(self.player.currentItem!.duration))
let seconds = value * totalTime
let time = CMTime(seconds: Double(seconds), preferredTimescale: CMTimeScale(NSEC_PER_SEC))
self.player.seek(to: time)
}).disposed(by: bag)
let interval = CMTime(seconds: 0.1, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
player.addPeriodicTimeObserver(forInterval: interval, queue: nil) { [weak self] time in
self?.updateSlider(with: time)
}
with one private function:
private func updateSlider(with time: CMTime) {
let currentTime = CMTimeGetSeconds(time)
var totalTime = CMTimeGetSeconds(player.currentItem!.duration)
if totalTime.isNaN {
totalTime = 0
}
startLabel.text = Int(currentTime).descriptiveDuration
endLabel.text = Int(totalTime).descriptiveDuration
slider.value = Float(currentTime / totalTime)
}
When audio plays, everything is fine and slider is pretty much updated. The problem occurs when I try to move slider manually while audio is playing, then it jumps. Why?
UPDATE:
I know why actually. Because I update it twice: manually and from player observer, but how to prevent from this behaviour? I have no idea;) please, help.
One simple way to go about this would be to prevent addPeriodicTimeObserver from calling self?.updateSlider(with: time) when the slider is being touched.
This can be determined via the UISliders isTracking property:
isTracking
A Boolean value indicating whether the control is currently tracking
touch events.
While tracking of a touch event is in progress, the control sets the
value of this property to true. When tracking ends or is cancelled for
any reason, it sets this property to false.
Ref: https://developer.apple.com/documentation/uikit/uicontrol/1618210-istracking
This is present in all UIControl elements which you can use in this way:
player.addPeriodicTimeObserver(forInterval: interval, queue: nil) { [weak self] time in
//check if slider is being touched/tracked
guard self?.slider.isTracking == false else { return }
//if slider is not being touched, then update the slider from here
self?.updateSlider(with: time)
}
Generic Example:
#IBOutlet var slider: UISlider!
//...
func startSlider() {
slider.value = 0
slider.maximumValue = 10
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] (timer) in
print("Slider at: \(self?.slider.value)")
guard self?.slider.isTracking == false else { return }
self?.updateSlider(to: self!.slider.value + 0.1)
}
}
private func updateSlider(to value: Float) {
slider.value = value
}
I'm sure there are other (better) ways out there but I haven't done much in RxSwift (yet).
I hope this is good enough for now.
I've got an AVAudioEngine setup with a AVAudioPlayerNode that is playing some background music.
I'm trying to find a best approach to create a volume fadeout on the node over a 2 second timeframe. I'm considering using CADisplayLink in order to do this. I was wondering if somebody had experience with this scenario and could advise me on their approach?
My approach is below. Note that I assign the timer to a member var so I can invalidate it at other points (viewWillDisappear, delloc, etc.). I was worried that it wouldn't sound smooth, but I tried it and it works fine, didn't need to use CADisplayLink.
- (void)fadeOutAudioWithDuration:(double)duration {
double timerInterval = 0.1;
NSNumber *volumeInterval = [NSNumber numberWithDouble:(timerInterval / duration)];
self.fadeOutTimer = [NSTimer scheduledTimerWithTimeInterval:timerInterval target:self selector:#selector(fadeOutTimerDidFire:) userInfo:volumeInterval repeats:YES];
}
- (void)fadeOutTimerDidFire:(NSTimer *)timer {
float volumeInterval = ((NSNumber *)timer.userInfo).floatValue;
float currentVolume = self.audioEngine.mainMixerNode.outputVolume;
float newValue = MAX(currentVolume - volumeInterval, 0.0f);
self.audioEngine.mainMixerNode.outputVolume = newValue;
if (newValue == 0.0f) {
[timer invalidate];
}
}
You can use global gain in EQ.
for example
AVAudioUnitEQ *Volume;
Volume = [[AVAudioUnitEQ alloc] init];
[engine attachNode:Volume];
[engine connect:Volume to:engine.outputNode format:nil];
And then
Volume.globalGain = /*here your floatValue*/
In case anyone like me still looking for an answer:
As from docs, AVAudioPlayerNode doesn't support volume property, only AVAudioMixerNode node does. So ensure you envelope your AVAudioPlayerNode into AVAudioMixerNode.
Here's a code used to fade in, fade out and generally fade (Swift 5)
typealias Completion = (() -> Void)
let mixer = AVAudioMixerNode()
func fade(from: Float, to: Float, duration: TimeInterval, completion: Completion?) {
let stepTime = 0.01
let times = duration / stepTime
let step = (to - from) / Float(times)
for i in 0...Int(times) {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * stepTime) {
mixer.volume = from + Float(i) * step
if i == Int(times) {
completion?()
}
}
}
}
func fadeIn(duration: TimeInterval = 1.3, completion: Completion? = nil) {
fade(from: 0, to: 1, duration: duration, completion: completion)
}
func fadeOut(duration: TimeInterval = 1.3, completion: Completion? = nil) {
fade(from: 1, to: 0, duration: duration, completion: completion)
}
I have a pulsing rectangle animated with animateWithDuration and setAnimationRepeatCount().
I'm trying to add a sound effect in the animations block witch is "clicking" synchronously. But the sound effect is only playing once. I can't find any hint on this anywhere.
UIView.animateWithDuration(0.5,
delay: 0,
options: UIViewAnimationOptions.AllowUserInteraction | UIViewAnimationOptions.CurveEaseOut | UIViewAnimationOptions.Repeat,
animations: {
UIView.setAnimationRepeatCount(4)
self.audioPlayer.play()
self.img_MotronomLight.alpha = 0.1
}, completion: nil)
The sound effect should play four times but doesn't.
Audio Implementation:
//global:
var metronomClickSample = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("metronomeClick", ofType: "mp3")!)
var audioPlayer = AVAudioPlayer()
override func viewDidLoad() {
super.viewDidLoad()
audioPlayer = AVAudioPlayer(contentsOfURL: metronomClickSample, error: nil)
audioPlayer.prepareToPlay()
....
}
#IBAction func act_toggleStartTapped(sender: AnyObject) {
....
UIView.animateWithDuration(....
animations: {
UIView.setAnimationRepeatCount(4)
self.audioPlayer.play()
self.img_MotronomLight.alpha = 0.1
}, completion: nil)
}
Providing the UIViewAnimationOptions.Repeat option does NOT cause the animation block to be called repeatedly. The animation is repeated on a CoreAnimation level. If you place a breakpoint in the animation block, you'll notice that it is only executed once.
If you want the animation to execute in a loop alongside the sound, create a repeating NSTimer and call the animation / sound from there. Keep in mind that the timer will retain the target, so don't forget to invalidate the timer to prevent retain cycle.
EDIT: Added implementation below
First, we'll need to create the timer, assuming we have an instance variable called timer. This can be done in viewDidLoad: or init method of a view. Once initialized, we schedule for execution with the run loop, otherwise it will not fire repeatedly.
self.timesFired = 0
self.timer = NSTimer(timeInterval: 0.5, target: self, selector:"timerDidFire:", userInfo: nil, repeats: true)
if let timer = self.timer {
NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSDefaultRunLoopMode)
}
The following is the method fired by the timer every interval (in this case 0.5 seconds). Here, you can run your animations and audio playback. Note that UIViewAnimationOptions.Repeat option has been removed since the timer is now responsible for handling the repeating animation and audio. If you only the timer to fire a specific number of times, you can add an instance variable to keep track of times fired and invalidate the timer if the count is above the threshold.
func timerDidFire(timer: NSTimer) {
/*
* If limited number of repeats is required
* and assuming there's an instance variable
* called `timesFired`
*/
if self.timesFired > 5 {
if let timer = self.timer {
timer.invalidate()
}
self.timer = nil
return
}
++self.timesFired
self.audioPlayer.play()
var options = UIViewAnimationOptions.AllowUserInteraction | UIViewAnimationOptions.CurveEaseOut;
UIView.animateWithDuration(0.5, delay: 0, options: options, animations: {
self.img_MotronomLight.alpha = 0.1
}, completion: nil)
}
Without seeing the implementation of the audio player some things I can think of include:
The audio file is too long and perhaps has silence on the end of it so it is not playing the first part of the file which has the sound in it
the audio file needs to be set to the beginning of the file every time (it could just be playing the end of the file the other 3 times leading to no audio output)
The animation is happening to quickly and the audio doesn't have time to buffer
Hopefully these suggestions help you narrow down the problem but without seeing the implementation of the player, it is really hard to say what the actual problem is.
I have a button which shows a view and which automatically goes away after specified time interval. Now if the button is pressed again while the view is already visible then it should go away and show a new view and the timer for new view be reset.
On the button press I have following code:
func showToast() {
timer?.invalidate()
timer = nil
removeToast()
var appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
var toAddView = appDelegate.window!
toastView = UIView(frame: CGRectMake(0, toAddView.frame.height, toAddView.frame.width, 48))
toastView.backgroundColor = UIColor.darkGrayColor()
toAddView.addSubview(toastView)
timer = NSTimer.scheduledTimerWithTimeInterval(2.0, target: self, selector: Selector("removeToast"), userInfo: nil, repeats: false)
UIView.animateWithDuration(0.5, animations: { () -> Void in
self.toastView.frame.origin.y -= 48
})
}
To remove toast i have the following code:
func removeToast() {
if toastView != nil {
UIView.animateWithDuration(0.5,
animations: { () -> Void in
self.toastView.frame.origin.y += 48
},
completion: {(completed: Bool) -> Void in
self.toastView.removeFromSuperview()
self.toastView = nil
})
}
}
Now even though I reset the timer each time by doing timer.invalidate() I get two calls in removeToast() which removes the newly inserted view. Can it be that UIView.animate be causing problems, I'm not getting how to debug the two callbacks for removeToast(). A demo project showing the behavior is here
NOTE: I did find some post saying to use dispatch_after() instead of timer, also as asked by #jervine10, but it does not suffice my scenario. As if I use dispatch_after then it's difficult to invalidate GCD call. Is there something that could be accomplished with NSTimers. I think that NSTimers are meant for this and there's something that I'm doing wrong.
Sorry for not seeing your sample project, and thanks for directing me towards it. I can clearly see where the issue is, and the solution is super simple. Change remove toast to this:
func removeToast() {
guard let toastView = self.toastView else {
return
}
UIView.animateWithDuration(0.1,
animations: { () -> Void in
toastView.frame.origin.y += 48
},
completion: {(completed: Bool) -> Void in
toastView.removeFromSuperview()
if self.toastView == toastView {
self.toastView = nil
}
})
}
Basically, the problem is that you are capturing self in the animation blocks, not toastView. So, once the animation blocks execute asynchronously, they will remove the new toastView set in the previous function.
The solution is simple and also fixes possible race conditions, and that is to capture the toastView in a variable. Finally, we check if the instance variable is equal to the view we are removing, we nullify it.
Tip: Consider using weak reference for toastView
Use dispatch_after to call a method after a set period of time instead of a timer. It would look like this:
let popTime = dispatch_time(DISPATCH_TIME_NOW, Int64(2 * NSEC_PER_SEC))
dispatch_after(popTime, dispatch_get_main_queue()) { () -> Void in
self.removeToast()
}
I'm looking to animate a few things when a long process (parsing) is complete. My barebones code currently looks like such:
func run_on_background_thread(code: () -> Void)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), code)
}
func run_on_main_thread(code: () -> Void)
{
dispatch_async(dispatch_get_main_queue(), code)
}
func manage_response()
{
run_on_background_thread
{
long_process()
run_on_main_thread
{
UIView.animateWithDuration(animation_duration, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: UIViewAnimationOptions.CurveEaseInOut, animations:
{
some_view.transform = CGAffineTransformMakeScale(0, 0)
}, completion: nil)
}
}
}
Quite obviously, the animation does not occur. The UI just updates to the final stage. What is the proper way to thread this (I know animateWithDuration() dispatch's it's own thread, but I don't know how to hold this animation until the long process is complete.
Thanks.
I think U way can work. just change
some_view.transform = CGAffineTransformMakeScale(0, 0)
to :
some_view.transform = CGAffineTransformMakeScale(0.01, 0.01)
change the scale value close to zero; U can see the animation occur. if scale value == 0,there is no animation occur. U can try this.