I'm optimizing the scrolling smoothness of a UITableView, but it's tough to notice subtle gains by eye. I'm looking for a straightforward way to print out the "load time" of each UITableViewCell as it appears. Is this possible?
but it's tough to notice subtle gains by eye.
Don't try. Use Instruments! That's what it's for. The Core Animation instrument will tell you the FPS when you scroll, which is exactly what you want to know here. And the Time Profiling instrument will tell you exactly where in your code the time is being spent.
Don't guess. Don't eyeball. Don't add your own timing instrumentation. Use Instruments!
You can do two things:
1: Measure execution time for - tableView:cellForRowAtIndexPath:
2: Measure execution time for - reloadData
Measuring:
- (UITableViewCell * _Nonnull)tableView:(UITableView * _Nonnull)tableView cellForRowAtIndexPath:(NSIndexPath * _Nonnull)indexPath {
CFTimeInterval startTime = CACurrentMediaTime();
//Do your thing - dequeue, setup your cell.
CFTimeInterval endTime = CACurrentMediaTime();
NSLog(#"Cell Creation: %g s", endTime - startTime);
return cell;
}
and wherever you call reloadData()
CFTimeInterval startTime = CACurrentMediaTime();
[self.tableView reloadData];
CFTimeInterval endTime = CACurrentMediaTime();
NSLog(#"Cell Creation: %g s", endTime - startTime);
you can use the mach absolute time for the most accurate results. there is a good explanation here: https://developer.apple.com/library/mac/qa/qa1398/_index.html
example code:
uint64_t startTime = mach_absolute_time();
// do work here
uint64_t endTime = mach_absolute_time();
uint64_t elapsedTime = endTime - startTime;
mach_timebase_info_data_t sTimebaseInfo;
mach_timebase_info(&sTimebaseInfo);
uint32_t numer = sTimebaseInfo.numer;
uint32_t denom = sTimebaseInfo.denom;
uint64_t elapsedNano = (elapsedTime * numer / denom);
NSTimeInterval elapsedSeconds = elapsedNano / (CGFloat)NSEC_PER_SEC;
NSLog(#"elapsed time: %f", elapsedSeconds);
Related
Problem:
If you take a look at my current code, you'll see that it works fine if targetSeconds is higher than ~2-3 seconds.
However, it will not work if targetSeconds is 0.005 seconds because there's no way it can finish 100 method calls in 0.005 seconds. Therefore, does anyone have any suggestions to what I can do to improve it? I'd rather not include third party GitHub repositories.
Current code:
// Target seconds means the seconds that it'll take to become 100.0f.
- (void)startAnimatingWithTargetSeconds:(NSTimeInterval)targetSeconds{
// Try to set timeInterval to 0.005f and you'll see that it won't finish in 0.005f
[NSTimer scheduledTimerWithTimeInterval:targetSeconds / 100.0f target:self selector:#selector(animateWithTimer:) userInfo:nil repeats:YES];
}
- (void)animateWithTimer:(NSTimer *)timer {
BOOL isFinished = self.currentProgress >= 100;
if (isFinished) {
// Invalidate timer
if (timer.isValid) {
[timer invalidate];
}
// Reset currentProgress
self.currentProgress = 0.0f;
}else{
if (timer.isValid) {
self.currentProgress += 1;
}
}
}
// Overriden setter
- (void)setCurrentProgress:(CGFloat)currentProgress {
if (_currentProgress == currentProgress) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
_currentProgress = currentProgress;
[self setNeedsDisplay];
});
}
And then in drawRect, I have an UIBezierPath that basically draws the circle depending on self.currentProgress.
Something like this: CGFloat endAngle = (self.currentProgress / 100.0f) * 2 * M_PI + startAngle;
Question:
Is there any formula or anything that'll help me in my case? Because if I were to set self.currentProgress to self.currentProgress += 5; instead of 1, it'll animate a lot faster, which is precisely what I'm looking for.
First of all, when would you want to redraw every 0.005 seconds? That's 200 FPS, way more than you need.
Don't reinvent the wheel – leverage Core Animation! Core Animation already knows how to call your state change function at the proper rate, and how to redraw views as necessary, assuming you tell it what to do. The gist of this strategy is as follows:
Add a dynamic property to your layer that represents the completeness of your pie slice.
Tell Core Animation that this property can be animated by either overriding actionForKey:, or setting the animation into the actions dictionary (or even more options, detailed here).
Tell Core Animation that changes to this property require redraws of the layer using needsDisplayForKey:.
Implement the display method to redraw the pie based on the presentation layer's value of your dynamic property.
Done! Now you can animate the dynamic property from any value to any other, just as you would opacity, position, etc. Core Animation takes care of the timing and the callbacks, and you get a buttery smooth 60 FPS.
For some examples, see the following resources, listed in order of decreasing usefulness (in my opinion):
Animating Pie Slices using a custom CALayer – this is basically what you want to do
Animating Custom Layer Properties – better written but a bit less applicable
Apple's Core Animation Guide – esoteric, but worth a read if you want to master the strange beast that is Core Animation
Good luck!
I prefer use something like this, because timer (and usleep) on small intervals works very inaccurately:
-(void)startAnimatingWithTargetSeconds:(NSTimeInterval)targetSeconds
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
float fps = 60;
float currentState = 0;
float frameStateChange = fps/targetSeconds;
NSDate *nextFrameDate = [NSDate date];
while (currentState < 1) {
currentState += frameStateChange;
self.currentProgress = roundf(currentState * 100.);
nextFrameDate = [nextFrameDate dateByAddingTimeInterval:1./fps];
while ([nextFrameDate timeIntervalSinceNow] > 0) {
usleep((useconds_t)(100000/fps));
}
}
self.currentProgress = 0;
});
}
I am getting millisecond and i want to convert that millisecond to day,hours,minutes,second and display and want to display it in uiTableview. and i want to update that timer every second..
i had try this code using uiTableview delegate method cellForRowAtIndexPath.
NSInteger totleTime = [[[productListDataArray objectAtIndex:indexPath.row]objectForKey:#"_auction_duration"] integerValue];
totleTime = totleTime - timerValue ;
long days = (totleTime) / (60 * 60 * 24);
long hours = (totleTime - days * 60 * 60 * 24) / (60 * 60);
long minuts = (totleTime - days * 60 * 60 * 24 - hours * 60 * 60) / 60;
long seconds = (totleTime - days * 60 * 60 * 24 - hours * 60 *60 - minuts * 60);
NSLog(#"seconds : %.f minutes : %.f hours : %.f days : %.f ", seconds, minuts, hours, days);
I had used this method for call method and update tableview. but its not working so suggest me another option or tell me what i had done wrong in this code..
What I understand from your question is that you want to reload your tableView every second. First of all, let me say that it is a bad idea. If your tableView is being reloaded then the user will not be able to see anything. Having said that, you can use a timer with a certain interval that calls a method. That method can decide when to reload the tableView.
EDIT
From your last comment, I understand that you are trying to display a countdown timer in a tableview cell. For that purpose, you don't need to reload the tableview. You only have to update the datasource and reload the row that is displaying the counter. Something along these lines.
The code snippet below is in Swift 2
//Call a method named tick() every second
let timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: #selector(tick), userInfo: nil, repeats: true)
func tick() {
//DO YOUR CALCULATIONS HERE TO CALCULATE YOUR DAYS, MINUTES, etc. THEN CALL THE FOLLOWING METHOD
tblView.reloadRowsAtIndexPaths([NSIndexPath(forRow: {rowNumber}, inSection: {sectionNumber})], withRowAnimation: .None)
//ALSO MAKE SURE YOUR cellForRowAtIndexPath IS CORRECTLY SETTING THE CELL VALUES
}
Don't forget to invalidate the timer when you're finished with it
i have get solve this issues.
1.At initial i had set timer is 1 second.
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(updateCounter:) userInfo:nil repeats:YES];
2.Every Second Call Method and change value and reload tableview.
- (void)updateCounter:(NSTimer *)theTimer {
timerValue += 1;
[ProductListTableView reloadData];
}
3. now calculate remaining time in uitableView deleget method cellForRowAtIndexPath
NSInteger totleTime = [[[productListDataArray objectAtIndex:indexPath.row]objectForKey:#"_auction_duration"] integerValue];
totleTime = totleTime - timerValue ;
long days = (totleTime) / (60 * 60 * 24);
long hours = (totleTime - (days * 60 * 60 * 24)) / (60 * 60) ;
long minuts = (totleTime - (days * 60 * 60 * 24) - (hours * 60 * 60)) / 60;
long seconds = (totleTime - (days * 60 * 60 * 24) - (hours * 60 * 60) - (minuts * 60));
cell.saleTimeLabel.text = [NSString stringWithFormat:#"%# Sold %ld:%ld:%ld:%ld",[[productListDataArray objectAtIndex:indexPath.row]objectForKey:#"sales_qty"], days,hours,minuts,seconds];
I don't know timeValue's meanings, if timeValue assume system time. you can write a timer to reload tableView's data.
Ok so recently I made a project which had like a bit of a gravity timer,
int speed = 5;
NSTimer *gravitytimer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:#selector(gravity) userInfo:nil repeats:YES];
-(void)gravity {
image.center = CGPointMake(image.center.x, image.center.y - speed)
}
The problem is that the speed keeps multiplying or keeps adding and the image goes faster and faster. I don't know what the problem is. I am a newbie so sorry if this is a bad question. Thanks to all the people who take the time to answer.
When you create a timer, while it will try to call it every 0.01 seconds, you actually have no assurances that it will be called precisely at that rate (i.e. if it takes too long, it may skip one or two; if the main thread is blocked doing something else (and it's quite busy when the app first starts), you may skip quite a few). You can request to update image.center 100 times per second, but you have no guarantee that this is how often it actually will end up getting called.
If you wanted to use this technique to animate, you do so in a manner that isolates the speed of the animation from the frequency of the timer's selector is actually called. So, you might capture the start time, e.g.
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
CGPoint startPoint = image.center;
And then in your timer routine, you'd capture the elapsed time, multiply your speed times the elapsed time, and go from there:
-(void)gravity {
CFAbsoluteTime elapsed = CFAbsoluteTimeGetCurrent() - self.start;
CGFloat speed = 100.0; // e.g. 100 points per second
CGPoint center = startPoint;
center.y += (elapsed * speed);
image.center = center;
}
As an aside, I assume this is just an exercise in timers. If this were a real animation, there are far more effective ways to do such animation (e.g. block-based animateWithDuration, UIKit Dynamics, and if you really wanted to do something timer-based, you'd often use CADisplayLink rather than a NSTimer). Also, BTW, the frame rate of the display is 60fps, so there's no point in trying to call it more frequently than that.
I have the code below to animate a score label. How would you go about changing it so it becomes an ease out animation?
Thanks
- (void)animateFrom:(float)fromValue toValue:(float)toValue
{
self.scoreAnimationFrom = fromValue;
self.scoreAnimationTo = self.question.correctValue;
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:#selector(animateNumber:)];
self.startTimeInterval = CACurrentMediaTime();
[link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)animateNumber:(CADisplayLink *)link
{
float dt = ([link timestamp] - self.startTimeInterval) / self.duration;
if (dt >= 1.0)
{
[link removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
return;
}
float current = ((self.scoreAnimationTo - self.scoreAnimationFrom) * dt + self.scoreAnimationFrom);
self.valueLabel.text = [NSString stringWithFormat:#"%f", current];
}
The progress of your animation is described by the dt variable, which is a value between 0 and 1. Applying an ease out timing to your animation is as simple as funnelling this value into an appropriate timing function before applying it further. The responsibility of a timing function is to turn the original value into another value between 0 and 1, following a specific timing curve. For more information about timing functions, simply refer to Apple documentation.
In your case, you therefore need to apply an ease out timing function to dt, something like:
dt = [[TimingFunction easeOutTimingFunction] solveForInput:dt];
Core Animation provides the CAMediaTimingFunction class, but sadly its _solveForInput: solving method is private. There exists several open-source implementations of timing functions that you can use instead, e.g. https://github.com/warrenm/AHEasing.
If you are curious, I also recently implemented a method equivalent to _solveForInput: within a category of CAMediaTimingFunction.
I have made a very simple solution by using a sin function instead of the complex curve calculation with polynomial coefficients:
//EasyOut
dt = sin(dt*M_PI/2)+0.01f;
Enjoy it :)
I am having a problem with displaying time on UITableViewCell
unsigned long long seconds = milliseconds/1000;
unsigned long long minutes = seconds/60;
unsigned long long hours = minutes/60;
seconds -= minutes * 60;
minutes -= hours * 60;
NSString * result1 = [NSString `enter code here`stringWithFormat:#"%02lluH:%02lluM:%02lluS",hours,minutes,seconds];
self.menushowTime.text = result1;//populate cell label with time
Whenever I am reloading the table view my timer is running with 2x speed, I mean very fast. How can I prevent this from happening.
facing problem when i start scrolling tableview or reloading the tableview.?
Yes it happens, because timer is attached to NSRunLoop, so when the UI thread is busy doing animations , NSRunLoop has to halt. It finishes animations and later increments your timer. so the timer wont increase perfectly during this time.
use below one,
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
first calculate minutes, later seconds.
Change your code to below.
minutes -= hours * 60;
seconds -= minutes * 60;