I have a method that runs concurrently with recording a video. When the method ends it fires off a chain of other methods that continues until the recording ends. I want to be able to press a button to stop the recording prematurely that also exits the method at the same time. The way I'm currently trying to do it is with an NSTimer that checks to see if the recording is still happening, and if it isn't, it stops playing audio and should also call return to stop the method.
-(void) method
{
self.stopTimer = [NSTimer scheduledTimerWithTimeInterval:.05 target:self selector:#selector(checkRecording) userInfo:nil repeats:YES];
// Stuff happens
}
-(void) checkRecording
{
if (isRecording == NO)
{
if (player.playing == YES)
{
[player stop];
}
return;
}
}
This stops the audio immediately but the method continues to run until it's done. It doesn't call the next method in the sequence, which is a step in the right direction, but I need it to stop immediately. My only theory is that it's because I'm not calling return inside the actual method that I want to stop and instead in a different method, but even if that's the case I'm not really sure how to fix that because as far as I know timers can only point to other methods and I can't just tell it what I want it to do inside of the method that I want to stop. And if that's not the issue then I'm really not sure why this isn't working.
If a timer is valid you can invalidate it (that stops the timer).
I'm not sure if all the checking is really necessary (& the last line) but I do it currently that way:
if ( myTimer != nil && [myTimer isValid] )
{
[myTimer invalidate];
myTimer = nil;
}
EDITED:
if ( [myTimer isValid] )
{
[myTimer invalidate];
myTimer = nil;
}
My only theory is that it's because I'm not calling return inside the actual method that I want to stop and instead in a different method
Your theory is correct. return ends the function or method it is in, and none other. It pops the current function's context off the stack and returns execution to the calling function.
I'm not really sure how to fix that because as far as I know timers can only point to other methods and I can't just tell it what I want it to do inside of the method that I want to stop
We can use objects to store state and use that state to control the flow of our program. That state can be continually updated and checked. With a long-running task that needs to be cancelled in response to changes in that state, the state must be updated in parallel with the task. Since you say the timer works for stopping audio, but that the work done in method doesn't, I'm assuming that method is performing its long-running task asynchronously already.
This need to do an asynchronous long-running task (or series of tasks) in the background, with the possibility of cancellation, is nicely matched to the NSOperation and NSOperationQueue classes.
You can perform your work inside NSOperation objects, either via implementing methods or blocks. Implement your code to check if the operation has been cancelled at all appropriate times, and bail out as soon as that happens.
Below is an example that hopefully matches your use case. It was created in an iOS app 'empty application' template an everything is in the application delegate. Our app delegate keeps track of the state necessary to make the decision of whether to cancel or not, and also schedules a timer to poll for changes in that state. If it does determine that it should cancel, it delegates the actual cancellation of work to the operation queue and its operations.
#import "AppDelegate.h"
#interface AppDelegate ()
#property (nonatomic) BOOL shouldStop; // Analogous to your isRecording variable
#property (nonatomic, strong) NSOperationQueue *operationQueue; // This manages execution of the work we encapsulate into NSOperation objects
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Typical app delegate stuff
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
// Start our long running method - analogous to method in your example
[self method];
return YES;
}
- (void)method
{
// allocate operation queue and set its concurrent operation count to 1. this gives us basic ordering of
// NSOperations. More complex ordering can be done by specifying dependencies on operations.
self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
// We create three NSBlockOperations. They only sleep the thread a little while,
// check if they've been cancelled and should stop, and keep doing that for a few seconds.
// When they are completed (either through finishing normally or through being cancelled, they
// log a message
NSMutableArray *operations = [NSMutableArray array];
for (int i = 0; i < 3; i++) {
// Block operations allow you to specify their work by providing a block.
// You can override NSOperation to provide your own custom implementation
// of main, or start, depending. Read the documentation for more details.
// The principle will be the same - check whether one should cancel at each
// appropriate moment and bail out if so
NSBlockOperation *operation = [[NSBlockOperation alloc] init];
// For the "weak/strong dance" to avoid retain cycles
__weak NSBlockOperation *weakOperation = operation;
[operation addExecutionBlock:^{
// Weak/strong dance
NSBlockOperation *strongOperation = weakOperation;
// Here is where you'd be doing actual work
// Either in a block or in the main / start
// method of your own NSOperation subclass.
// Instead we sleep for some time, check if
// cancelled, bail out if so, and then sleep some more.
for (int i = 0; i < 300; i++) {
if ([strongOperation isCancelled]) {
return;
}
usleep(10000);
}
}];
// The completion block is called whether the operation is cancelled or not.
operation.completionBlock = ^{
// weak/strong dance again
NSBlockOperation *strongOperation = weakOperation;
NSLog(#"Operation completed, %# cancelled.", [strongOperation isCancelled] ? #"WAS" : #"WAS NOT");
};
[operations addObject:operation];
}
// Set up a timer that checks the status of whether we should stop.
// This timer will cancel the operations if it determines it should.
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(checkShouldKeepGoing:) userInfo:nil repeats:YES];
// Use GCD to simulate a stopped recording to observe how the operations react to that.
// Comment out to see the usual case.
double delayInSeconds = 5;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
self.shouldStop = YES;
});
// Add the operations to the operation queue, exeuction will start asynchronously from here.
[self.operationQueue addOperations:operations waitUntilFinished:NO];
}
// If we should stop, cancel the operations in the queue.
- (void)checkShouldKeepGoing:(NSTimer *)timer
{
if (self.shouldStop) {
NSLog(#"SHOULD STOP");
[timer invalidate];
[self.operationQueue cancelAllOperations];
}
}
#end
Related
I have created a UIButton and on click event, I am showing an image in the web view. Also, I am refreshing the image in every 30 sec. But when I click on button multiple times, refresh method get called multiple time as well.
I want it to work like, It saves last click time and refreshes as per that time instead of multiple times.
What can I do for it?
I tried to kill all previous thread instead of the current thread but that's not working.
Please help if anyone already know the answer.
Below is my image refresh code:
- (void)refreshBanner:(id)obj {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
if (![SNRunTimeConfiguration sharedInstance].isInternetConnected) {
[self removeBannerAdWithAdState:kADViewStateNotConnectedToInternet];
return;
}
if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {
self.bannerPaused = YES;
return;
}
self.adView.hidden = YES;
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
topController = [SNADBannerView topViewControllerWithRootViewController:topController];
if ([self checkInViewHierarchy:self parentView:topController.view]) {
// NSLog(#"Visible View Is: %#", self.adId);
SNADMeta *meta = [[SNADDataBaseManager singletonInstance] adToShowWithBanner:YES excludeTyrooAd:YES audio:NO zoneId:self.adSoptZoneId fixedView:NO condition:nil contextualKeyword:nil onlyFromAJ:NO];
SNADAdLocationType type = SNADAdLocationTypeHeader;
if (self.bannerType == SmallViewTypeFooter) {
type = SNADAdLocationTypeFooter;
}
if (self.isFromCustomEvent) {
type = SNADAdLocationTypeAdMobBanner;
}
NSString *message = meta ? nil : kSNADOppMissReason_NoAdToShow;
[SNRunTimeConfiguration fireOpportunityForAdLocation:type zoneId:self.adSoptZoneId reason:message];
NSLog(#"******************* Opportuninty fired for refresh banner ***************************");
if (meta) {
self.meta = meta;
[self updateContentForWebAd:nil];
[self updateStatsForAd];
//fireImpression
[SNADBannerView fireImpression:self.meta];
if ([meta.adSource isEqualToString:kSNADParameter_APC]) {
self.sdkMediation = [[SdkMediation alloc] init];
[self.sdkMediation fireTrackingAdType:self.meta.type isFill:YES];
}
// Ad Height Delegate.
if ([self.meta.displayType isEqualToString:kSNADDisplayType_web]) {
self.adHeightDelegateCalled = YES;
NSInteger height = self.meta.height.integerValue;
self.bannerCH.constant = height;
if ([self.callBackDelegate respondsToSelector:#selector(adWillPresentWithHeight:adId:adType:)]) {
[self.callBackDelegate adWillPresentWithHeight:height adId:self.adId adType:SeventynineAdTypeMainStream];
}
}
} else {
[self removeBannerAdWithAdState:kADViewStateNoAdToShow];
if ([meta.adSource isEqualToString:kSNADParameter_APC]) {
[self.sdkMediation fireTrackingAdType:self.meta.type isFill:NO];
}
return;
}
} else {
// NSLog(#"View Which Is Not Visible Now: %#", self.adId);
}
SNAdConfiguration *configuration = [SNAdConfiguration sharedInstance];
[self.timer invalidate];
self.timer = [NSTimer scheduledTimerWithTimeInterval:configuration.autoRefRate target:self selector:#selector(refreshBanner:) userInfo:nil repeats:NO];
}];
}
Use GCD, and not NSOperationQueue.
Then you step away from your immediate task. You do lots and lots of complicated things inside refreshBanner. And you will do more complicated things to make it work when the user taps multiple times.
Think about what exactly you need. Abstract the "refresh automatically, and when the button is clicked, but not too often" into a class. Then you create a class that takes a dispatch_block_t as an action, where a caller can trigger a refresh anytime they want, and the class takes care of doing it not too often. Then you create an instance of the class, set all the needed refresh actions as its action block, refreshBanner just triggers a refresh, and that class takes care of the details.
You do that once. When you've done it, you actually learned stuff and are a better programmer than before, and you can reuse it everywhere in your application, and in new applications that are coming.
NSOperationQueue have cancelAllOperations method. But for the main queue it's not a good decision to use this method, cause main queue is shared between different application components. You can accidentally cancel some iOS/other library operation together with your own.
So you can create NSOperation instances and store them in an array. Then you can call cancel for all scheduled operations by iterating trough this array, and it will only affect your operations.
Note that block operations doesn't support cancellation. You will need to create your own NSOperation subclass, extract code from your execution block into that subclass main method. Also, you'll need to add [self isCancelled] checks that will abort your logic execution at some points.
I forgot to mention that currently your execution block is fully performed on the main queue. So, you'll need to move any heavy-lifting to background thread if you want to cancel your operation in the middle of processing from main thread.
I need to add that I agree with #gnasher729 - this doesn't look like an optimal solution for the problem.
I have resolved the issue.
Multiple threads created because a new view is created every time I call the API to display image. So now I am removing views if any available before displaying image, then only last object remains and refresh is called as per last called time.
Every View has it's own object that's why multiple threads has created.
By removing views my issue has been resolved.
Thanks everyone for replying.
Here is the code
#interface ViewController ()
#property (nonatomic, strong) NSOperationQueue *queue;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_queue = [[NSOperationQueue alloc] init];
NSBlockOperation *aBlockOperation = [[NSBlockOperation alloc] init];
__weak NSBlockOperation* aWeakBlockOperation = aBlockOperation;
[aBlockOperation addExecutionBlock:^{
NSLog(#"queue should still have the operation. And it does. yay!: %#", [_queue operations]); // This should print correctly. It will show the NSBlock operation correctly residing inside the NSOperationQueue
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(#"Now queue is empty??: %#", [_queue operations]); // This should print as being empty
NSLog(#"And a weak block is nil???: %#", aWeakBlockOperation); // This should print out **nil**
if (![aWeakBlockOperation isCancelled]) {
// Now i have no way to check the operation
}
});
}];
[_queue addOperation:aBlockOperation];
#end
[Edit]
The goal is to have a user interaction like this:
There is a tableView on screen with 5 or more cells. When ever a user click a cell, background process will perform background process that will take a while. The App will, at 3 second intervals, check to see if the user clicked on another cell. If the user clicked on another cell, I should cancel the current operation from queue, and begin processing the new one the user clicked on.
From the code above i have 2 problems i cant solve.
How do i make it so that my weak reference isnt deallocated in the dispatch_after block? The goal of putting it there is to pause the app for exactly 3 seconds. If dispatch_after is incorrect, then what code do i use there to prevent it becoming nil?
Why is it that my NSOperationQueue become empty after I call dispatch_after? Is there a way to make it not become empty?
dispatch_after schedules the block and returns immediately. So, your NSBlockOperation's executionBlock has almost no work to do — it immediately finishes and is removed from the queue. At that time, the operation is released and so the weak reference becomes nil before the dispatch_after block is called later.
If you do the dispatch_after first and schedule the operation from inside that block, it might suit your needs. You could just use sleep, but I wouldn't recommend that since you will be unnecessarily blocking a thread. See this question for more discussion on NSOperation and delays.
You can schedule operation inside the dispatch_after block and declare aBlockOperation as an instance variable/property so aWeakBlockOperation will not became nil.
But you do not need to hassle with the NSBlockOperation to achieve your goal. You can use dispatch_block_t instance variable which you would set to a new value (block with your code you need to be executed after the column is clicked) each time the column is clicked:
#implementation ViewController
{
dispatch_block_t columnBlock;
}
- (void)columnClicked
{
columnBlock = ^{ ... your code ... };
__weak dispatch_block_t weakColumnBlock = columnBlock;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
dispatch_block_t colBlock = weakColumnBlock;
if (colBlock)
colBlock();
});
}
I have a UIViewController that does the following in viewDidLoad
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
items = [[DataFetcher sharedInstance] getItems handler:^(NSArray *currentItems){
if (currentItems.count % 30 == 0) { //don't wait for all the items to be ready show by chunks of 30
items = currentItems;
[tableView reloadData];
}
items = currentItems;
}];//Pretty complex call that takes some time to finish there is WebService calls, data parsing, storing some results ...
dispatch_async(dispatch_get_main_queue(), ^{
[tableView reloadData];
});
});
What I need to do is to stop getItems when I pop this viewController. It's pointless and it takes CPU Time and energy (This call may take up to a minute on some cases).
I am assuming I should be doing this in viewWillDisappear but how exactly?
You can use NSBlockOperation. Periodically check if it's been cancelled, and stop doing work if it has been:
- (void)getItemsWithHandler:(void (^)(NSArray *currentItems))handler {
self.operation = [NSBlockOperation blockOperationWithBlock:^{
if (self.operation.isCancelled) {
return;
}
// Do something expensive
if (self.operation.isCancelled) {
return;
}
// Do something else expensive
for (int i = 0; i < 10000; i++) {
if (self.operation.isCancelled) {
return;
}
// Do expensive things in a loop
}
}];
}
- (void) cancelGetItemsRequest {
[self.operation cancel];
self.operation = nil;
}
Alternatively, you can put a bunch of NSBlockOperations in an NSOperationQueue. You can set dependencies for the work, and cancel the entire queue at once if you want.
Cancelling asynchronous operations is nicely supported in NSOperation and NSOperationQueue, but it's quite a bit more complicated. In any case, your asynchronous code has to check from time to time whether it is cancelled or should still continue.
Best thing is to have a property "cancelled" somewhere, that you set when you don't want any more work to be done, and whenever you try to do more work, you check that property.
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.
I am creating a new Thread which runs one of my method:
Now what i am doing is as follows:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:#selector(myThreadFunc) object:nil];
[thread start];
in myThreadFunc
{
while(isRunning){
[self updateSomething];
[NSThread sleepForTimeInterval:3.0];
}
NSLog(#"out");
}
In another func,i set isRunning = NO and thread = nil or [thread cancel] but myThreadFunc is sleeping so thread cannot exit.
How can i control this case?
Thanks so much.
Don't use a thread. Use a timer. If the something is expensive, dispatch it off to some queue other than the main queue and set some state variable to show it's still running (if the something isn't meant to run concurrently). Then, just cancel your timer. A simple example of the timer call-back function might be:
- (void)doSomething:(NSTimer*)timer
{
// this assumes that this "something" only ever
// runs once at a time no matter what, adjust this
// to an ivar if it's per-class instance or something
static BOOL alreadyDoingSomething = NO;
if( alreadyDoingSomething ) return;
alreadyDoingSomething = YES;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self updateSomething];
alreadyDoingSomething = NO;
});
}
Now, if you simply cancel the timer, this will stop running. When you're ready to start it again, schedule a new timer with this method as the designated selector. To make this behave similar to your example above, you might set the timer interval to three seconds.