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

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

Related

Memory Allocations Profiler and Steadily Increasing Persistent Memory - Signs of Trouble?

I have an app I am developing and the stakeholder using it said that the app becomes slow and unusable/unresponsive after consistent usage all day. Killing it and starting over causes it to run fine.
I don't seem to have this trouble on my device, but I started looking at the memory usage in both simulator/phone in debugger, and observed my memory would steadily increase if I took the basic action of going between screen to screen. These are pretty involved screens, but if I just go forward to the 'add new item' screen, then back to the product listing screen, the memory jumps up 30mb. If I keep doing this same action, over and over and over, I can get it to 1.1gb of memory
I then took it a step further, hooked up my phone, and ran profiler (specifically memory leaks). I found one leak involving my usage of ads, so I just commented out all the code for a test and while the leaks are gone, the memory continues to go up steadily.
I then ran the allocations tool, and after a few min of going back and forth in the same manner, here is the output:
As you can see, it's 1.53GB and if I kept doing the same action I can get it to 2GB+. Oddly enough, my phone never seems to mind, and the screens are just slightly laggy at times otherwise not too bad. Certainly usable.
Before I start ripping out the floor boards, I wanted to confirm this is a likely sign of a problem. Any suggestions on where I can start looking? If persistent memory is the issue, what would be some typical gotchas or pitfalls? What is "anonymous vm?"
Thank you so much if you're reading this far, and appreciate any guidance!
UPDATE/EDIT
After some guidance here, I noticed, oddly enough, that on the "add product" page it causes the memory to jump ~10MB each time I visit it. After commenting out code, I narrowed it down to this section (and even the line of code) causing the jump. Removing this code causes it to remain stable and not increase.
//Render collection views
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath as IndexPath)
let member: MemberDto = groupMembers[indexPath.item]
let contactInitials = cell.viewWithTag(1) as! UILabel
let contactAvatar = cell.viewWithTag(2) as! UIImageView
contactAvatar.image = UIImage(named: "anonymous")
contactInitials.text = member.displayName
contactAvatar.layer.cornerRadius = contactAvatar.frame.size.width / 2
contactAvatar.clipsToBounds = true
contactAvatar.contentMode = UIViewContentMode.scaleAspectFill
contactAvatar.layer.borderWidth = 5.0
if (member.profileImage.trimmingCharacters(in: CharacterSet.whitespaces) != "") {
UserService.getProfilePicture(userId: member.userId) {
response in
contactAvatar.image = response.value
}
}
So, the offending line of code is here:
contactAvatar.image = response.value
Adding it in, and going back and forth to this tableviewcontroller causes the memory to go up and up and up all the way to 2gb. Removing that one line of code (where I set the image) keeps it stable at ~40-70mb, or it goes up but very very slowly (dozens of repeats only got it to 80mb)
I realized I was not caching this image
I decided to try caching this with my framework, and that immediately resolved the issue. I suppose the line of code was pulling the image into memory or something like that? It doesn't seem like the networking call is the actual issue, since I left that in (and even went so far to make additional calls to my API) and that doesn't seem to do much by way of memory increase.
Just a few pieces of info:
From the main screen, you tap on a + symbol in the navigation menu bar to come to this screen.
I am using a regular segue on my storyboard, associated with the navigationbutton, to take the user here
Placing deinit on this vc does not seem to ever hit, even with print/code in there and breakpoints
Making API calls from within my uitableviewcontroller doesn't seem to cause the image to load UNLESS I combine that with SETTING the image. If I make a network call, but don't set the image, it doesn't increase.
What mistake did I make? I feel like caching the image is a bandaid - I recall reading that you're not supposed to make calls to images within a UITableViewController but what is the alternative, to pull all user images from the collection in advance and cache them before the tableview loads?
EDIT 2
As #matt suggested, this was just a bandaid. The true problem still lingered as I knew deinit() was not being called. After pulling out major chunks of code, I found this
lblMessage.addTapGestureRecognizer {
self.txtMessage.becomeFirstResponder()
}
which maps to an extension class:
public func addTapGestureRecognizer(action: (() -> Void)?) {
self.isUserInteractionEnabled = true
self.tapGestureRecognizerAction = action
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture))
self.addGestureRecognizer(tapGestureRecognizer)
}
public func addLongPressGestureRecognizer(action: (() -> Void)?) {
self.isUserInteractionEnabled = true
self.longPressGestureRecognizerAction = action
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture))
self.addGestureRecognizer(longPressGestureRecognizer)
}
// Every time the user taps on the View, this function gets called,
// which triggers the closure we stored
#objc fileprivate func handleTapGesture(sender: UITapGestureRecognizer) {
if let action = self.tapGestureRecognizerAction {
action?()
} else {
print("no action")
}
}
So somewhere in here the problem must lie. I'm taking this to a new thread:
Deinit not calling - Cannot find why something is retaining (code provided)
Thanks! Hope this helps someone.
Yes it's a problem, and yes you need to fix it. The two usual causes of this sort of thing are:
You've got a retain cycle such that at least some of your view controllers are never able to go out of existence.
You've designed the storyboard (or manual segue) sequence incorrectly, so that (for example) you present from view controller A to view controller B, and then in order to get "back" you present from controller B to view controller A. Thus you are not actually going "back"; instead, you are piling up a second view controller A on top of the first one, and so on, forever.
Either way, you can rapidly test that that sort of thing is going on just by implementing deinit to print(self) in all your view controllers. Then play with the app. If you don't see the printout in the log every time you go "back", you've got a serious memory problem, because the view controller is not being released when it should be, and you need to fix it.

