Possible to insert item at top of queue using AVQueuePlayer? - ios

Assuming the queue has already been initialized, the only method I see to add items into the queue is:
- (void)insertItem:(AVPlayerItem *)item afterItem:(AVPlayerItem *)afterItem
The documentation says Pass nil to append the item to the queue.
So then is it not possible to add an item into the top of the queue? I want to be able to replay what was previously played without removing and requeueing everything up again.

Larme's comment above got me thinking and I was actually able to mimic the behavior I was looking for by doing the following:
// pause the player since we're messing with the currently playing item
[_avQueuePlayer pause];
// get current first item
id firstItem = [_avQueuePlayer.items objectAtIndex:0];
// add new item in 2nd spot
[_avQueuePlayer insertItem:newItem afterItem:firstItem];
// remove our first item so the new item becomes first in line
[_avQueuePlayer removeItem:firstItem];
// now add the original first item back in after the newly insert item
[_avQueuePlayer insertItem:firstItem afterItem:newItem];
// continue playing again
[_avQueuePlayer play];
This worked great! I think the only downside is the player has to buffer again the next item which we removed and inserted back in. However the remaining items in the queue will stay buffered so that's better than having to reset the entire queue.

I know this is an old question but I hit the same problem again today. Oren's solution didn't do the trick for me so I went with an even more radical approach. My case is also different since I only ever have two items in the queue (hence I use removeAll):
if let currentItem = player.currentItem {
player.removeAllItems()
player.insert(item, after: nil)
player.insert(AVPlayerItem(asset: currentItem.asset), after: item)
} else {
player.insert(item, after: nil)
}
Note: It wasn't enough to insert the same currentItem again after it was removed because afterwards the player's items() still just returned 1 item (item) even though calling canInsert(currentItem, after: item) returned true.

Related

Stop setting up several AVAudioPlayers with playAtTime() in a for loop

When selecting a row the following code sets up a set of AVAudioPlayers to playback at a certain date (in this case, 50 players playing in the interval of 1 second).
Since I want the whole process to restart when touching again I need to break the setup in the for loop since it takes a few seconds to setup the players.
Apart from that, each player is being removed after finishing playback using the audioDidFinishPlaying delegate method of AVAudioPlayerDelegate. I did not include this in the code since it is not relevant to the question.
I've tried using a flag inside the for loop to check whether setup is allowed but that doesn't work.
var players: [AVAudioPlayer] = []
var loadError: NSError?
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Removes the players that have been setup already.
players.removeAll()
// The for loop from the previous row selection should be stopped here.
for i in 0..<50 {
do {
let player = try AVAudioPlayer(contentsOfURL: soundUrls[i])
players += [player]
// The process of setting these up takes a few seconds, I need to break it.
print("Firing timer")
player.playAtTime(player.deviceCurrentTime + NSTimeInterval(i))
} catch let error as NSError {
loadError = error
}
}
}
What happens is, that the setup triggered by the previous row selection will continue until it is finished and only then the new for loop starts.
I need to break it earlier.
I can't figure out how to tackle this. Maybe by removing the processes from the main thread(How?)? Any help much appreciated!
I'm a little bit confused about your statement of the problem, but I'll try to give a suggestion anyway.
You say that you set up the players when selecting a row, but the code to set them up is in cellForRowAtIndexPath. So it's set up and playing starts when a cell is returned and displayed in your table view.
What exactly are you trying to achieve? You have a table view with a number of rows, and whenever you tap on a cell the fifty sounds have to start playing one after the other (1 second apart). If you tap the same cell again they should stop and restart, is that it?
Then what I would do is set up the 50 players in viewDidLoad of your TableViewController. Use prepareToPlay().
Start them when needed.
Then if you need to restart them, just cycle through them, check if they are still playing using isPlaying. Pause them if needed, set the current time to 0 and call playAtTime again.
Don't remove the players in audioDidFinishPlaying. Because then you'd have to recreate them.
Just reset them so they're available for immediate playback again.
By the way, if you're going to do more with audio and want more control and better performance I highly recommend the excellent frameworks The Amazing Audio Engine 2, or AudioKit

Checking the current view state after block/closure completion

