Using UICollectionViewLayoutInvalidationContext to optimize layout calculations during inserts, deletes and moves - ios

I've implemented a custom UICollectionViewLayout for my UICollectionView and also a custom UICollectionViewLayoutInvalidationContext hoping to get good performance.
In my first use case, I have floating headers and during scrolling I only invalidate the headers that need to slide.
I do this as follows:
I return YES from shouldInvalidateLayoutForBoundsChange:.
When my invalidationContextForBoundsChange: method gets called I add stale headers to the context by calling invalidateSupplementaryElementsOfKind:atIndexPaths:.
When invalidateLayoutWithContext: gets called I don't really need to do anything because invalidating the header was enough to get the following key thing to happen: layoutAttributesForSupplementaryViewOfKind:atIndexPath: gets called instead of everything starting over with prepareLayout.
This works great.
My first question is: What about this scenario suppressed the prepareLayout sequence? At first I thought that adding any data to the context (in this case the index paths of stale supplementaries) was enough for Apple to presume I knew what I was doing (iOS8-wise) and not call prepareLayout. I know longer believe that as you'll see below.
My second use case: In practice, my data source almost always adds items only at the end. And for my layout algorithm, that means that the layout of earlier (indexPath-wise) items don't go invalid with new data. I don't want to spend time recomputing potentially large numbers of layout attributes that I don't need to. So I need to do "the right thing" when the client code calls say:
[collection insertItemsAtIndexPaths: newIndices];
by noting what the earliest indexPath in newIndices is and recompute my layout starting from there. Almost always, newIndices starts at the very end and no existing cells go stale--so only attributes for the new cells should be asked of my layout object by Apple.
But note, there is no analogous method to invalidationContextForBoundsChange: for insert/delete/move calls. So I have no way to preconfigure my context with say what indices are coming and going.
So inside the call to insertItemsAtIndexPaths: this is what happens:
My context object gets -init'ed. This happens early--before invalidateLayout is called. At init time, self.invalidateEverything and self.invalidateDataSourceCounts are (NO,NO) (and they're readonly).
Next my invalidateLayoutWithContext: is called now with invalidateDataSourceCounts set to YES--Apple telling me only that my counts are stale. That's useless as that doesn't tell me anything about where to restart my layout calculations. But this is my first (and only chance) to notice something or to do something.
Next my prepareLayout gets called--Grrr! I don't want to start over. I don't want prepareLayout called--or at least I need to stash some info before it's called so I don't overwork. But my only chance was in invalidateLayoutWithContext: and I don't know anything at the time this was called (remember there is nothing similar to invalidationContextForBoundsChange:). Further invalidateDataSourceCounts==YES is useless.
I tried to invalidateItemsAtIndexPaths: any old cell in view inside invalidateLayoutWithContext: to test if that's enough to prevent a call to prepareLayout (in case I did somehow figure out which cells to inval). No go--prepareLayout was still called. So second question: Is invalidateLayoutWithContext: too late to call things like invalidateItemsAtIndexPaths: for Apple to react?
Now to add insult to injury, I notice in the debugger that at invalidateLayoutWithContext: time my context has a private ivar named _updateItems that has all the info I need (at the only time I get a chance to use it). But there is no getter for this ivar. These UICollectionViewUpdateItem entries are delivered only when my prepareForCollectionViewUpdates: method gets called. But guess when this gets called--after prepareLayout and friends (collectionViewContentSize and layoutAttributesForElementsInRect:).
I'm either missing something key or these APIs are broken. Certainly I should be able to add a cell at the end of my layout efficiently. Was I supposed to notice the invalidateDataSourceCounts==YES and just warn my prepareLayout implementation--"Sorry you got called, don't do anything yet. I'll try to call you again later myself after I know what to do."
Or was the client code supposed to somehow create an instance of my context and configure it before calling [collection insertItemsAtIndexPaths: newIndices]? But then how are clients suppose to hand this context down inside the insertItemsAtIndexPaths: call? Further are clients really supposed to be exposed to this implementation detail?

Related

What happens if the `NEPacketTunnelflow` method `readPacketsWithCompletionHandler` is called multiple times?

