I would like to animate UI elements that are not supported by animateWithDuration:animations: and can't be animated by a transition.
First thing that comes to mind is doing something like this:
_animationStartDate = [NSDate date];
[self performSelector:#selector(manualAnimation) withObject:nil afterDelay:1.0/MYManualAnimationFrameRate];
- (void)manualAnimation
{
NSDate *now = [NSDate date];
NSTimeInterval interval = [now timeIntervalSinceDate:_animationStartDate];
CGFloat progress = interval / MYManualAnimationDuration;
if (progress < 1)
{
[self setValuesOfManualAnimationAtProgress:progress];
[self performSelector:#selector(manualAnimation:) withObject:number afterDelay:1.0/MYManualAnimationFrameRate];
} else {
[self setValuesOfManualAnimationAtProgress:1];
}
}
Before reinventing the wheel, is there any API for these kinds of animations?
If not, anything else I should take into account (e.g., calling cancelPreviousPerformRequestsWithTarget:selector:object: in dealloc, dynamic frame rate)? Would I gain something by using GCD?
Before reinventing the wheel, is there any API for these kinds of animations?
That would be CADisplayLink which "is a timer object that allows your application to synchronize its drawing to the refresh rate of the display"
You use it to call a selector with this signature whenever it's time to redraw
- (void) selector:(CADisplayLink *)sender;
In your drawing code you can use the timestamp of the display link to know how far into the animation to draw.
Related
I was wondering if there was a clean way to do a countdown timer with Grand Central Dispatch (then display for example in a UILabel) that's synchronized to the system clock... based on a reference date? — So if my reference date is an NSDate that's 20 minutes from now, I'd have a countdown displayed in seconds (don't worry about formatting) that's synced to the system clock.
Just doing a quick version of this skips seconds every once in a while in case the update method call doesn't arrive on schedule.
To me this seems like a pretty basic question, so I'm looking ideally for a basic/clean solution besides increasing the update interval to be 10x or something.
Also, the solution shouldn't use NSTimer.
I would use a recursive method like this:
- (void) updateTimer{
NSDate *now = [NSDate date];
NSTimeInterval secondsToDate = [now timeIntervalSinceDate:self.referenceDate];
_timerLabel.text = [NSString stringWithFormat:#"%.0f", secondsToDate];
if( secondsToDate < 0 ){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self updateTimer];
});
}else{
NSLog(#"Timer triggered!");
}
}
You just have to call it the first time, then it will update the countdown every 0.1 seconds.
Two fold question:
Am I using the afterDelay in a correct/efficient manor? I'm new to objective-c so just wanted to be sure I'm using it properly.
Whenever I call the this again before the time is up, it starts running two methods (which makes sense). After some research I found cancelPreviousPerformRequestWithTarget but it seems to cancel out the ability to call the method again. I wasn't sure if there was a better way to handle this.
[self beginShow:5 myIncrease:0.1];
- (void)beginShow:(CGFloat)duration myIncrease:(CGFloat)myIncrease{
CGFloat currentTime = 0.0;
while(duration > currentTime){
[self performSelector:#selector(setRed) withObject:nil afterDelay:currentTime];
[self performSelector:#selector(setTorchToLevelOn) withObject:nil afterDelay:currentTime];
currentTime = currentTime+myIncrease;
[self performSelector:#selector(setBlack) withObject:nil afterDelay:currentTime];
[self performSelector:#selector(setTorchToLevelOff) withObject:nil afterDelay:currentTime];
currentTime = currentTime+myIncrease;
[self performSelector:#selector(setPurple) withObject:nil afterDelay:currentTime];
[self performSelector:#selector(setTorchToLevelOn) withObject:nil afterDelay:currentTime];
currentTime = currentTime+myIncrease;
[self performSelector:#selector(setDefault) withObject:nil afterDelay:currentTime];
[self performSelector:#selector(setTorchToLevelOff) withObject:nil afterDelay:currentTime];
currentTime = currentTime+myIncrease;
}
}
try this code, i think you need this
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(method name) object:nil];
The answer to first question is NO. This logic could be performed using NSTimer and macro NS_ENUM to declare enumeration type YourState that has several states(for example YourStateDefault = 0, YourStateRed, YourStateBlack, YourStatePurple).
Why so? Because it gives you control on the logic flow and easy-to-maintain/debug solution. You could check what the state of the system just by referring to the currentState property(read below).
than
(void)beginShow:(CGFloat)duration myIncrease:(CGFloat)myIncrease {
check if a timer running, than deactivate it.
set currentState to the initial state value.
setup new timer with repeatInterval = myIncrease, set value of remainTime == duration, action:#selector(toggleState)
run timer.
}
(void) toggleState {
it should change current state saved in currentState property to the next one and call update UI logic. The toggle action should be performed only for positive remainTime value.
If remainTime became negative you should deactivate timer and return, if it positive run logic.
Determinate which state should go next using switch statement based on currentState value. Set current state to new one and run [self changeUIAccordingToState: currentState].
You also need decrease "remainTime" on every toggle action.
}
(void)changeUIAccordingToState:(YourState)state {
it should change color and torchToLevel accordingly
}
I am creating a simple drum machine. This function controls the time between each sample that is played (thus controlling the tempo of the drum machine). I need to control the tempo with a slider, so I'm hoping to be able to control the 'time duration until next step' value with this if possible. However, when I have tried to do this, it tells me "time is part of NSDate"
-(void)run
{
#autoreleasepool
{
// get current time
NSDate* time = [NSDate date];
// keeping going around the while loop if the sequencer is running
while (self.running)
{
// sleep until the next step is due
[NSThread sleepUntilDate:time];
// update step
int step = self.step + 1;
// wrap around if we reached NUMSTEPS
if (step >= NUMSTEPS)
step = 0;
// store
self.step = step;
// time duration until next step
time = [time dateByAddingTimeInterval:0.5];
}
// exit thread
[NSThread exit];
}
}
This tells me NSTimeInterval is an incompatable type
// time duration until next step
time = [time dateByAddingTimeInterval: self.tempoControls];
Here is where the slider is declared
.m
- (IBAction)sliderMoved:(UISlider *)sender
{
AppDelegate* app = [[UIApplication sharedApplication] delegate];
if (sender == self.tempoSlider)
{
PAEControl* tempoControl = app.tempoControls[app.editIndex];
tempoControl.value = self.tempoSlider.value;
}
}
.h
#interface DetailController : UIViewController
#property (weak, nonatomic) IBOutlet UISlider *tempoSlider;
- (IBAction)sliderMoved:(UISlider *)sender;
Any help would me much appriciated, thanks in advance.
It looks like self.tempoControls is an array of PAEControl objects. The method named dateByAddingTimeInterval: needs an argument of type NSTimeInterval (aka double). It looks like you're trying to pass in this array instead.
Try changing this line -
time = [time dateByAddingTimeInterval: self.tempoControls];
To maybe this -
PAEControl* tempoControl = self.tempoControls[self.editIndex];
time = [time dateByAddingTimeInterval: (NSTimeInterval)tempoControl.value];
On another note, if this is all running on the main thread, be aware that you are blocking it and the UI will become very unresponsive.
I'm building a "monitoring" app on my iPhone. I'm using AFNetworking-2.0. I have a backend server exposing a RESTful interface written in Python3/tornado.
Depending on what level of ViewController I'm at, I want to poll different data with different queries (the focus of the application tunes the focus of the queries). In the interest of "Make it Work", I have set up the following:
#pragma mark - Pull Loop
- (void) forkPull {
NSString* uri = [NSString stringWithFormat: #"%#/valves", Site.current.serialID];
[[HttpConnection current]
GET: uri
parameters: #{}
success:^(NSURLSessionDataTask* task, id responseObject){
[Site.current performSelectorOnMainThread: #selector(fromDoc:) withObject:responseObject waitUntilDone:YES];
NSTimeInterval delay = 60; // default poll period
// attempt to hone in if we have valid lastTouch info
if (Site.current.touched != nil) {
NSDate *futureTick = [Site.current.touched dateByAddingTimeInterval: 65];
if ([futureTick compare: [NSDate date]] == NSOrderedDescending) {
delay = futureTick.timeIntervalSinceNow;
}
}
[self performSelector: #selector(forkPull) withObject:nil afterDelay:delay];
NSLog(#"%# forkPull again in %f", self, delay);
}
failure:^(NSURLSessionDataTask* task, NSError* error){
NSLog(#"%# forkPull error: %# (uri=%#)", self, error, uri);
[self performSelector: #selector(forkPull) withObject:nil afterDelay:60];
}
];
}
- (void) stopPull {
[NSObject cancelPreviousPerformRequestsWithTarget: self];
}
#pragma mark - View Management
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear: animated];
....
[self forkPull]; // start up polling while I'm visible
}
-(void) viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self stopPull]; // I'm going away, so shut down the pull loop?
...
}
Basically, when the controller's view appears, it sends a REST query (when it gets back asynchronously, it will update the model in the fromDoc: methods; the controller has KVO relationships set up which will cause UI changes. After the update completes, it's able to approximate about when it should make the next pull, and schedules that with performSelector:withObject:afterDelay:. When another controller takes center stage, the viewWillDisappear: method attempts to stop any forkPulls that have been queued.
While this kinda works. I'm pretty sure it doesn't pass the "Make it Right" test. I'm naive about how all the tasks and backgrounding work, but it seems to me that AFNetworking adds its own level of them, so my stopPull might not be effective. I've seen some evidence of that with my NSLog output, where it seems that controllers that aren't on the top anymore, still have loops running.
But I'm sure others have done this kind of pattern before. I'd love to know how to better architect/implement this. I'm looking for someone to share the pattern they've used for doing the semi-periodic REST queries, that has been vetted and works well.
Use Grand Central Dispatch:
#property (strong, nonatomic) dispatch_source_t timer;
- (void)startTimer
{
if (!self.timer) {
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
}
if (self.timer) {
dispatch_source_set_timer(self.timer, dispatch_walltime(NULL, 0), 60ull*NSEC_PER_SEC, 10ull*NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^(void) {
[self tick];
});
dispatch_resume(_timer);
}
}
- (void)tick
{
// Do your REST query here
}
This will invoke your tick method every 60 seconds.
To suspend and resume your timer, use dispatch_suspend and dispatch_resume:
dispatch_suspend(self.timer);
dispatch_resume(self.timer);
You can invoke dispatch_source_set_timer at any time later to schedule ticks sooner or delay them until later:
// Fire sooner than 60 seconds, but resume 60s fires after that
unsigned long long delaySeconds = arc4random() % 60;
dispatch_source_set_timer(self.timer, dispatch_walltime(NULL, delaySeconds * NSEC_PER_SEC), 60ull*NSEC_PER_SEC, 10ull*NSEC_PER_SEC);
See the Apple Concurrency Programming Guide for full docs on this.
After setting mPlayer.usesExternalPlaybackWhileExternalScreenIsActive to YES in AVPlayerDemoPlaybackViewController of Apple's AVPlayerDemo sample project, how do you throttle the scrubbing so it doesn't lag behind on the AppleTV?
What I mean is that when you move the slider really fast back and forth the AppleTV performs each and every seekToTime operation, but takes longer to do it then the user takes to slide.
One of the problems with the demo is it uses both the "Touch Drag Inside" and "Value Changed" events which causes it to send the same value twice. If you remove "Value Changed" it improves a bit, but still lags.
I've tried rounding to whole seconds and then only send seekToTime when the second changes, but that doesn't seem to help as much. What I really need to do is send fewer commands the faster the user moves the slider, but more when the user moves slower.
Any ideas on how to accomplish this?
The UISlider already somewhat throttles itself. The faster you move it the fewer values you get from point A to point B. This isn't enough to stop the seek operations from stacking up over AirPlay.
You can, however, use the seekToTime:completionHandler: to prevent the stack up like this:
if(seeking) {
return;
}
seeking = YES;
[player seekToTime:CMTimeMakeWithSeconds(time, NSEC_PER_SEC) completionHandler:^(BOOL finished) {
seeking = NO;
}];
This drops any new seeks until the one in progress finishes. This seems to work well. You just need to make sure to send one last seek operation after the user stops scrubbing.
While an NSTimer can do the same thing, it's less accurate, and results will vary depending on the latency of the connection. The completionHandler used in this manner ensures that seeks do not stack up, regardless of latency times.
I also found that the "Value Changed" action of the UISlider can happen before any touch start actions. So it's better to use the touch drag inside/outside actions instead, which are guaranteed to happen after a touch start.
Improved Luke answer with some additional code:
static NSTimeInterval ToleranceForAsset(AVAsset *asset) {
NSTimeInterval tolerance = 0.0;
for (AVAssetTrack *track in asset.tracks) {
NSTimeInterval trackTolerance = CMTimeGetSeconds(track.minFrameDuration);
tolerance = MAX(tolerance, trackTolerance);
}
return tolerance;
}
#interface MyPlayerWrapper ()
#property (strong, nonatomic) AVPlayer *player;
#property (assign, nonatomic) NSTimeInterval playerTime;
#property (assign, nonatomic, getter=isSeeking) BOOL seeking;
#property (assign, nonatomic) CGFloat latestSetTime;
#end
#implementation MyPlayerWrapper
- (NSTimeInterval)playerTime {
return CMTimeGetSeconds(self.player.currentItem.currentTime);
}
- (void)setPlayerTime:(NSTimeInterval)playerTime {
NSTimeInterval tolerance = ToleranceForAsset(self.player.currentItem.asset);
if (tolerance) {
// round to nearest seek tolerance (for example 1/30 sec)
playerTime = floor(playerTime / tolerance) * tolerance;
}
self.latestSetTime = playerTime;
if (self.isSeeking) {
return;
}
self.seeking = YES;
[self.player seekToTime:CMTimeMakeWithSeconds(playerTime, self.player.currentItem.duration.timescale) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) {
self.seeking = NO;
if (ABS(self.player.currentItem.currentTime - latestSetTime) > MAX(tolerance, DBL_EPSILON)) {
self.playerTime = latestSetTime;
}
}];
}
#end