How to change a label in only one table view cell and save it with Swift?

I'm building an app that is a list of homework.
I'm a sort of beginner so I'm not using custom classes to save it and neither using CoreData. Just using NSUserDefaults and passing the data inside arrays between the views. My project is almost done (I want to implement some stuff like animations and etc but first I need to get it fully working right?), but I have 2 silly problems to solve, but I can't do it alone!
If I was going to put the code here the question would be too long, so I will leave the link of the project for you who is helping me check out.
https://github.com/HenriqueDoura/Agenda Here it is!
So, the first problem:
as it is a homework list app, it will control if the app isPendent or if it !isPendent, for that, I've put a label to the custom AtivCell called pendenciaLbl (which means pendencyLbl in English). By default, the bool is initialized as true, cause if the user is adding a homework, it is obvious that he did not finish it yet, right?
To change the bool value to false I've added a new actionForRowAtIndexPath action, called Feito (which means Done in English). By pressing it, I make the isPendent value false. On the table view func
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
I've put an if/else statement:
if isPendent {
cell.pendenciaLbl.text = "Pendente"
cell.pendenciaLbl.textColor = UIColor.redColor()
} else {
cell.pendenciaLbl.text = "Concluída"
cell.pendenciaLbl.textColor = UIColor.greenColor()
}
Pendente means Pendent and Concluída means concluded.
The thing is that when I set the cell I'm on to Feita/Done, it is setting all the other cells as done too and changing all the pendenciaLbl.text to Concluded, which is obviously not what I want, I just want to set ONE cell as Done.
The second and last problem envolves the same boolean isPendent. I don't know how to save this value, cause if I set a cell as Done and relaunch the app (after killing it via the app switcher) the pendenciaLbl comes back to "Pendent". How can I save the value when a cell is set to "Concluded" and check if it was set before when I launch the app?
Sorry for all english mistakes, I'm Brazillian.
Right now you only have one variable for isPendent. So if one cell changes that variable, all the cells are changed.
What you'll need to do is create a Array of Bools, one for each cell. Just like you have arrays of strings for other data like material and describes.
var pendent = [Bool]()
Then in cellForRowAtIndexPath
if pendent[indexPath.row] {
cell.pendenciaLbl.text = "Pendente"
cell.pendenciaLbl.textColor = UIColor.redColor()
} else {
cell.pendenciaLbl.text = "Concluída"
cell.pendenciaLbl.textColor = UIColor.greenColor()
}

XCTestCase - iOS UI Tests - dealing with UITableViews with many cells

