THE SCENARIO I need a method to fire off every second. I also need to be able to stop the firing of the method at any time. At the moment I am using an NSTimer:
THE CODE
self.controlTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:#selector(updatePlayer) userInfo:nil repeats:YES];
THE ISSUE I am certain I can achieve this functionality using an NSTimer and call invalidate when I want it to stop, however I am concerned about the performance overhead of placing an NSTimer in a UITableViewCell.
THE QUESTION Does anyone know of a more light-weight alternative to calling a method every second?
I have used NSTimer instances inside of UITableViewCell and UICollectionViewCell custom subclasses to do what you are doing, but I created a protocol PLMMonitor to provide -startMonitoring and -stopMonitoring contracts on my cells to start/stop (see: invalidate) any timing mechanisms.
The Protocol
(Obviously the protocol name prefix can be easily changed)
#protocol PLMMonitor <NSObject>
#required
- (void)startMonitoring;
- (void)stopMonitoring;
#end
Using Cell Visibility to Control the Timers
I could then utilize -[UITableViewDataSource tableView:cellForRowAtIndexPath:] or -[UICollectionViewDelegate collectionView:willDisplayCell:forItemAtIndexPath:] to call -startMonitoring on the cell if it conforms to the protocol (allows for mixed cells in the UITableView/UICollectionView):
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath
{
if ([cell conformsToProtocol:#protocol(PLMMonitor)])
{
[(UICollectionViewCell<PLMMonitor> *)cell startMonitoring];
}
}
Then I utilized the -[UITableViewDelegate tableView:didEndDisplayingCell:forRowAtIndexPath:] or -[UICollectionViewDelegate collectionView:didEndDisplayingCell:forItemAtIndexPath:] to call -stopMonitoring on the cell if it conformed to the protocol (again allowing for mixed cells in the UITableView/UICollectionView):
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath
{
if ([cell conformsToProtocol:#protocol(PLMMonitor)])
{
[(UICollectionViewCell<PLMMonitor> *)cell stopMonitoring];
}
}
Using View Controller Visibility to Control the Timers
You should also add code to -viewWillAppear and -viewWillDisappear to -startMonitoring and -stopMonitoring on the visible cells that conform to the protocol to ensure the timers get started/stopped appropriately when they are no longer visible:
- (void)viewWillAppear
{
for (UICollectionViewCell *aCell in [self.collectionView visibleCells])
{
if ([aCell conformsToProtocol:#protocol(PLMMonitor)])
{
[(UICollectionViewCell<PLMMonitor> *)aCell startMonitoring];
}
}
}
- (void)viewWillDisappear
{
for (UICollectionViewCell *aCell in [self.collectionView visibleCells])
{
if ([aCell conformsToProtocol:#protocol(PLMMonitor)])
{
[(UICollectionViewCell<PLMMonitor> *)aCell stopMonitoring];
}
}
}
Performance Implications / Energy Usage of NSTimers
One way you can reduce the impact NSTimer instances have on battery life, etc is the make use of their tolerance property which allows iOS to do some power savings magic with them while sacrificing a strict firing interval.
Alternative Timer/Trigger Mechanisms
You can utilize Grand Central Dispatch's (GCD) dispatch_after()
mechanism, but you will lose the ability to cancel the invocation.
Another option is to utilize -[NSObject
performSelector:withObject:afterDelay:] methods and the
accompanying +[NSObject
cancelPreviousPerformRequestsWithTarget:selector:object:]
method to schedule a selector to be invoked and cancel an invocation
respectively.
NSTimer is pretty lightweight. You just need to make sure you properly handle the Cell's timer when the cell is reused.
Related
In UICollectionView when i select a cell, it opens a new UIViewController where some info is displayed.
- (void)collectionView:(UICollectionView *)collectionView
didDeselectItemAtIndexPath:(NSIndexPath *)indexPath
{
MyClass *myClass = [myClassArray objectAtIndex:indexPath.item];
[myClass MyClassInstanceMethod];
}
In new UIViewController some info about myClass instance is printed. However, in MyClassInstanceMethod i make some HTTP requests which get some data and assign to myClass's properties. As you may guess, the problem is HTTP requests delay and i am not able to get data to be assigned before new view get load.
Then i think that i should let my UICollectionView to open selected cell's page after MyClassInstanceMethod finishes it job. How can i implement that? Thanks in advance.
As my understanding,
you have to make http call in
- (void)collectionView:(UICollectionView *)collectionView
didDeselectItemAtIndexPath:(NSIndexPath *)indexPath;
and in success response you should open your UIViewController with Details you want to show in it.
To keep your method encapsulated by the class, you should put a completion handler in your MyClassInstanceMethod. Like
- (void)MyClassInstanceMethodWithCompletion:(void (^)(BOOL finished))completion {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Do your HTTP Requests here
dispatch_async(dispatch_get_main_queue(), ^{ // This will be called just when the http requests finish
if (completion) { // Here you could check it out if the requests went well
completion(YES); // If YES return YES
}
});
});
}
Inside of the didDeselectItemAtIndexPath: you can call this like:
- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath {
MyClass *myClass = [myClassArray objectAtIndex:indexPath.item];
[myClass MyClassInstanceMethodWithCompletion:^(BOOL finished) {
if(finished){
// Call the segue
}
}];
}
I hope this can help.
ps: you have used a method name starting with uppercase MyClass... usually it's better a lowercase myClass... it's a better approach just to differentiate a Class name of a Method name.
In my project I'm creating custom cells by subclassing UITableViewCell. When cellForRowAtIndexPath: is fired I do a pretty basic stuff like:
MyCustomCell *cell = [self.tableView dequeueReusableCellWithIdentifier:[MyCustomCell identifier]];
I don't want to manually configure cell properties in cellForRowAtIndexPath: so I thought I'd create a method inside MyCustomCell called configureWithModel: which is filling MyCustomCell with proper data. So far, so good! Now inside cellForRowAtIndexPath: I have something like:
MyCustomCell *cell = [self.tableView dequeueReusableCellWithIdentifier:[MyCustomCell identifier]];
[cell configureWithModel:model];
In configureWithModel: I assign some data (image also) to cell so as you'd guess it could be slow'n'heavy so I wonder if this is a good solution to have a method like this in subclass of MyCustomCell? What is more, how it's related to prepareForReuse?
Doing this [cell configureWithModel:model]; is the best approach because take for a case when you want to use configureWithModel: in more than 2 tableViews you can avoid code redundancy and cell level control would be there with cell itself.
Use of [cell configureWithModel:model]; will make your code look like more structured, but for image use the following delegate
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
Example :
- (void)tableView:(UITableView *)tableView willDisplayCell:(AlbumCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
AlbumBO *album = [self.arrAlbums objectAtIndex:indexPath.row];
dispatch_async(imageQueue_, ^{
UIImage *image = [self retrieveImageWithImageUrl:album.coverPhoto];
dispatch_async(dispatch_get_main_queue(), ^{
[cell.imgVwAlbum setImage:image];
});
});
}
Here
AlbumCell is my Custom table cell
AlbumBO is the object for containing image object
And
[self retrieveImageWithImageUrl:album.coverPhoto]
is the user defined method to download image.
This sounds like a fairly decent usage of the singular responsibility principle. Where this might bite you is if your cells need to be binded with images that must be downloaded from a server. In this instance you don't want your cell responsible for triggering a download since the cell will then also be responsible for monitoring the progress of the download. Since these cells are reusable this becomes more problematic as the cell becomes reused.
So yes, in a simple case where you need to bind data to a cell it makes sense for the cell to be responsible for configuring its subviews with the relevant data.
Regarding prepareForReuse a casual glance at the documentation details
Discussion If a UITableViewCell object is reusable—that is, it has a
reuse identifier—this method is invoked just before the object is
returned from the UITableView method
dequeueReusableCellWithIdentifier:. For performance reasons, you
should only reset attributes of the cell that are not related to
content, for example, alpha, editing, and selection state. The table
view's delegate in tableView:cellForRowAtIndexPath: should always
reset all content when reusing a cell. If the cell object does not
have an associated reuse identifier, this method is not called. If you
override this method, you must be sure to invoke the superclass
implementation.
I have the following UITableViewCell (well, subclassed).
With didSelectRowAtIndexPath it is possible to capture that a cell has been selected in UITableViewController. My problem occurs due to the fact that directly pressing Choose User bypasses the selection of the cell.
How could I allow my UITableViewController to be aware that UITableViewCell foo has been pressed even if the user immediately hits Choose User?
N.B. I don't need the Selection capability per se, this was just by method of knowing that a user had tapped within a cell area.
You could just call the method directly. If we say that for each Choose User button we are setting the row number as the tag and assuming that you don't have sections so everything will happen in section 0 we could do.
- (void)hitChooseUser:(id)sender
{
// Do whatever you want for when a user hits the `Choose User` button
// Code......
// Then do this at the end or whenever you want to do it.
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[sender tag] inSection:0];
// Also assuming you have created you have created you UITableView correctly.
[self tableView:myTableView didSelectRowAtIndexPath:indexPath];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Do whatever it is you want.
}
I also found this link that may help you Manually call didSelectRowatIndexPath
You could also disable the user interaction with the cell itself by setting userInteractionEnabled: to NO for each cell in cellForRowAtIndexPath: so didSelectRowAtIndexPath: will only get called when you want to call it manually.
Do not call didSelectRowAtIndexPath: It is a UITableViewDelegate method and, where possible, should be used as such (meaning let the UITableView send messages to it). In addition, it creates an unnecessary dependency on UITableView implementation.
That being said, in order to achieve shared behavior that is performed either on button click, or on row selection, refactor it out into a common method that is not coupled with UITableViewDelegate
For example:
-(void)doSomethingCommon {
//do shared code here
}
-(void)chooseUserButtonPressed:(id)sender {
[self doSomethingCommon];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self doSomethingCommon];
}
And if your UITableView shows more than one of these rows, for which you depend on knowing which corresponding model object is related to the cell, than you can use the tag property on UIView subclasses (usually something in your cell) to mark the row that the object is shown in.
I am using the standard table view datasource protocol to delete table cells:
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if(editingStyle == UITableViewCellEditingStyleDelete)
{
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
I would like to run some other code once the animation is completed but the deleteRowsAtIndexPaths:withRowAnimation method does not have a completion block. How else would I run some code after this method completes?
For iOS 6 Apple added - tableView:didEndDisplayingCell:forRowAtIndexPath: to UITableViewDelegate. You should be able to use it to get a callback immediately when each UITableViewCell has been removed from the display, so if you know you've started a deletion animation on a particular index path then you can use it as a usually reliable means of knowing that the animation has ended.
(aside: I guess if the user scrolled the cell off screen during its animation then you could get a false positive, but such things are going to be so unlikely that I'd be likely to add a basic guard against persistent negative consequences and not worry about the ephemeral ones, such as if I end up showing an empty cell when the mid-deletion object is scrolled back onto screen because I already removed it from my store)
I believe one way to do this is to implement UITableViewDataSource's method tableView:commitEditingStyle:forRowAtIndexPath: and execute a delayed performance method there inside.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (UITableViewCellEditingStyleDelete == editingStyle) {
[self performSelector:#selector(delayedMethod) withObject:nil afterDelay:0.1];
}
}
-(void)delayedMehtod {
// Your code here...
}
It may not be as pretty as a "completion" block but I'm sure it would do the trick.
Hope this helps!
I implement a UITableView of UIImageView cells, each of which periodically refreshes itself every 5 seconds via NSTimer. Each image is loaded from a server in the background thread, and from that background thread I also update the UI, displaying the new image, by calling performSelectorOnMainThread. So far so good.
The problem I noticed is the number of threads is increasing over time and UI becomes non-responsive. Therefore, I want to invalidate NSTimer if a cell goes off screen. Which delegation methods in UITableView should I use to do this efficiently?
The reason why I associate an NSTimer with each cell because I don't want image transition to occur at the same time for all cells.
Is there any other methods to do this by the way? For example, is it possible to use just a single NSTimer?
(I can't use SDWebImage because my requirement is to display a set of images in loop loaded from a server)
// In MyViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
...
NSTimer* timer=[NSTimer scheduledTimerWithTimeInterval:ANIMATION_SCHEDULED_AT_TIME_INTERVAL
target:self
selector:#selector(updateImageInBackground:)
userInfo:cell.imageView
repeats:YES];
...
}
- (void) updateImageInBackground:(NSTimer*)aTimer
{
[self performSelectorInBackground:#selector(updateImage:)
withObject:[aTimer userInfo]];
}
- (void) updateImage:(AnimatedImageView*)animatedImageView
{
#autoreleasepool {
[animatedImageView refresh];
}
}
// In AnimatedImageView.m
-(void)refresh
{
if(self.currentIndex>=self.urls.count)
self.currentIndex=0;
ASIHTTPRequest *request=[[ASIHTTPRequest alloc] initWithURL:[self.urls objectAtIndex:self.currentIndex]];
[request startSynchronous];
UIImage *image = [UIImage imageWithData:[request responseData]];
// How do I cancel this operation if I know that a user performs a scrolling action, therefore departing from this cell.
[self performSelectorOnMainThread:#selector(performTransition:)
withObject:image
waitUntilDone:YES];
}
-(void)performTransition:(UIImage*)anImage
{
[UIView transitionWithView:self duration:1.0 options:(UIViewAnimationOptionTransitionCrossDissolve | UIViewAnimationOptionAllowUserInteraction) animations:^{
self.image=anImage;
currentIndex++;
} completion:^(BOOL finished) {
}];
}
willMoveToSuperview: and/or didMoveToSuperview: do not work on ios 6.0
from ios 6.0 you have the following method of UITableViewDelegate
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
Use this method to detect when a cell is removed from a table view, as
opposed to monitoring the view itself to see when it appears or
disappears.
If you properly manage memory and dequeue reusable cells, you can subclass UITableViewCell and override its - prepareForReuse method in order to stop the timer.
Furthermore, as #lnfaziger points out, if you want to stop the timer immediately when the cell is removed from the table view, you can also override its willMoveToSuperview: and/or didMoveToSuperview: method and check if its superview parameter is nil - if it is, the cell is being removed, so you can stop the timer.