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.
Related
I'm trying to add a countdown timer to an existing app. Naturally, I don't want this timer to stall the rest of the application so I wanted to use an asynchronous thread dedicated to the timer. This is my current code and it doesn't even get to the update function (I used print statements to test this), but does print "Got". Also, I'm trying to update a label with the correct time and you can't do that within the thread. The time variable is a class variable. Not sure if this is even the correct approach, any suggestions?
Edit: Running the timer on main queue doesn't work as it interferes with a pan gesture I already have on the app. Also, any proposed solutions to the timing inaccuracies of the Timer Class would also be great.
func startTimer() {
time = 30
let queue = DispatchQueue(label: "timer")
queue.async {
print("Got")
_ = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.update), userInfo: nil, repeats: true)
}
}
func update() {
DispatchQueue.main.sync {
if(time >= 0) {
time -= 1
timer.text = String(time)
} else {
timer.text = "0"
}
}
}
Okay I'm trying to do something similar to iTunes not sure if it's still the same. It's when you click a song and it gives a sample of the audio file. This is my code looks.
The music file is like 2-3min long. I got the start time to start at 42sec seconds. However, the song finishes to the end. I'm trying to make the audio file a sample of 30sec. So it should start at 42sec and end at 1min and 12sec.
Would appreciate any help thanks.
Every time your audio sample starts playing, you can create a Timer object which will make your player stop in a given amount of time.
var audioPlayer = AVAudioPlayer()
var timer: Timer?
func prepareMusic() {
....
// Your code to start playing sample
audioPlayer.currentTime = 42
audioPlayer.play()
// Here we are stopping previous timer if there was any, and creating new one for 30 seconds. It will make player stop.
timer?.invalidate()
timer = Timer(fire: Date.init(timeIntervalSinceNow: 30), interval: 0, repeats: false) { (timer) in
if self.audioPlayer.isPlaying {
self.audioPlayer.stop()
}
}
RunLoop.main.add(timer!, forMode: .defaultRunLoopMode)
}
func musicButton(sender: UIButton) {
....
// If sample is stopped by user — stop timer as well
if audioPlayer.isPlaying {
audioPlayer.stop()
timer?.invalidate()
}
}
There is one more edge case I can think of — if you hide/close view controller, you might also want to stop that timer.
override func viewWillDisappear(_ animated: Bool) {
timer?.invalidate()
}
I have an NSTimer set up that fires every 0.1 seconds, in the callback I fetch currentTime() and use it to update the label with the duration of the video.
When I am seeking forwards, by setting the rate to 3, this timer keeps up, but when I set the rate to -3, the video keeps up, but the currentTime() still returns the same value from when I started seeking. This occurs until I stop seeking and then currentTime() returns the correct time
How can I fetch the current time the video is at, which will work when seeking backwards?
Edit: Here is the code I use (translated from Xamarin C#):
class VideoPlayer: UIView {
var player: AVPlayer!
var wasPaused: Bool!
func play(url: String) {
// set the URL to the Video Player
let streamingURL: NSURL = NSURL(string: url)!
player = AVPlayer(URL: streamingURL)
let playerLayer = AVPlayerLayer(layer: player)
layer.insertSublayer(playerLayer, atIndex: 0)
// Reset the state
player.seekToTime(CMTime(seconds: 0, preferredTimescale: 600))
// Start a timer to move the scrub label
NSTimer(timeInterval: 0.1, target: self, selector: #selector(playbackTimeUpdated), userInfo: nil, repeats: true)
}
func playbackTimeUpdated() {
// This one is not correct when seeking backwards
let time = player.currentTime().seconds;
// Use the time to adjust a UIProgressView
}
// Gets called when the reverse button is released
func reverseTouchUp() {
player.rate = 1
}
// Gets called when the reverse button is pressed
func reverseTouchDown()
{
player.rate = -3;
}
}
Try CMTimeGetSeconds(player.currentTime()) instead of player.currentTime().seconds. It works for me.
Also check that you timer is actually running (add NSLog calls to it for example), maybe you just blocking its thread.
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.
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()
}