I am displaying a UIView containing a button giving the user an option to undo something. The view stays visible for a few seconds, then closes. I am creating the view as follows:
[self performSelector:#selector(endUndoOption) withObject:self afterDelay:delay];
Then canceling it if necessary using the following:
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(endUndoOption) object:self];
Is there any way to determine if there is an operation scheduled (in this case, endUndoOption)? Or if the timer has begun? Currently I am doing this with a BOOL flag but was wondering if there is a way to check to see if there has been one queued? THanks!
If you check Cocoa Pods (http://cocoapods.org) BlocksKit pod, http://zwaldowski.github.io/BlocksKit/, there is a special category on NSObject with two very useful methods:
+ (id)bk_performBlock:(void (^)(void))block afterDelay:(NSTimeInterval)delay;
which returns an id which is cancellation handle.
And
+ (void)bk_cancelBlock:(id)handle;
to cancel your scheduled perform.
So, to achieve your target, you can store the cancellation handle in some property, e.g.
self.endUndoCancellationHandle = [[self class] bk_performBlock:^{
[self endUndoOption];
self.endUndoCancellationHandle = nil;
} afterDelay:delay];
then cancellation:
if (self.endUndoCancellationHandle)
{
[[self class] bk_cancelBlock:self.endUndoCancellationHandle];
self.endUndoCancellationHandle = nil;
}
To check if something is scheduled, just check if you currently have the handle:
if (self.endUndoCancellationHandle)
{
...
}
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.
In iOS app widget, I can see on only some devices, doubled data (see figure below). I have tried to identify device, iOS version, but it seems to be "random". Plus, I am unable to debug this by myself, because on every of my devices, all is rendered correctly and doing blind debugging is not working (several updates on AppStore but still with the same error).
In widget, I download (in background thread) new data from web and put them (in dispatch_get_main_queue()) into labels, images etc. All is working OK, but sometimes the old data are not "cleared". In my design file for widget, I have cleared all "default" texts, so this is not this problem.
Doubled icon & texts 4.1°C and 7.9°C are overlapping
Main part of my widget code is (shortened by removing other labels, tables and geolocation):
- (void)viewDidLoad
{
[super viewDidLoad];
if ([self.extensionContext respondsToSelector:#selector(widgetLargestAvailableDisplayMode)])
{
//this is iOS >= 10
self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(FinishDownload:) name:#"FinishDownload" object:nil];
self.preferredContentSize = CGSizeMake(320, 160);
[self updateData];
}
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self updateData];
}
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self updateData];
}
-(void)updateData
{
[[[DataManager SharedManager] settings] Reload];
[[CoreDataManager SharedManager] reset];
if ([[DataManager SharedManager] DownloadDataWithAfterSelector:#"FinishDownload"] == NO)
{
//no need to download update - refill data now
//if downloading - wait for download
[self FillData];
}
}
}
-(void)FinishDownload:(NSNotification *)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
[self FillData];
});
}
-(void)FillData
{
//a lot of code - example of setting temperature
NSString *str = [NSString stringWithFormat:#"%# °C", act.temp_act];
self.lblTemp.text = str;
[self.lblTemp sizeToFit];
if (self.completionHandler != nil)
{
self.completionHandler(NCUpdateResultNewData);
}
}
- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler
{
// Perform any setup necessary in order to update the view.
// If an error is encountered, use NCUpdateResultFailed
// If there's no update required, use NCUpdateResultNoData
// If there's an update, use NCUpdateResultNewData
//completionHandler(NCUpdateResultNewData);
NSLog(#"=== widgetPerformUpdateWithCompletionHandler === ");
self.completionHandler = completionHandler;
[self updateData];
}
- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets
{
return UIEdgeInsetsMake(0, 0, 5, 5);
}
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize
{
if (activeDisplayMode == NCWidgetDisplayModeExpanded)
{
self.preferredContentSize = CGSizeMake(320, 160);
}
else if (activeDisplayMode == NCWidgetDisplayModeCompact)
{
self.preferredContentSize = maxSize;
}
}
View Lifecycle
Do not duplicate the work in viewDidLoad and viewWillAppear/viewDidAppear.
A view that was loaded will hit all three methods. Use viewDidLoad for operations that must be performed exactly once for the life of the UIViewController.
Potential problem:
Triggering 3 invocations, possibly conflicting, to [self updateData] back to back, possibly with competing NCUpdateResult completion handlers3.
Balance Observers
It appears that addObserver is never balanced by a removeObserver. A good location for these registration methods is a set of balanced messages, such as the view___Appear and view___Disappear methods, as outlined in this StackOverflow answer.
Potential problem:
Lasting registration to notifications on objects that may go out of scope.
Do not cache OS handlers
Possible misuse of NCUpdateResultNewData completion handler: the NCUpdateResult is passed to widgetPerformUpdateWithCompletionHandler to be used for that specific invocation, not stored for multiple reuse. It should probably be handed down to updateData as a parameter rather than stored in a global, in turn passed to FillData, and eventually cleared after a one-time use.
if (nil != self.completionHandler) {
self.completionHandler(NCUpdateResultNewData);
self.completionHandler = nil; // One time use
}
Every invocation to widgetPerformUpdateWithCompletionHandler has its own cycle, as outlined in this StackOverflow answer.
Layout & Autolayout
Be aware that the iOS is making a snapshot of your widget ; in Interface Builder, make sure that you use proper layering of views. Pay special attention to transparency and drawing flags. Leverage Autolayout to resize/size/snap objects
Check the UILabel's options in Interface Builder, make sure 'opaque' is unchecked. If the label is set as opaque, it might not be properly clearing the entire view when you change the text. You probably want to check on the 'clears graphics context' property as well, which should be checked.
In the code you add a Notification observer. You do not remove the observer.
I suspect that the notification will be fired multiple times which will result jn a race condition or something.
Solution:
- check hoe often the addObserver is executed. (Including screen changes like back-forward etc)
remove the observer when the notification is caught.
clear / remove the observer when leaving the VC
Besides: check / reduce the action in the ViewWillAppear and ViwDidAppear.
A number of Cocoa Touch classes leverage a design pattern of coalescing events. UIViews, for example, have a method setNeedsLayout which causes layoutSubviews to be called in the very near future. This is especially useful in situations where a number of properties influence the layout. In the setter for each property you can call [self setNeedsLayout] which will ensure the layout will be updated, but will prevent many (potentially expensive) updates to the layout if multiple properties are changed at once or even if a single property were modified multiple times within one iteration of the run loop. Other expensive operations like the setNeedsDisplay and drawRect: pair of methods follow the same pattern.
What's the best way to implement pattern like this? Specifically I'd like to tie a number of dependent properties to an expensive method that needs to be called once per iteration of the run loop if a property has changed.
Possible Solutions:
Using a CADisplayLink or NSTimer you could get something working like this, but both seem more involved than necessary and I'm not sure what the performance implications of adding this to lots of objects (especially timers) would be. After all, performance is the only reason to do something like this.
I've used something like this in some cases:
- (void)debounceSelector:(SEL)sel withDelay:(CGFloat)delay {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:sel object:nil];
[self performSelector:sel withObject:nil afterDelay:delay];
}
This works great in situations where a user input should only trigger some event when a continuous action, or things like that. It seems clunky when we want to ensure there is no delay in triggering the event, instead we just want to coalesce calls within the same run loop.
NSNotificationQueue has just the thing you're looking for. See the documentation on Coalescing Notifications
Here a simple example in a UIViewController:
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(configureView:)
name:#"CoalescingNotificationName"
object:self];
[self setNeedsReload:#"viewDidLoad1"];
[self setNeedsReload:#"viewDidLoad2"];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self setNeedsReload:#"viewWillAppear1"];
[self setNeedsReload:#"viewWillAppear2"];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self setNeedsReload:#"viewDidAppear1"];
[self setNeedsReload:#"viewDidAppear2"];
}
- (void)setNeedsReload:(NSString *)context
{
NSNotification *notification = [NSNotification notificationWithName:#"CoalescingNotificationName"
object:self
userInfo:#{#"context":context}];
[[NSNotificationQueue defaultQueue] enqueueNotification:notification
postingStyle:NSPostASAP
coalesceMask:NSNotificationCoalescingOnName|NSNotificationCoalescingOnSender
forModes:nil];
}
- (void)configureView:(NSNotification *)notification
{
NSString *text = [NSString stringWithFormat:#"configureView called: %#", notification.userInfo];
NSLog(#"%#", text);
self.detailDescriptionLabel.text = text;
}
You can checkout the docs and play with the postingStyle to get the behavior you desired. Using NSPostASAP, in this example, will give us output:
configureView called: {
context = viewDidLoad1;
}
configureView called: {
context = viewDidAppear1;
}
meaning that back-to-back calls to setNeedsReload have been coalesced.
I've implemented something like this using custom dispatch sources. Basically, you setup a dispatch source using DISPATCH_SOURCE_TYPE_DATA_OR as such:
dispatch_source_t source = dispatch_source_create( DISPATCH_SOURCE_TYPE_DATA_OR, 0, 0, dispatch_get_main_queue() );
dispatch_source_set_event_handler( source, ^{
// UI update logic goes here
});
dispatch_resume( source );
After that, every time you want to notify that it's time to update, you call:
dispatch_source_merge_data( __source, 1 );
The event handler block is non-reentrant, so updates that occur while the event handler is running will coalesce.
This is a pattern I use a fair bit in my framework, Conche (https://github.com/djs-code/Conche). If you're looking for other examples, poke around CNCHStateMachine.m and CNCHObjectFeed.m.
This borders on "primarily opinion based", but I'll throw out my usual method of handling this:
Set a flag and then queue processing with performSelector.
In your #interface put:
#property (nonatomic, readonly) BOOL needsUpdate;
And then in your #implementation put:
-(void)setNeedsUpdate {
if(!_needsUpdate) {
_needsUpdate = true;
[self performSelector:#selector(_performUpdate) withObject:nil afterDelay:0.0];
}
}
-(void)_performUpdate {
if(_needsUpdate) {
_needsUpdate = false;
[self performUpdate];
}
}
-(void)performUpdate {
}
The double check of _needsUpdate is a little redundant, but cheap. The truly paranoid would wrap all the relevant pieces in #synchronized, but that's really only necessary if setNeedsUpdate can be invoked from threads other than the main thread. If you're going to do that you also need to make changes to setNeedsUpdate to get to the main thread before calling performSelector.
It's my understanding that calling performSelector:withObject:afterDelay: using a delay value of 0 causes the method to be called on the next pass through the event loop.
If you want your actions to be queued up until the next pass through the event loop, that should work fine.
If you want to coalesce multiple different actions and only want one "do everything that accumulated since the last pass through the event loop" call, you could add single call to performSelector:withObject:afterDelay: in your app delegate (or some other single instance object) at launch, and invoke your method again at the end of each call. You could then add an NSMutableSet of things to do, and add an entry to the set each time you trigger an action that you want to coalesce. If you created a custom action object and overrode the isEqual (and hash) methods on your action object, you could set it up so there would only ever be a single action object of each type in your set of actions. Adding the same action type multiple times in a pass through the event loop would add one and only one action of that type).
Your method might look something like this:
- (void) doCoalescedActions;
{
for (CustomActionObject *aCustomAction in setOfActions)
{
//Do whatever it takes to handle coalesced actions
}
[setOfActions removeAllObjects];
[self performSelector: #selector(doCoalescedActions)
withObject: nil
afterDelay: 0];
}
It's hard to get into details on how to do this without specific details of what you want to do.
I am building a simple messaging app using Parse's framework. I have a method called displayMessages. This is called each time the phone receives a push.
However, as this message is doing work in the Parse database I don't want to call it again if it's already running. I want to wait until it is finished and then call it.
I am using the following code:
-(void)receivedPush
{
[self displayMessages];
}
and:
-(void)displayMessages
{
//code here
}
If received push is called I want it to wait until displayMessages is finished before calling it. Could someone please point me in the right direction with this?
UPDATE
I tried using the NSOperationQueue method and realised that although this does work for waiting for displayMessages it doesn't result in the required behavior.
In displayMessages I have: [PFObject deleteAllInBackground:toDelete]; it's actually this I need to wait for completion before calling displayMessages again.
Create a NSOperationQueue and set the maxConcurrentOperationCount to 1. Implement your data access method as an operation (possibly block-type operation) and submit it to the queue. (I like this better than gcd since you can do cancellation or test the number of items already in the queue.)
Note that if the method actually displays things, you'll need to dispatch back to the main queue for UI work.
You could use a NSOperationQueue with maxConcurrentOperationCount set to 1.
Declare the NSOperationQueue as an iVar of your class, initialize it in the init method and set
[_opQueue setMaxConcurrentOperationCount:1];
and then when you receive the push:
- (void)receivedPush {
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(displayMessages) object:nil];
[_opQueue addOperation:op];
}
Shortest and simples would be creating BOOL isExecuting and checking if you can call method based on that (changing values before execution but after check and after execution)
How about this for a fairly lightweight solution:
#property (nonatomic, assign) BOOL needsToDisplayMessages;
#property (nonatomic, assign) BOOL displayingMessages;
Then
-(void)receivedPush
{
if (!self.displayingMessages) {
[self displayMessages];
} else {
self.needsToDisplayMessages = YES;
}
}
-(void)displayMessages
{
self.needsToDisplayMessages = NO;
self.displayingMessages = YES;
//long-running code here
self.displayingMessages = NO;
if (self.needsToDisplayMessages) {
[self displayMessages]
}
(ignoring concurrency issues ... for which you could use GCD in displayMessages or NSOperationQueue as per a couple of the other answers)
With your new updated requirement, you can use deleteAllInBackground:block:. According to document:
"Deletes a collection of objects all at once asynchronously and executes the block when done."
Why not schedule each message handling using:
-(void)receivedPush
{
dispatch_async(dispatch_get_main_queue(), ^{
/* Show the update on the display */
NSLog(#"Handling new messages");
NSArray *newMessages=<populate with new messages>;
[handler displayMessages:newMessages];
});
}
This will queue up your handling of each set as they come in. Only one displayMessages will run at a time.
I'm currently using
[NSObject cancelPreviousPerformRequestsWithTarget:self];
to cancel my performSelector for doneMoving:. This causes an issue because I have other performSelectors running that I do not want to cancel. The solution would be to use
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(doneMoving:) object:objectIDontKnow];
but I don't know the object that was passed with the original performSelector. I want to be able to cancel all doneMoving: requests, no matter what object was passed, while still maintaining my other scheduled methods. Thanks!
try to rewrite your code in a different way, so you don't pass the object
- (void)moveObject {
[self.objectToMove doSmth];
}
- (void)performMoveObjectInFuture:(id)moveObject {
self.objectToMove = moveObject;
[self performSelector:#selector(moveObject) withObject:nil afterDelay:2];
}