Within a asynchronously executed block/closure, I want to get a check on my current state before I executed anything within that block.
A common example of where this presents itself is segueing to the next View Controller after a NSURLsession request.
Here's an example:
#IBAction func tappedButton(sender: UIButton) {
//This closure named fetchHistorical goes to the internet and fetches an array
//The response is then sent to the next view controller along with a segue
Order.fetchHistorical(orderID, completionHandler: { (resultEnum) -> () in
switch resultEnum {
case .Success(let result):
let orderItemsArray = result.orderItems!.allObjects
self.performSegueWithIdentifier("showExchanges", sender: orderItemsArray)
default:
let _ = errorModal(title: "Error", message: "Failed!")
}
})
}
Assume that the user has been impatient and tapped this button 3 times.
That would mean this function will be called three times and each time it would attempt to segue to the next view controller (iOS nicely blocks this issue with "Warning: Attempt to present on whose view is not in the window hierarchy!")
I wanted to know how do you folks tackle this problem? Is it something like ... within the closure, check if you are still in the present viewcontroller ... if you are, then segueing is valid. If not, you probably have already segued and don't execute the segue again.
***More generally, how are you checking the current state within the closure because the closure is executed asynchronously?
Since the closure isn't executing on the main thread the state would be in accurate if you check it here (as you stated). You can use GCD to go to the main thread and check the state there. There are a couple of ways you can keep this code from running multiple times. If it will take some time to perform the calculations you can use an acitivity indicator to let the user know the app is busy at the moment. If you want the user to still have the option of pressing the button you can put a tag like:
var buttonWasTapped:Bool = false //class property
#IBAction func tappedButton(sender: UIButton) {
if !self.buttonWasTapped{
self.buttonWasTapped = true
}
}
Then change it back to false on viewDidAppear so they can press once every time that page is shown.
When starting some task that will take some time to complete I would do two things;
Show some sort of activity indicator so that the user knows something is happening
Disable the button so that there is further indication that the request has been received and to prevent the user from tapping multiple times.
It is important that you consider not only the correct operation of your app but also providing a good user experience.

Long delays displaying UIImageView loaded from local file?

I see questions regarding long delays in displaying UIImageViews after downloading, but my question involves long delays when
reading from local storage.
After archiving my hierarchy of UIImageViews to a local file (as per narohi's answer in
How to output a view hierarchy & contents to file? ),
I find that if I want to reload them, it takes 5 to 20 seconds for the views to actually appear on screen,
despite my setting setNeedsDiplay() on the main view and all the subviews.
I can immediately query the data contained in the
custom subclasses of UIView that get loaded -- showing that NSKeyedUnarchiver and all the NS-decoding and all the init()'s have completed -- however
the images just don't appear on the screen for a long time. Surely the next redraw cycle is shorter than 5-20 seconds...?
It seems odd that images from PhotoLibrary appear instantly, but anything loaded from local file storage using NSKeyedUnarchiver takes "forever."
What's going on here, and how can I speed this up?
.
.
To be explicit, the relevant part of my Swift code looks like this:
let view = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as! UIView!
if (nil == view) {
return
}
myMainView.addSubview(view)
view.setNeedsDisplay()
// now do things with the data in view ...which all works fine
I find that, even if I add something like...
for subview in view.subviews {
subview.setNeedsDisplay()
}
...it doesn't speed up the operations.
We are not talking huge datasets either, it could be just a single imageview that's being reloaded.
Now, I do also notice these delays occurring when downloading from the internet using a downloader like the one shown in
https://stackoverflow.com/a/28221670/4259243
...but I have the downloader print a completion message after not only the download but when the (synchronous operation)
data.writeToFile() is complete (and before I try to load it using NSKeyedUnarchiver), so this indicates that the delay
in UIImageView redraws is NOT because the download is still commencing....and like I say, you can query the properties of the data and it's all in memory, just not displaying on the screen.
UPDATE: As per comments, I have enclosed the needsDisplay code in dispatch_async as per Leo Dabus's advice, and done some Time Profiling as per Paulw11's. Link to Time Profiling results is here: https://i.imgur.com/sa5qfRM.png I stopped the profiling immediately after the image appeared on the screen at around 1:00, but it was actually 'loaded' during the bump around 20s. During that period it seems like nothing's happening...? The code is literally just waiting around for a while?
Just to be clear how I'm implementing the dispatch_async, see here:
func addViewToMainView(path: String) {
let view = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as! UIView!
if (nil == view) {
return
}
dispatch_async(dispatch_get_main_queue(), {
self.myMainView.addSubview(view)
view.setNeedsDisplay()
self.myMainView.setNeedsDisplay()
})
}
...Since posting this I've found a few posts where people are complaining about how slow NSKeyedUnarchiver is. Could it just be that? If so, :-(.
SECOND UPDATE: Ahh, the "let view = " needs to be in the dispatch_async. In fact, if you just put the whole thing in the dispatch_async, it works beautifully! so...
func addViewToMainView(path: String) {
dispatch_async(dispatch_get_main_queue(), {
let view = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as! UIView!
if (nil == view) {
return
}
self.myMainView.addSubview(view)
view.setNeedsDisplay()
self.myMainView.setNeedsDisplay()
})
}
This works instantly. Wow.. Credit to Leo Dabus. Leaving this here for others...