I am experimenting with the (Xcode 7) UI XCTestCase test cases and I just stumbled onto an issue with one UIView, in which I have a UITableView with many cells(4000+).
When the app is running normally, only the visible cells are rendered and there is no performance issue at all.
However, if I run the app within the context of recording a XCTestCase and I navigate to this screen, the simulator freezes, apparently because each single cell is rendered as if it were visible.
If I try to script the navigation manually and I run the XCTestCase, the test case fails right after navigating to this screen, exiting with a "UI Testing Failure - Failed to get refreshed snapshot", apparently again because all cells are being rendered and this does not finish in time.
I think this has to do with the fact that the testing framework builds an entire metamodel of the screen under display, adding each of the 4000+ cells into the view tree hierarchy.
I tried adding an expectation, hoping this would give the testing container enough time to finish rendering all cells, but this does not work.
Is there a workaround for this? Is it somehow possible to skip building part of the UI tree hierarchy or something?
My goal is being able to write UI tests for this screen.
You might be able to avoid having the entire table render, if you can use firstMatch instead of element, and also avoid count.
I had a test that checks for expected labels in the first two cells of a table. At first, I was using app.table.cells.element(boundBy: 0) and app.table.cells.element(boundBy: 1) to find the first and second cells. This was resulting in the whole table being rendered before I could access the cells.
I adapted my test to be slightly less precise, but still good enough for me (given the huge amount of time it would take otherwise). Instead, I use matching with predicates on the expected label values, with firstMatch, to find the first cells matching the criteria I want. This way the traversal stops as soon as it finds them (and since they're at the top of the table, it's quick).
Here's the code before and after.
Before (slow, yet more precise):
private func checkRhymes(query: String, expectedFirstRhyme: String, expectedSecondRhyme: String) {
let table = app.tables.element
let cell0 = table.cells.element(boundBy: 0)
let cell1 = table.cells.element(boundBy: 1)
let actualRhyme0 = cell0.staticTexts.matching(identifier: "RhymerCellWordLabel").firstMatch.label
let actualRhyme1 = cell1.staticTexts.matching(identifier: "RhymerCellWordLabel").firstMatch.label
XCTAssertEqual(expectedFirstRhyme, actualRhyme0, "Expected first rhyme for \(query) to be \(expectedFirstRhyme) but found \(actualRhyme0)")
XCTAssertEqual(expectedSecondRhyme, actualRhyme1, "Expected first rhyme for \(query) to be \(expectedSecondRhyme) but found \(actualRhyme1)")
}
Faster, but less precise (but good enough):
private func checkRhymes(query: String, expectedFirstRhyme: String, expectedSecondRhyme: String) {
let table = app.tables.firstMatch
let label0 = table.cells.staticTexts.matching(NSPredicate(format: "label = %#", expectedFirstRhyme)).firstMatch
let label1 = table.cells.staticTexts.matching(NSPredicate(format: "label = %#", expectedSecondRhyme)).firstMatch
// We query for the first cells that we find with the expected rhymes,
// instead of directly accessing the 1st and 2nd cells in the table,
// for performance issues.
// So we can't add assertions for the "first" and "second" rhymes.
// But we can at least add assertions that both rhymes are visible,
// and the first one is above the second one.
XCTAssertTrue(label0.frame.minY < label1.frame.minY)
XCTAssertTrue(label0.isHittable)
XCTAssertTrue(label1.isHittable)
}
Reference:
https://developer.apple.com/documentation/xctest/xcuielementquery/1500515-element
Use the element property to access a query’s result when you expect a
single matching element for the query, but want to check for multiple
ambiguous matches before accessing the result. The element property
traverses your app’s accessibility tree to check for multiple matching
elements before returning, and fails the current test if there is not
a single matching element.
In cases where you know categorically that there will be a single
matching element, use the XCUIElementTypeQueryProvider firstMatch
property instead. firstMatch stops traversing your app’s accessibility
hierarchy as soon as it finds a matching element, speeding up element
query resolution.
I had the same issue, and I agree it is frustrating having to wait for the entire table to load, but that is what I had to do using the following workaround.
This may not be what you are looking for but it may help others:
Basically I am counting the cells in the table 2 times consecutively if they are not equal that means the table is still loading. Put it in a loop it and do that until both counts return the same number which would mean the table is finished loading. I then put in a stop of 30 seconds so that if this takes longer than 30 seconds, the test will fail (this was enough time in my case). If your table will take longer than that you could increase the number to 180 for 3 mins etc...
let startTime = NSDate()
var duration : TimeInterval
var cellCount1 : UInt = app.tables.cells.count
var cellCount2 : UInt = app.tables.cells.count
while (cellCount1 != cellCount2) {
cellCount1 = app.tables.cells.count
cellCount2 = app.tables.cells.count
duration = NSDate().timeIntervalSince(startTime as Date)
if (duration > 30) {
XCTFail("Took too long waiting for cells to load")
}
}
//Now I know the table is finished loading and I can tap on a cell
app.tables.cells.element(boundBy: 1).tap()

