I'm using https://github.com/mineschan/MZTimerLabel/
and in my Tableview cellForRowAtIndex using the timer like below:
UILabel *lblTimer=(UILabel *)[cell viewWithTag:10];
MZTimerLabel *UpgradeTimer = [[MZTimerLabel alloc] initWithLabel:lblTimer andTimerType:MZTimerLabelTypeTimer];
[UpgradeTimer setCountDownTime:timestamp];
[UpgradeTimer startWithEndingBlock:^(NSTimeInterval timestamp) {
lblTimer.text = #"✔";
}];
But after any table reloading or scrolling, the timer behaves strange and seems it re-generates multiple timers for counting in the same place.
How should I fix this while using this timer?
Appreciate any help,
Elias
I had a look at MZTimerLabel, and it violates MVC badly. It puts something that belongs into the model (the timer that count's down the time) into the view. That is where your problem comes from. Views should be able to be recreated without having side effects on the model.
I would recommend to ditch that class, and create your own. It's actually quite easy to achieve something like this.
Create a new class that saves a title and a endDate
Store instances of that class in the model that backs your table
Create one NSTimer that refreshes the tableView
Set up your cells.
That's basically all the code you need for a basic countdown in a table. Because it does not store any data in the view you can scroll as much as you like:
#interface Timer : NSObject
#property (strong, nonatomic) NSDate *endDate;
#property (strong, nonatomic) NSString *title;
#end
#implementation Timer
#end
#interface MasterViewController () {
NSArray *_objects;
NSTimer *_refreshTimer;
}
#end
#implementation MasterViewController
- (void)viewDidLoad
{
[super viewDidLoad];
NSMutableArray *modelStore = [NSMutableArray arrayWithCapacity:30];
for (NSInteger i = 0; i < 30; i++) {
Timer *timer = [[Timer alloc] init];
timer.endDate = [NSDate dateWithTimeIntervalSinceNow:i*30];
timer.title = [NSString stringWithFormat:#"Timer %ld seconds", (long)i*30];
[modelStore addObject:timer];
}
_objects = modelStore;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[_refreshTimer invalidate]; // timer should not exist, but just in case.
_refreshTimer = [NSTimer timerWithTimeInterval:0.5f target:self selector:#selector(refreshView:) userInfo:nil repeats:YES];
// should fire while scrolling, so we need to add the timer manually:
[[NSRunLoop currentRunLoop] addTimer:_refreshTimer forMode:NSRunLoopCommonModes];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[_refreshTimer invalidate];
_refreshTimer = nil;
}
- (void)refreshView:(NSTimer *)timer {
// only refresh visible cells
for (UITableViewCell *cell in [self.tableView visibleCells]) {
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
[self configureCell:cell forRowAtIndexPath:indexPath];
}
}
#pragma mark - Table View
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return _objects.count;
}
- (void)configureCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
Timer *timer = _objects[indexPath.row];
cell.textLabel.text = timer.title;
NSInteger timeUntilEnd = (NSInteger)[timer.endDate timeIntervalSinceDate:[NSDate date]];
if (timeUntilEnd <= 0) {
cell.detailTextLabel.text = #"Finished";
}
else {
NSInteger seconds = timeUntilEnd % 60;
NSInteger minutes = (timeUntilEnd / 60) % 60;
NSInteger hours = (timeUntilEnd / 3600);
cell.detailTextLabel.text = [NSString stringWithFormat:#"%02ld:%02ld:%02ld", (long)hours, (long)minutes, (long)seconds];
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
[self configureCell:cell forRowAtIndexPath:indexPath];
return cell;
}
#end
Related
I want to set multiple running timer value in tableview so user easily checked how many time is remaining and stop any specific timer.
Multiple Timer Start and updated value display in NSLog perfectly.
User set NSTimer for particular recipe.
Now suppose user set timer for recipe1 in recipedetail and now back from that recipedetail.
Then again set timer for recipe2 in recipedetail and back from that recipedetail.
and i want to All timer value display in UITableView. And tableview placed on recipedetail screen.
So my point is TimerUpdated value display in NSLog but not display in tableview because every Recipedetails screen generate new object of UITableView So value is not updated perfactly
-(IBAction)okBtn:(id)sender
{
[self countdownTimer];
}
-(void)countdownTimer {
[[NSUserDefaults standardUserDefaults]setObject:recipeboxId forKey:[NSString stringWithFormat:#"timer_%#",recipeboxId]];
[[NSUserDefaults standardUserDefaults]synchronize];
// set timer
[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
(theAppDelegate).timer = [NSTimer scheduledTimerWithTimeInterval:0.6f
target:self
selector:#selector(updateCounter:)
userInfo:recipeboxId
repeats:YES];
[(theAppDelegate).timer fire];
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];//Timer run in background
[self CreateLocalNotification];
}
-(void)calculateTimeFromPicker
{
NSString *hoursStr = [NSString stringWithFormat:#"%#",[hoursArray objectAtIndex:[self.picker_timer selectedRowInComponent:0]]];
NSString *minsStr = [NSString stringWithFormat:#"%#",[minsArray objectAtIndex:[self.picker_timer selectedRowInComponent:1]]];
NSString *secsStr = [NSString stringWithFormat:#"%#",[secsArray objectAtIndex:[self.picker_timer selectedRowInComponent:2]]];
int hoursInt = [hoursStr intValue];
int minsInt = [minsStr intValue];
int secsInt = [secsStr intValue];
interval = secsInt + (minsInt*60) + (hoursInt*3600);
secondsLeft=interval;
}
- (void)updateCounter:(NSTimer *)theTimer {
if(secondsLeft > 0 )
{
secondsLeft -- ;
hours = secondsLeft / 3600;
minutes = (secondsLeft % 3600) / 60;
seconds = (secondsLeft %3600) % 60;
[[NSUserDefaults standardUserDefaults]setInteger:secondsLeft forKey:[NSString stringWithFormat:#"secondsLeft_%#",recipeboxId]];
[[NSUserDefaults standardUserDefaults]synchronize];
NSLog(#"timer :%#",[NSString stringWithFormat:#"%02d:%02d:%02d", hours, minutes, seconds]);
NSString *timer_updated_value=[NSString stringWithFormat:#"%02d:%02d:%02d", hours, minutes, seconds];
[[NSUserDefaults standardUserDefaults]setObject:timer_updated_value forKey:[NSString stringWithFormat:#"updated_timer_%#",[(theAppDelegate).timer userInfo]]];
[[NSUserDefaults standardUserDefaults]synchronize];
recipeArr = [[(theAppDelegate).Timer_recipeIdArr reverseObjectEnumerator] allObjects];
for (int section = 0; section < [recipeArr count]; section++)
for (int section = 0; section < [recipeArr count]; section++)
{
for (int row = 0; row < (int)[self.timerWindowTbl numberOfRowsInSection:section]; row++)
{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
UITableViewCell *cell = [self.timerWindowTbl cellForRowAtIndexPath:indexPath];
for(UILabel *lbl in [cell.contentView subviews])
{
if([lbl isKindOfClass:[UILabel class]])
{
if(lbl.tag == 1)
{
NSString *timer_desc= [[NSUserDefaults standardUserDefaults]objectForKey:[NSString stringWithFormat:#"timerDescString_%#",recipeArr[indexPath.row]]];
NSString *recipe_title=[[NSUserDefaults standardUserDefaults] objectForKey:[NSString stringWithFormat:#"timerRecipeTitle_%#",recipeArr[indexPath.row]]];
NSString *updated_timer=[[NSUserDefaults standardUserDefaults] objectForKey:[NSString stringWithFormat:#"updated_timer_%#",recipeArr[indexPath.row]]];
NSString *desc=[NSString stringWithFormat:#"%#\n'%#\' %# %#.",recipe_title,timer_desc,NSLocalizedString(#"is done in",nil),updated_timer];
lbl.text=updated_timer;
[lbl setNeedsDisplay];
NSLog(#"*******************************lbl.text:%#",lbl.text);
lbl.numberOfLines=0;
[lbl sizeToFit];
for(UIButton *btn in [cell.contentView subviews])
{
if([btn isKindOfClass:[UIButton class]])
{
if(btn.tag==2)
{
btn.titleLabel.text=[NSString stringWithFormat:#"%#",recipeArr[indexPath.row]];
}
}
}
break;
}
}
}
}
}
}
else
{
secondsLeft = hours = minutes = seconds = 0;
[self TimerInvalidate];
NSLog(#"Time out :");
}
}
i think u need to refractor your code, u need reload the tableview cell each time when recipe timer stared for particular recipe
this is sample demo u can try with separate project,
first create a Recipe class subclass of NSObject like below,
in Recipe .h file
#import <Foundation/Foundation.h>
#class Recipe;
#protocol RecipeDelegate <NSObject> //we need to notify which recipe time changing
- (void)timerChangedInRecipe:(Recipe *)recipe;
#end
#interface Recipe : NSObject
#property (nonatomic, assign) NSInteger recipeboxId;
#property (nonatomic, strong) NSString *recipeName;
#property (nonatomic, strong) NSString *countTimer;
#property (nonatomic, assign) NSTimeInterval secondsLeft;
#property (nonatomic, assign) NSTimeInterval interval;
#property (nonatomic, assign) int hours;
#property (nonatomic, assign) int minutes;
#property (nonatomic, assign) int seconds;
#property (nonatomic, strong) NSTimer *timer;
#property (nonatomic, assign) id<RecipeDelegate>delegate;
- (id)initWithRecipieId:(NSInteger)recipeId recipeName:(NSString *)name;
- (void )currentTimerString;
- (void)startTimer; //use to start the timer
- (void)stopTimer; //stop the timer
#end
in Recipe.m file
#import "Recipe.h"
#implementation Recipe
- (id)init
{
self = [super init];
if(self)
{
}
return self;
}
- (id)initWithRecipieId:(NSInteger)recipeId recipeName:(NSString *)name
{
self = [super init];
if(self)
{
self.recipeboxId = recipeId;
self.recipeName = name;
}
return self;
}
- (void)currentTimerString //this is similar to your method updateCounter:
{
self.secondsLeft -- ;
if(self.secondsLeft > 0 )
{
_hours = (int)self.secondsLeft / 3600;
_minutes = ((int)self.secondsLeft % 3600) / 60;
_seconds = ((int)self.secondsLeft %3600) % 60;
self.countTimer = [NSString stringWithFormat:#"%02d:%02d:%02d", self.hours, self.minutes, self.seconds];
if([self.delegate respondsToSelector:#selector(timerChangedInRecipe:)])
[self.delegate timerChangedInRecipe:self]; //notifying the tableview to reload the particular cell
}
else
{
_hours = _minutes = _seconds = 0;
[self stopTimer]; //timer finished stop the timer
}
}
- (void)startTimer
{
if(_timer == nil)
_timer = [NSTimer scheduledTimerWithTimeInterval:0.6f target:self selector:#selector(currentTimerString) userInfo:nil repeats:YES];
else
[_timer fire];
}
- (void)stopTimer
{
if(_timer)
[self.timer invalidate];
self.timer = nil;
}
#end
we are created the recipe object, this will handle the timer and notification to tableview, starting the timer and stoping the timer you can add some other functionalities if u want to.. :)
in ViewController.h file
#import <UIKit/UIKit.h>
#import "Recipe.h" //import the recipe class
#interface ViewController : UIViewController <UITableViewDataSource,UITableViewDelegate,RecipeDelegate>
#property (weak, nonatomic) IBOutlet UITableView *timerWindowTbl;
#property (nonatomic, strong) NSMutableArray *recipeArr; //array to hold the recipes
#end
in ViewController.m file
#import "ViewController.h"
#import "RecipeDetailViewController.h"
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
_recipeArr = [[NSMutableArray alloc] init];
[self populateRecipe]; //i am simply creating the recipes hear for testing
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
}
- (void)populateRecipe
{
for(NSInteger k = 0 ; k < 10 ; k++)
{
Recipe *recipe = [[Recipe alloc] initWithRecipieId:k recipe eName:[NSString stringWithFormat:#"recipe_%ld",(long)k]];
[self.recipeArr addObject:recipe];
}
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
//just configure your tableview
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.recipeArr.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"RECIPEE_CELL"];
if(cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:#"RECIPEE_CELL"];
}
Recipe *recipe = self.recipeArr[indexPath.row];
recipe.delegate = self;
cell.textLabel.text = recipe.recipeName;
cell.detailTextLabel.text = recipe.countTimer;
return cell;
}
//hear go to recipe detail controller and set timer
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
Recipe *selRecipe = self.recipeArr[indexPath.row];
RecipeDetailViewController *recipeDetailController = [[RecipeDetailViewController alloc] initWithNibName:#"RecipeDetailViewController" bundle:nil];
recipeDetailController.selectedRecipe = selRecipe;
[self.navigationController pushViewController:recipeDetailController animated:YES];
}
//this is the delegate method from recipe class
//this method is responsible for reloading the particular cell
- (void)timerChangedInRecipe:(Recipe *)recipe
{
NSInteger index = recipe.recipeboxId;
NSIndexPath *rowPath = [NSIndexPath indexPathForRow:index inSection:0];
[self.timerWindowTbl reloadRowsAtIndexPaths:#[rowPath] withRowAnimation:UITableViewRowAnimationNone];
}
#end
and in detail controller same as your recipe detail controller RecipeDetailViewController.h file
#import <UIKit/UIKit.h>
#import "Recipe.h"
#interface RecipeDetailViewController : UIViewController
#property (weak, nonatomic) IBOutlet UIDatePicker *timePicker;
#property (weak, nonatomic) IBOutlet UIButton *okButton;
#property (weak, nonatomic) IBOutlet UILabel *timerLabel;
#property (nonatomic, strong) Recipe *selectedRecipe;
- (IBAction)okBtn:(id)sender;
#end
and in RecipeDetailViewController .m file,
#import "RecipeDetailViewController.h"
#interface RecipeDetailViewController ()
#end
#implementation RecipeDetailViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
[_timePicker addTarget:self action:#selector(timerChanged:) forControlEvents:UIControlEventValueChanged]; //i am setting a sample picker
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)timerChanged:(UIDatePicker *)datePickerView
{
NSTimeInterval duration = datePickerView.countDownDuration;
int hours = (int)(duration/3600.0f);
int minutes = ((int)duration - (hours * 3600))/60;
int seconds = minutes/60;
_selectedRecipe.interval = seconds + (minutes*60) + (hours*3600);
_selectedRecipe.secondsLeft =_selectedRecipe.interval;
_timerLabel.text = [NSString stringWithFormat:#"%02d:%02d:%02d", hours, minutes, seconds];
_selectedRecipe.interval = duration;
}
- (IBAction)okBtn:(id)sender //hear start the timer particular recipe
{
[_selectedRecipe startTimer];
}
#end
Edit in the source code u are shared
the problem in the cell type i think, if you want u can subclass the tableview cell and place the timer labels in the required position,
so in your code, ViewController.m file replace below method
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"RECIPEE_CELL"];
if(cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"RECIPEE_CELL"];
}
Recipe *recipe = self.recipeArr[indexPath.row];
recipe.delegate = self;
cell.textLabel.text = recipe.recipeName;
cell.detailTextLabel.text = recipe.countTimer;
NSLog(#"timer in cell :%#",cell.detailTextLabel.text);
return cell;
}
and in the RecipeDetailViewController.m file replace,
//no need to wait for value change
- (IBAction)okBtn:(id)sender //hear start the timer particular recipe
{
NSTimeInterval duration = _timePicker.countDownDuration;
_selectedRecipe.interval = duration;
[_selectedRecipe startTimer];
}
and also change the timePicker mode to Count Down Timer in attribute inspector
u can download the edited code hear
Put UI related changes in main queue for better understanding show given below code. You can also put other UI related changes in Main_queue
dispatch_async(dispatch_get_main_queue(), ^{
lbl.text=updated_timer;
});
i have search thousand in GG to find solution update data to UITableViewCell but all show me the solution is
UITableViewCell *cell=(UITableViewCell*)[self.tableView cellForRowAtIndexPath:indexPath];
but the cell is nil for all cells that are visible. I have use NSNotification to send data from one method to ViewController.m , and the Reiever method i want update data to cell by indexPath. but all cell is nil and cannt not update that.
here my code
ViewController.h
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>
#property(nonatomic, strong) IBOutlet UITableView *tableView;
#end
ViewController.m
#implementation ViewController
{
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[[NSNotificationCenter defaultCenter]addObserver:self selector:#selector(theReciever:) name:#"theSender" object:nil];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [recipes count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleTableIdentifier = #"SimpleTableCell";
UITableViewCell* cell = [self.tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:simpleTableIdentifier];
NSLog(#"cell nil");
}
NSString *idgame=#"Gamexyz";
cell.textLabel.text = idgame;
cell.tag=indexPath.row;
return cell;
}
-(void)theReciever:(NSNotification*)notif{
if([notif.object isKindOfClass:[packeData class]]){
packeData *data=[notif object];
NSString *key=data.key;
NSInteger *index=[key integerValue];
NSIndexPath *indexPath=[NSIndexPath indexPathWithIndex:index];
UITableViewCell *cell=(UITableViewCell*)[self.tableView cellForRowAtIndexPath:indexPath];
//UITableViewCell *cell=(UITableViewCell*)[self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:7 inSection:0]];
if(cell==nil)
{
NSLog(#"cell NULL");
}else{
cell.textLabel.text=data.process;
}
}else{
NSLog(#"ERR: object not recognised");
}
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
any one can help me or solution example for update data in UITableViewCell by indexPath
NOTE: below is just an example u can do it in new project
One thing u need to change the data model packeData, lets say it contains key as NSIntager which holdes the index of the cell and process is NSString which holds the progress as string value for example
in packeData.h
#import <Foundation/Foundation.h>
#interface packeData : NSObject
#property (nonatomic, assign) NSInteger key; //holds index
#property (nonatomic, strong) NSString *process; //holds the progress info
#end
and in packeData.m
#import "packeData.h"
#implementation packeData
- (id)init //simply initialise it
{
self = [super init];
if(self)
{
}
return self;
}
#end
and in view controller where u are tableview,
in ViewController.h
#import <UIKit/UIKit.h>
#import "packeData.h"
#interface ViewController : UIViewController <UI TableViewDataSource,UITableViewDelegate>
#property (weak, nonatomic) IBOutlet UITableView *aTableView;
#property (strong,nonatomic) NSMutableArray *recipes; //array acts as datasource
#end
in in ViewController.m
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[[NSNotificationCenter defaultCenter]addObserver:self selector:#selector(theReciever:) name:#"theSender" object:nil];
_recipes = [[NSMutableArray alloc]init]; //initilise your datasource
for(int j = 0 ;j< 20;j++)
{
// for my example i took some values
//initially put some initial values
packeData *data = [[packeData alloc] init];
data.key = j;
data.process = [NSString stringWithFormat:#"game_name_%d",j];
[_recipes addObject:data];
}
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[NSTimer scheduledTimerWithTimeInterval:0.8 target:self selector:#selector(PostNotification) userInfo:nil repeats:YES]; //just for testing
}
- (void)PostNotification
{
//i am simply posting the notification with some random values
packeData *data = [[packeData alloc]init];
data.key = arc4random()%15;
data.process = [NSString stringWithFormat:#"%ld",( data.key + 20)];
[[NSNotificationCenter defaultCenter] postNotificationName:#"theSender" object:data];
}
- (void)theReciever:(NSNotification *)notif
{
if([notif.object isKindOfClass:[packeData class]]){
packeData *data=[notif object];
NSInteger key=data.key;
NSInteger index= key;
//modify the datasource
packeData *recipes_data = [_recipes objectAtIndex:index]; //get the pocket present in array
recipes_data.process = data.process; //modify the recipes data
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
UITableViewCell *cell=(UITableViewCell*)[self.aTableView cellForRowAtIndexPath:indexPath];
if(cell==nil)
{
NSLog(#"cell NULL");
}else
{
[self.aTableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
// cell.textLabel.text=data.process; no need u already mofied the content in the datasource this will call the "cellForRowAtIndexPath" method and displays the process in place of game name
}
}else{
NSLog(#"ERR: object not recognised");
}
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [_recipes count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleTableIdentifier = #"SimpleTableCell";
UITableViewCell* cell = [self.aTableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:simpleTableIdentifier];
NSLog(#"cell nil");
}
packeData *idgame= [_recipes objectAtIndex:indexPath.row];
cell.textLabel.text = idgame.process; //initially contains game name
cell.tag=indexPath.row;
return cell;
}
#end
EDIT
replace the below methods
- (void)PostNotification
{
//i am simply posting the notification with some random values
packeData *data = [[packeData alloc]init];
data.key = arc4random()%15; //15 change the number of rows
data.process = [NSString stringWithFormat:#"%ld",( data.key + arc4random() % 100)];
[[NSNotificationCenter defaultCenter] postNotificationName:#"theSender" object:data];
}
- (void)theReciever:(NSNotification *)notif
{
if([notif.object isKindOfClass:[packeData class]]){
packeData *data=[notif object];
NSInteger key=data.key;
NSInteger index= key;
//modify the datasource
packeData *recipes_data = [_recipes objectAtIndex:index]; //get the pocket present in array
recipes_data.process = data.process; //modify the recipes data
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
UITableViewCell *cell=(UITableViewCell*)[self.aTableView cellForRowAtIndexPath:indexPath];
if(cell==nil)
{
NSLog(#"cell NULL");
[self.aTableView reloadData]; //if cell is not visible then reload the whole table
}else
{
[self.aTableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
// cell.textLabel.text=data.process; no need u already mofied the content in the datasource this will call the "cellForRowAtIndexPath" method and displays the process in place of game name
}
}else{
NSLog(#"ERR: object not recognised");
}
}
Edit 2
as for testing just change the below method and as soon as simulator launches the app scroll to down so that only top 5 rows only updates, wait for 5 to 10 seconds and scroll to top and u will see all the calls are updates with same process 5
//scroll down as soon as launches the app and wait for 5 to 10 seconds then scroll to top u will see top 5 cells are updates with progress 5
- (void)PostNotification
{
packeData *data = [[packeData alloc]init];
data.key = arc4random()%5; //only top 5 cells are modify other wont modify
data.process = [NSString stringWithFormat:#"%ld",5];//updates with some same progress lates give it as 5 //( data.key + arc4random() % 100)];
[[NSNotificationCenter defaultCenter] postNotificationName:#"theSender" object:data];
}
form the above test u will see the top 5 cells are updates even when they are not visible
You can't set the value of any of your cell's controller apart from cellForRowAtIndexPath you have to populate the UITableViewCell data with an array, then when you want to update the data in your cell, update your array according to data, then update the single cell of your UITableView like this.
[tableView beginUpdates];
[tableView reloadRowsAtIndexPaths:#[indexPathOfYourCell] withRowAnimation:UITableViewRowAnimationNone];
[tableView endUpdates];
Just specify your index path of your row and reload...
NSIndexPath* path = [NSIndexPath indexPathForRow:3 inSection:0];
NSArray* rowsToReload = [NSArray arrayWithObjects:path, nil];
[tableView reloadRowsAtIndexPaths:rowsToReload withRowAnimation:UITableViewRowAnimationNone];
I am loading data into my UITableViewCell one at a time in the following method
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
_anIterationCounter++;
if (_anIterationCounter==_currentCount) {
if ((_currentCount+1)<=[_allTrains count]&&(_currentCount+1)<=5) {
_currentCount++;
_anIterationCounter=0;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_async(queue, ^{
[tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:YES]; //call apis here
});
}
}
the "_iterationCount" variable is initialised to be 0 in the beginning. Data is loaded from the array [_allTrains objectAtIndex:_currentCount] and I only want to show 5 cells atmost.
But my program has a further step where a didSelectRowAtIndexPath method exists.
But the control doesn't go into the delegate function until all the cells are done loading. How will I resolve this issue?
what about doing it like so?
#import "TableViewController.h"
#interface TableViewController ()
#property (strong, nonatomic) NSArray *regularData;
#property (strong, nonatomic) NSMutableArray *lazyLoadedData;
#property (strong, nonatomic) NSTimer *insertTimer;
#end
#implementation TableViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.tableFooterView = [[UIView alloc] init];
self.regularData = #[#"value1",
#"value2",
#"value3",
#"value4",
#"value5",
#"value6",
#"value7",
#"value8",
#"value9"];
self.lazyLoadedData = [#[] mutableCopy];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.insertTimer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:#selector(insertRow) userInfo:nil repeats:YES];
}
- (void)insertRow {
if (self.lazyLoadedData.count == 5) {
// maximum reached
[self.insertTimer invalidate];
self.insertTimer = nil;
return;
}
[self.lazyLoadedData addObject:self.regularData[self.lazyLoadedData.count]];
[self.tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:self.lazyLoadedData.count - 1 inSection:0]] withRowAnimation:UITableViewRowAnimationAutomatic];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.lazyLoadedData.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.textLabel.text = self.lazyLoadedData[indexPath.row];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"%s", __PRETTY_FUNCTION__);
}
#end
or take a look at my demo project:
https://www.dropbox.com/sh/oe1t4u8zksalmdy/AAAJO7G32YUhXfw7ixBcIQXla?dl=0
hope i got you right. :)
Rather than reloading the table each time, use the tableView's insertAtIndexPaths: method to insert cells one by one.
Change the
[tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:YES];
to
[tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];
Basically I set waitUntilDone to NO.
Use UISCrollView and create cell object add into the scrollview then load all the cells one at a time.
I'm using https://github.com/mineschan/MZTimerLabel/
and in my Tableview cellForRowAtIndex using the timer like below:
UILabel *lblTimer=(UILabel *)[cell viewWithTag:10];
MZTimerLabel *UpgradeTimer = [[MZTimerLabel alloc] initWithLabel:lblTimer andTimerType:MZTimerLabelTypeTimer];
[UpgradeTimer setCountDownTime:timestamp];
[UpgradeTimer startWithEndingBlock:^(NSTimeInterval timestamp) {
lblTimer.text = #"✔";
}];
But after any table reloading or scrolling, the timer behaves strange and seems it re-generates multiple timers for counting in the same place.
How should I fix this while using this timer?
Appreciate any help,
Elias
I had a look at MZTimerLabel, and it violates MVC badly. It puts something that belongs into the model (the timer that count's down the time) into the view. That is where your problem comes from. Views should be able to be recreated without having side effects on the model.
I would recommend to ditch that class, and create your own. It's actually quite easy to achieve something like this.
Create a new class that saves a title and a endDate
Store instances of that class in the model that backs your table
Create one NSTimer that refreshes the tableView
Set up your cells.
That's basically all the code you need for a basic countdown in a table. Because it does not store any data in the view you can scroll as much as you like:
#interface Timer : NSObject
#property (strong, nonatomic) NSDate *endDate;
#property (strong, nonatomic) NSString *title;
#end
#implementation Timer
#end
#interface MasterViewController () {
NSArray *_objects;
NSTimer *_refreshTimer;
}
#end
#implementation MasterViewController
- (void)viewDidLoad
{
[super viewDidLoad];
NSMutableArray *modelStore = [NSMutableArray arrayWithCapacity:30];
for (NSInteger i = 0; i < 30; i++) {
Timer *timer = [[Timer alloc] init];
timer.endDate = [NSDate dateWithTimeIntervalSinceNow:i*30];
timer.title = [NSString stringWithFormat:#"Timer %ld seconds", (long)i*30];
[modelStore addObject:timer];
}
_objects = modelStore;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[_refreshTimer invalidate]; // timer should not exist, but just in case.
_refreshTimer = [NSTimer timerWithTimeInterval:0.5f target:self selector:#selector(refreshView:) userInfo:nil repeats:YES];
// should fire while scrolling, so we need to add the timer manually:
[[NSRunLoop currentRunLoop] addTimer:_refreshTimer forMode:NSRunLoopCommonModes];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[_refreshTimer invalidate];
_refreshTimer = nil;
}
- (void)refreshView:(NSTimer *)timer {
// only refresh visible cells
for (UITableViewCell *cell in [self.tableView visibleCells]) {
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
[self configureCell:cell forRowAtIndexPath:indexPath];
}
}
#pragma mark - Table View
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return _objects.count;
}
- (void)configureCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
Timer *timer = _objects[indexPath.row];
cell.textLabel.text = timer.title;
NSInteger timeUntilEnd = (NSInteger)[timer.endDate timeIntervalSinceDate:[NSDate date]];
if (timeUntilEnd <= 0) {
cell.detailTextLabel.text = #"Finished";
}
else {
NSInteger seconds = timeUntilEnd % 60;
NSInteger minutes = (timeUntilEnd / 60) % 60;
NSInteger hours = (timeUntilEnd / 3600);
cell.detailTextLabel.text = [NSString stringWithFormat:#"%02ld:%02ld:%02ld", (long)hours, (long)minutes, (long)seconds];
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
[self configureCell:cell forRowAtIndexPath:indexPath];
return cell;
}
#end
The basics are are follows:
I need to control 2 times (NSDate) with 1 UIDatePicker. I created a new Single View project in XCode (v 4.4.1) just to start fiddling with the items I'll be needing. I used a storyboard in the creation of the project, easy peasy. In the storyboard I got rid of the view controller and dragged out a navigation controller. Removed the table view that was the root view controller and added a regular view controller as the root view controller. On there I place a button. I dragged out another view controller and connect that button on the root view controller to this new view controller using 'Push'. Very simple. I test it, it works, basic.
I create a class for the new view controller I just added and set the view controller in storyboard to be of that type. In storyboard I then add a UIDatePicker, a UIButton, and a UITableView (please keep in mind this is just me fiddling trying to make things work and trying to learn how to use everything, still quite new to this).
The code for my ViewController class is as follows:
NewViewController.h
#import <UIKit/UIKit.h>
#interface Reminders : UIViewController <UITableViewDataSource, UITableViewDelegate> {
NSInteger selectedRow;
}
#property (nonatomic, retain) NSDate *amTime;
#property (nonatomic, retain) NSDate *pmTime;
#property (nonatomic, retain) IBOutlet UITableView *tbl;
#property (nonatomic, retain) IBOutlet UIDatePicker *picker;
- (IBAction)timeChange:(id)sender;
- (IBAction)pickerChange:(id)sender;
- (NSString *)dateAsString: (NSDate *)date;
#end
NewViewController.m
#import "Reminders.h"
#interface Reminders ()
#end
#implementation Reminders
#synthesize tbl, picker;
#synthesize amTime, pmTime;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
NSLog(#"init");
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(#"did load");
amTime = [NSDate date];
NSLog(#"AM time viewDidLoad: %#", [self dateAsString:amTime]);
pmTime = [NSDate date];
NSLog(#"PM time viewDidLoad: %#", [self dateAsString:pmTime]);
selectedRow = (NSInteger)1;
}
- (void)viewDidUnload
{
[super viewDidUnload];
// Release any retained subviews of the main view.
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
-(NSInteger) numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
-(NSInteger) tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section
{
return 3;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
//NSLog(#"cell for row");
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"MyIdentifier"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:#"MyIdentifier"];
}
if (indexPath.row == 0) {
cell.textLabel.text = #"Dose";
cell.detailTextLabel.text = #"120";
}
else if (indexPath.row == 1) {
cell.textLabel.text = #"AM";
//cell.detailTextLabel.text = #"time am";
cell.detailTextLabel.text = [self dateAsString:amTime];
}
else if (indexPath.row == 2) {
cell.textLabel.text = #"PM";
//cell.detailTextLabel.text = #"time pm";
cell.detailTextLabel.text = [self dateAsString:pmTime];
}
if (selectedRow == indexPath.row) {
[tbl selectRowAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:0] animated:NO scrollPosition:0];
}
return cell;
}
-(void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == 0) {
selectedRow = 0;
}
else if (indexPath.row == 1) {
selectedRow = 1;
[picker setDate:amTime];
}
else if (indexPath.row == 2) {
selectedRow = 2;
[picker setDate:pmTime];
}
}
- (IBAction)timeChange:(id)sender {
if (selectedRow == 1) {
amTime = [NSDate date];
NSLog(#"time change: %#", [self dateAsString:amTime]);
}
else if (selectedRow == 2) {
pmTime = [NSDate date];
NSLog(#"time change: %#", [self dateAsString:pmTime]);
}
else {
}
[tbl reloadData];
}
- (IBAction)pickerChange:(id)sender {
if (selectedRow == 1) {
amTime = [picker date];
NSLog(#"PICKER time change: %#", [self dateAsString:amTime]);
}
else if (selectedRow == 2) {
pmTime = [picker date];
NSLog(#"PICKER time change: %#", [self dateAsString:pmTime]);
}
else {
NSLog(#"picker does NOTHING");
}
[tbl reloadData];
}
- (NSString *)dateAsString:(NSDate *)date {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:#"yyyy-MM-dd HH:mm:ss"];
NSString *stringFromDate = [formatter stringFromDate:date];
return stringFromDate;
}
Yes, I totally realize there are actions for both the button and date picker that are altering the times and the reason the button and corresponding action are there is because I started adding things in when I couldn't get it to work the way I thought it should have been working.
Anyway. The times seem to start off fine, but are some point are made invalid objective-c objects from what I can see by inspection. It chokes on the cellForRowAtIndexPath method when it tried to put the 'amTime' into the cell detail text.
Here's the kicker.... I created the same app, using the same steps I mentioned above, same code (copy pasted), on another machine (one that has proper code signing keys), and that version works properly, even if I run that project on my current machine. When I create the project on my current machine, it crashes.
I apologize for the length post but I wanted to try to give as much detail as possible.
It seems you have variables being deallocated too soon. I would save amTime and pmTime in instance variables, and access them in my code through self.amTime and self.pmTime: since they are properties, getters/setters methods will retain/release appropriately.
There is a wonderful three-part tutorial on raywenderlich.com that explain the basics of memory management in iOS development: a must read.
http://www.raywenderlich.com/2657/memory-management-in-objective-c-tutorial
ARD did the trick without much effort on my part but without ARC it was a matter of throwing in some retains on the amTime and pmTime variables.