How to return to a loop after button press

I am taking an online course and one of the problems has you build a simple number guessing game. That was easy enough. However, I want to modify it to limit the number of guesses. That would entail a loop for the number of guesses allowed.
When I try this, the code goes to the button press action and never returns to the loop. I have tried everything I know to get it to work, google searches have not helped, and the instructor has not answered my question posted 4 days ago!
So in short how does one get code to return to a loop after finishing the code for the button press?
In words here is what I want to do:
generate random number
for x = 1 to 6
get user guess in text field
press button to check if correct
if correct
do something
else
continue loop for another guess
x = x+1
You can't do what you want.
The user interface is an event-driven activity -- things happen as a result of the user typing in a textField or tapping a button. The code to handle the UI is distributed among the response routines that handle the events and you have to figure out where you can do each part of what you want to do. It can be maddening!
A general answer is that the code to handle events is distributed among multiple methods and you have to have some sort of shared state so that each event response can know what to do. Some code somewhere starts a round of the game and initialize the number of guesses. Your textField delegate routines get the user guess and store it in an instance variable or property. When the user taps a button, the button-response code can check the answer in that property and keep track of the number of guesses.
To be specific, here is some pseudocode that does what you proposed in your question:
#property (strong, nonatomic) NSString *currentGuess;
#property (nonatomic) int numberOfGuesses;
- (void) startNewRound {
...
// some code that gets ready for the user to guess
generate random number
// initialize the number of guesses where button code can get at it
self.numberOfGuesses = 0;
...
}
// This method is called when the user finishes typing
// a guess in the text field as a result of the user pressing Return
// (or however you manage the data entry). The parameter is whatever
// the user typed as a guess.
- (void) userEnteredAGuess:(NSString *)guess
// put the guess where the button-event code can get at it
self.currentGuess = guess;
}
// This is the method that gets run when the user taps the
// "Check the Guess" button
- (IBACTION) tapButton:(UIButton *)sender {
if (the guess in self.currentGuess is correct...) {
// Do whatever you do when the guess is correct
} else {
// Do whatever you do when the guess is wrong
}
self.numberOfGuesses += 1;
if (self.numberOfGuesses > guessLimit) {
// Exceeded guess limit
// Do whatever should happen --
// re-initialize the guesses, decrement score, ... whatever
// maybe...
[self startNewRound];
}
}

UITableView not refreshing

I am creating a list and displaying it in a UITableView. While viewing I want to delete an item and then save the updated list, allowing me to refresh the old list with a refresh button. I use a button to save the current list, then a refresh button to reload the data into the view. The problem is the code within the save button always runs, even when I don't press the button. I can explain best like this:
btnSave.TouchUpInside += delegate { _ListCopy = new List<Tasks>();
};
btnReset.TouchUpInside += delegate {
tblTotal.ReloadData();
tblTotal.DataSource = new ListDataSource(_ListCopy,txtTotalCost);
};
tblTotal.DataSource = new ListDataSource (_ListOfTasks,txtTotalCost);
When I run this the table view shows the _ListOfTaskslist, when I press btnReset the _ListCopy is reloaded as the data source. The issue I am having is that the value for _ListCopy is assigned even when I don't press btnSave. In this case the view is cleared. If I comment out the line _listCopy = new List<Tasks>(); I receive a null reference exception in the table view Data Source here:
public override int RowsInSection (UITableView tableView, int section)
{ return _ListOfTasks.Count; } <-- exception
indicating to me that the value of _ListCopy is empty. I thought I could make _ListCopy = _ListOfTasks in place of new List. But, for some reason the code is running in the save button regardless, so I am never able to save the correct list value when I want it. Why is_ListCopy be assigned a value without telling it? I do not assign it a value anywhere else either. Can someone explain what is happening? Is there a different solution that I could use instead?
I discovered that I had my outlets setup wrong. For some reason I had two outlets wired to btnreset and no outlets for btnsave. So when I pressed reset, I was also firing off the code for btnsave. I solved the whole issue by correcting my outlets. As for making a copy of the original list: I was trying to simply make _ListCopy = _ListOfTasks for the save function. This only pointed to the memory location of _ListOfTasks, which included any changes. I corrected this by writing a foreach loop to copy one list to the other
foreach( var task in _ListOfTasks)
{
_ListCopy.Add(task);
}
Now I can press save and the first estimate is stored. I can do any deletions, and if the customer wants to revisit the initial estimate I can refresh the data.

Resources