When calling the method
- (void)readPacketsWithCompletionHandler:(void (^)(
NSArray<NSData *> *packets, NSArray<NSNumber *> *protocols))completionHandler;
the completionHandler is either called directly, in case packets are available at call time, or it is called at a later tim when packets become available.
Yet what is nowhere documented is: What happens if I call this method again before the prior set completionHandler has ever been called?
Will the new handler replace the prior set one and the prior set one won't get called at all anymore?
Are both handler scheduled and called as data arrives? And if so, will they be called in the order I passed them, in reverse order, or in random order?
Has anyone any insights on how that method is implemented?
Of course, I can make a demo project, create a test setup, and see what results I get through testing but that is very time consuming and not necessarily reliable. The problem with unspecified behavior is that it may change at will without letting anyone know. This method may behave differently on macOS and iOS, it may behave differently with every new OS release, or depending on the day of the week.
Or does the fact that nothing is documented is by intention? Do I have to interpret that as: You may call this method once and after your callback was executed, you may call it again with the same or a new callback. Everything else is undefined behavior and you cannot and should not rely on any specific behavior if use that API in a different manner.
As nobody has replied so far, I tried my best to figure it out myself. As testing is not good enough for me, here is what I did:
First I extracted the NetworkExtension framework binary from the dyld cache of macOS Big Sur using this utility.
Then I ran otool -Vt over the resulting binary file to get a disassembler dump of the binary.
My assembly skills are a bit rusty but from what I see the completionHandler is stored in a property named packetHandler, replacing any previous stored value there. Also a callback is created in that method and stored on an object obtained by calling the method interface.
When looking at the code of this created callback, it obtains the value of the packetHandler property and sets it to NULL after the value was obtained. Then it creates NSData and NSNumber objects, adds those to NSArray objects and calls the obtained handler with those arrays.
So it seems that calling the method again just replaces the previous completionHandler which is never be called in that case. So you must not rely that a scheduled handler will eventually be called at some time in the future if the tunnel is not teared down if the possibility exists that your code might replace it. Also calling the method multiple times to schedule multiple callbacks has no effect as as only the last one will be kept and eventually be called.

CNContactStoreDidChangeNotification multiple times [duplicate]

I am able to observe the CNContactStoreDidChangeNotification when the contact database is changed while the app is in background state. I am pretty sure that only one observer was added to NSNotificationCenter.
The problem is NSNotificationCenter posts MULTIPLE times (2, 3, 5, and even more times) even if I only add one new contact.
Where is the problem?
Make certain you aren't adding the observer multiple times. This can happen without you realizing it if (for example) you call -addObserver from -viewDidLoad or -viewDidAppear in your view controller (as these might get called more than once throughout the life of your application), or from any of the application state callbacks in your app delegate (-applicationDidBecomeActive, -applicationWillResignActive, -applicationDidEnterBackground, -applicationWillEnterForeground, etc).
Wrap the call to -addObserver in a conditional that ensures it can only be called once (set a flag), and put NSLog statements around it so you can see in the debug console if you are getting there more than once. Search your code for other calls to -addObserver that you might have forgotten about.
Call -removeObserver before adding it, just to be sure (making sure to pass the same name and object as when you added it). Calling -removeObserver on an observer that doesn't exist is okay. Note that this is more of a band-aid than a fix - your code should be smart enough to know whether or not you've already added it - but this might help you diagnose the problem).
I just wrote a quick minimal test program that adds an observer (once!) on CNContactStoreDidChangeNotification and I only get the notification once when I add or change a contact. Write a similar test program for yourself and see if you get the same result. If your test program works correctly, then it is likely that your app is doing something you don't expect (and calling -addObserver multiple times).
I had the same problem, the number of times it fired varied between 2 & 3. The solution that worked for me was to set a semaphore variable, set in the handler and reset the semaphore when finished. Wrap the address book processing in an if statement on the semaphore to ignore further calls.
addressBkSemphore is reset to false in buildFrendsAndContacts
- (void)addressBkChange:(NSNotification *)note
{
if (addressBkSemphore == false)
{
addressBkSemphore = TRUE;
[self buildFrendsAndContacts];
}
}
Hope it helps.
You can start a one time execution timer or a dispatch after few seconds and cancel it in case there's a new contacts update within those seconds, thus ensuring that only the timer or dispatch_after triggered by the last update will actually execute (taking into account that all update calls come one after the other within under a sec. difference, as far as I tested)
And btw, I could reproduce the issue only when making change to contacts on the same device with my app. If I change the contacts on another device linked to the same apple account, there was only one update.

Add behavior to delete method in parse

I have a custom subclass of PFObject which keeps track of a large video file on the device. I want to make sure that if i delete the PFObject the videoFile is also deleted.
For now if have override all variants of the delete method, but that seem wrong. Is there a central way to add a behavior when an object is deleted?
There's a hook that catches every delete on the back-end, (beforeDelete in cloud code), but from the question, it sounds like that's the wrong place to catch, because the file in need of deletion is local.
Parse recently open-sourced the SDK. Perusing the code. It looks like the delete variants ultimately all call deleteInBackground. So one idea -- a little too clever, IMO -- would be to override only that one. But I think it would be unwise to depend on this undocumented fact.
If you control the caller side, one idea is to just make a policy to never call delete directly, and provide an "otuswebDelete" method to do the object and file delete.
If you don't control the caller (or don't trust yourself to remember your own policy), I think you're better off, under your current design, to just override the few variants:
– delete
– delete:
– deleteInBackground
– deleteInBackgroundWithBlock:
– deleteEventually
They can all just call super to delete, then call a method in the subclass to delete the local file. Not so bad, IMO.
Finally, for reasons too numerous to detail here, I'm in the habit of "wrapping" my PFObjects (an NSObject subclass that has a PFObject property) rather than subclassing them.
The burden of this approach is a little tedium to create the accessors for the properties, but in return I get more control of (a) the use of SDK methods (as in your issue), (b) serialization, (c) fetching an managing related objects, (d) more...