Remote control from lock screen in iOS, where should I check for the events?

I am new to iOS /swift programming and I am working on an app developed by someone else, fixing some bugs.
The app is essentially a music player and the music has to be played also in background, giving the possibility to play/pause/skip from the lock screen. The app has several views, one of them, the main one, contains all the code related to the player itself (player.swift), the other ones containing other additional pages/features.
The commands from the lock screen works only when I lock the screen starting from the main view, if I do it starting from another view (e.g. the help view, which is just a page which is displayed over the player when the help link is tapped) they don't work. Reading several articles here I've realized that the reason is that the related code is in player.swift:
override func remoteControlReceivedWithEvent(event: UIEvent) {
if (event.type == UIEventType.RemoteControl) {
switch (event.subtype) {
case UIEventSubtype.RemoteControlPlay:
self.onPlayPause(self);
case UIEventSubtype.RemoteControlPause:
self.onPlayPause(self);
case UIEventSubtype.RemoteControlTogglePlayPause:
self.onPlayPause(self);
case UIEventSubtype.RemoteControlNextTrack:
onNext(nil)
default:
break
}
}
}
so I have understood the problem, but even if I've read several related articles (including remoteControlReceivedWithEvent called on iOS 7.0 device but not iOS 8.0, Using lock screen for my app?, Swift. Receive remote control events to work with MPNowPLayingInfoCenter) I can't figure out where do I need to move this code and if I need to move something else or make modifications.
EDIT. I moved the code in AppDelegate.swift (deleting the code in player.swift) , as suggested. It seems it now intercepts commands even if I lock the device from a view different than player.swift. I have two problems, though:
1) It seems it works just once, If I click on "next" from the lock screen I can see from a debug string that the command is intercepted, If I do it a second time nothing happens
2) I need to call the methods (onPlayPause and onNext) in player.swift from AppDelegate.swift, I guess those methods expect to have a player object set and/or they refer to variables declared in player.swift and I don't know how to handle this. For example the onNext method is declared as
#IBAction func onNext(sender: AnyObject?) {
oldImage = iAlbumArt.image
.......
and if I call the method as a new instance from AppDelegate
player().onNext(nil)
I get an error because iAlbumArt.image is NIL. iAlbumArt is a variable declared in the Player class as
#IBOutlet weak var iAlbumArt: UIImageView!
Sorry for the naive questions but I've been looking into iOS development just since a couple of weeks ago.
Try adding it to your App Delegate class.
Edit:
To forward remote control events to the view controller, add this code to the app delegate (assuming your player view controller is called PlayerViewController):
let vcs = (self.window!.rootViewController as! UINavigationController).viewControllers
let indexOfPlayer = (vcs as! NSArray).indexOfObjectPassingTest { (vc, idx, stop) in
return (vc.isKindOfClass(PlayerViewController))
}
let playerVC = vcs[indexOfPlayer];
Edit 2:
Place the override func remoteControlReceivedWithEvent method in your App Delegate Class.
At the top of that method, place the code snippet shown above.
In that method, replace self with playerVC.
In your player view controller, add code to respond to onPlayPause and onNext functions.
Note:
The reason why this code:
player().onNext(nil)
was throwing an error was because player() creates a brand-new instance of your player class. You want to use the existing instance so the changes get reflected on the screen.

Possible to insert item at top of queue using AVQueuePlayer?

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.

Resources