My app has four View Controllers (VC's):
1) Home: Has a SpriteKit animation on an SKView. A swipe on this VC lets the user proceed to the Authoring VC (next).
2) Authoring: Has a menu (a UITableView). The menu lets the user access ViewGames and Store (below).
3) ViewGames: Contains a UICollectionView, and a nav bar with a close button. This presents a single UICollectionViewCell at a time, and lets the user swipe to proceed to the next cell. Each cell has a SpriteKit animation on a SKView, and also has three buttons.
4) Store: Has an in-app purchase store, with UI implemented as a UITableView. For the purpose of this discussion, the only feature I'm using is SKProductsRequest to fetch back a list of products which are displayed in the table view.
Problem: In the ViewGames VC, under some circumstances, the UI of the second and following cells in the collection view operate very slowly. E.g., about a factor of 10 slower than normal. The animation in the SKView is very slow. And the four buttons (three on the collection view cell, one in the nav bar) operate very slowly. Often they won't respond at all and you have to tap them several times. The swipe to go to the next cell responds similarly-- and slowly, if at all. (If I go back to the first cell using swiping, the first cell is also similarly responsive, but initially it doesn't exhibit this problem).
Reproducing the problem. The good news is, that in my app, reproducing this problem is very consistent. Here's what will produce it:
Launch app > Swipe to go to Authoring > Use menu to go to the Store > exit Store to return to Authoring > exit Authoring back to Home > go to Authoring > go to View Games.
Some other facets:
A) If I exit View Games, back to Authoring, and then re-enter View Games, this problem is the same.
B) No other parts of the app exhibit this sluggishness of UI response.
C) If, after getting this behavior in the View Games VC, I now exit ViewGames back to Authoring, re-enter the Store, go back to Authoring, and go back to ViewGames, the problem goes away.
D) This problem is only exhibited on iOS9, iOS9.1, iOS9.2 (beta). It doesn't occur on iOS8.4. (All running on physical devices; I've yet to try the simulator). I was using Xcode 7.0.1 initially, but am now using Xcode 7.2 beta and the problem remains the same. My app is targeted at iOS8 and above.
E) If I launch the app, and go to Authoring, then ViewGames, this problem doesn't occur.
Question: What can make part of the UI run sluggishly, but only temporarily?
Avenues explored so far:
(i) I've looked at this app in the Time Profiling Instrument, but can't see anything that looks like its soaking up time.
(ii) Only one part of the app is doing network interaction, and that's the Store. And the product fetch succeeds, and displays that info.
(iii) My best guess right now is that this is related to memory usage. When the symptoms appear, it appear that there is at least a somewhat greater amount of RAM used in going from cell 1 of the Authoring UICollectionView to cell 2 (0.4 to 0.9MB in cases where the problem appears; 0.3MB in cases where the problem does not appear).
(iv) In the development history of the app, as I was getting ready to submit v1.0 to Apple, I had a memory leak that exhibited some of these symptoms. However, to my recollection, that memory leak only affected the SpriteKit animations, affected all SpriteKit animations (both on the Home and Authoring VC's), and was not temporary. You had to restart the app to get around it.
(v) I've looked at the app quite a bit using Instruments/Leaks/Allocations. There are some leaks, but they appear to be from Apple frameworks, not mine.
(vi) I've put breakpoints and log messages in the dealloc/deinit methods and all of the primary classes seem to be deallocating (e.g., the VC's, and the collection view and it's cells).
Update1: 11/4/15; 3:47pm MST: The problem is not related specifically to the ViewGames SpriteKit animation. I just disabled the animation in the ViewGames UICollectionViewCell's and the problem still occurs. The sluggishness still happens for swipe and button press responses. Of course, the cells still have an SKView/SKScene.
Update2: 11/4/15; 3:55pm MST: I just disabled the product fetch (which uses SKProductsFetch) out of the Store. AND the problem goes away!! Significant narrowing down of the issue!
Update3: 11/4/15; 6:10pm MST: With the product fetch in place, but with the delegate of the SKProductsFetch object set to nil, the problem does not occur! It is also relevant to note that a completion handler (called fetchProductsCompletion) that was part of my class construction was also set to nil.
Update4: 11/4/15; 6:10pm MST: With the product fetch in place, and with a non-nil delegate for SKProductsFetch, but with fetchProductsCompletion set to nil, the problem does not occur!
I've found a work-around for the problem. I don't know why it works. But, I'll post some of the code to give a context.
This is the method I am using in the Store to fetch products:
// Not calling this from init, so that we can make sure there is a network connection, and only give a single error/alert if there is no network connection.
private func initiateProductFetch(done:(success:Bool)->()) {
let productIds = self.productsInfo!.productIds()
if nil == productIds {
Assert.badMojo(alwaysPrintThisString: "No product Ids!")
}
/* Bug #C34, #C39.
10/30/15; I was having a memory retention issue in regards to the fetchProducts completion handler. The SMIAPStore instance wasn't getting deallocated until the *next* time the store was presented because the store delegate was retaining a reference to the completion handler, which had a strong reference to self (SMIAPStore).
At first I tried to resolve this by having [unowned self] in the following, but that fails with an exception. Not sure why. And having [weak self] also causes self! to fail-- "fatal error: unexpectedly found nil while unwrapping an Optional value". Why?
The only way I've found to work around this is to have a selfCopy as below, which is nil'ed out in the completion handler. Seems really clumsy. For consistency sake, and safety sake, I'm also using this technique in the other self.storeDelegate usages in this class.
*/
var selfCopy:SMIAPStore? = self
self.storeDelegate?.fetchProducts(productIds!) {
(products:[SKProduct]?, error:NSError?) in
Log.msg("\(products)")
if (products != nil && products!.count > 0 && nil == error) {
// No error.
selfCopy!.productsInfo!.products = products
Log.msg("Done fetching products")
done(success:true)
}
else {
// Show an error. The VC will not be displayed by now. Returning the error with the done call back will not allow it to be displayed.
// It seems possible the products are empty and error is nil (at least this occurs in my debug case above).
var message = ""
if error != nil {
message = error!.localizedDescription
}
let alert = UIAlertView(title: "Couldn't get product information from Apple!", message: message, delegate: nil, cancelButtonTitle: SMUIMessages.session().OkMsg())
selfCopy!.userMessageDetails = UserMessage.session().showAlert(alert, ofType: UserMessageType.Error) { buttonNumber in
done(success:false)
}
}
selfCopy = nil
}
}
The store delegate method (my construction) looks like this:
// Call this first to initiate the fetch of the SKProduct info from Apple
public func fetchProducts(productIdentifiers:[String], done: (products:[SKProduct]?, error:NSError?) ->()) {
self.requestDelegateUseType = .ProductsRequest
self.productsRequest = SKProductsRequest(productIdentifiers: Set(productIdentifiers))
self.productsRequest!.delegate = self
self.fetchProductsCompletion = done
self.productsRequest!.start()
}
And here is the delegate method for the SKProductsRequest:
// Seems like this delegate method gets called *before* the requestDidFinish method. Don't really want to rely on that behavior though.
public func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
Assert.If(.ProductsRequest != self.requestDelegateUseType, thenPrintThisString: "Didn't have a product request")
if 0 == response.invalidProductIdentifiers.count {
/*
#if DEBUG
// Debugging-- simulate an error fetching products.
self.fetchProductsCompletion!(products: nil, error: nil)
return
#endif
*/
self.fetchProductsCompletion?(products: response.products, error: nil)
//self.fetchProductsCompletion = nil
}
else {
let message = "Some products were invalid: \(response.invalidProductIdentifiers)"
self.fetchProductsCompletion!(products: nil, error: NSError.create(message))
}
}
The code as given above does exhibit the problem. When I set the fetchProductsCompletion handler to nil after it is used, the problem goes away. Any ideas as to why?