Method mysteriously exits execution in the middle to resume the rest of the program?

So i've got this code that tries to find an unused upload name, using the user's email and a number at its end. It does this with a list of uploaded objects we've already collected, the user's email.(upload_name), and the
current number that might be open (it is incremented when a match is found).
The list is not sorted, and it's pretty tricky to sort for a few reasons, so I'm having the method read through the list again if it reaches the end and the upload_number has changed.
- (NSString*)findUnusedUploadNameWithPreviousUploads:(NSMutableArray*)objects withBaseUploadName:(NSString*)upload_name {
previous_upload_number = upload_number;
for (NSString *key in objects) {
// the component of the object name before the first / is the upload name.
NSLog([key componentsSeparatedByString:#"/"][1]);
if ([[key componentsSeparatedByString:#"/"][1]
isEqualToString:([NSString stringWithFormat:#"%#_%ld", S3KeyUploadName1, upload_number])]) {
upload_number++;
NSLog([NSString stringWithFormat:#"upload name: %#_%ld", S3KeyUploadName1, upload_number]);
}
NSLog(#"pang");
}
NSLog(#"ping");
if (previous_upload_number == upload_number) {
return [NSString stringWithFormat:#"%#%ld", upload_name, upload_number];
}
return [self findUnusedUploadNameWithPreviousUploads:objects withBaseUploadName:upload_name];
}
The problem is, the program never reads the "ping". it just leaves the method after the first for loop is done.
Edit: No the NSlogs are fine, you can do simple string OR StringWithFormat.
Edit: Don't mind the unnecessary use of recursion, I did this because the simple way was having the same problem and i wanted to see if a different (albeit unnecessarily recursive) way would share that problem. It does.
Edit: I set a breakpoint in the for loop, and I set a break point at the "ping". It does not reach the ping. It completes the for loop and the ditches the whole thing.
Edit: Please try to help me figure out why it's exiting the the method immediately after the for loop. I'm aware this is stylistically meh, and I promise I'll make it shiny and spotless when it works. =]
Edit: to be clear, the method DOES exit. it does so early I know this because the rest of the program following this method (which is not threaded such that it wouldn't have to wait for it) runs AFTER this for loop, consistently.
There are a couple of possible explanations for the described behavior:
The method never exits. For some reason it blocks or performs infinitely somewhere in the loop. Make sure this is not the case by setting a breakpoint after the place where the message is called (i.e. the place to where it should return).
The method, or some method it calls, throws an exception. While seldom and unsupported in productive Cocoa code it could be some misbehaving 3rd party library or just a simple programmer error, where Cocoa actually does throw. Make sure this does not happen by setting an exception breakpoint in Xcode.
Undefined behavior. This is, sadly, part of official C and heavily exploited by modern compilers. It basically means: Anything can happen, if there's something in your code, where the standard says that the behavior is not defined. One example would be accessing a deallocated object. Another fine reason of undefined behavior can be threads accessing common data in an unsynchronized way.
Other than exceptions there's no explanation for a method to "exit early". If you still can't find the reason I suggest you invest some time to learn the debugger. Single stepping, like Aris suggested, might be a way to find out what's going on.

strange error when reload tableView

I have a very general table view.
when it is refreshed, it will go to fetch list of objects from Parse. Analyze these data in a dispatch_async queue, then refresh table view. Most time, it has no problem, but some time reloadData() crash
Is it crashes because the tableView is reloading data when I call it? (when the tableview is init, reloadData may be called automatically) How to avoid this error? ( there is no error message in console )
EDIT:
I tries to put ?, but does not work
This happens when your tableView (or whatever object you're sending the message to) is nil. So sometime before your async call dispatched this on the main queue, your tableView got dealloacted.
Check this link out for some info:
http://www.touch-code-magazine.com/how-to-debug-exc_bad_access/
You will get EXC_BAD_ACCESS error mostly in the following scenarios:
You are trying to access an object that is not initialized.
You are trying to access an object that no longer exists. Either it’s being released or it’s nil. In ARC mode, make sure you take ownership of the object that you want to use.
You are passing an message to an object that the object doesn’t understand.
It can also happen for bad typecast. Like the lines below where I am trying to access an int with %# in stead of %d.
int myAwesomeInt = 9;
NSLog(#"%#", myAwesomeInt);
How to debug:
Identify what you did that caused the crash. Did it crash while view of a particular view controller didLoad or in a delegate method or on a particular action. That will often help to find the object that is casuing the error.
(In your case look at what specifically happens when you are reloading the table. Do a stack trace line by line and see what your code is doing during a reload)
Most of the time “NSZombies” can help to identify the dead object. You can enable NSZombies by editing your scheme Product -> Edit Scheme -> Diagnostics.
If you still don’t find the root cause then always go backwards from child view controller to parent view controller to see what object needs to be retained or what message needs to be passed properly.
Look into Static Analyzer and Instruments for advanced debugging.
Credit:
The Basic Troubleshooting guide
Hope this helps. Good Luck

Resources