SetNeedsDisplay not working - ios

I saw many threads related to this issue, but none addresses my case (I think).
My case should be simple, I have a Custom UIView in my controller, from my controller, I use the [self.myView setNeedsDisplay] and it works perfectly.
I'm having problems when Im trying to call this from inside of the UIView itself...I have a notification being sent from another class, and it is being received by my View (this works) with the information it passes, I update internal properties of this view and than I'm calling the [self setNeedsDisplay] wishing to have my screen updated with the new states, but nothing is happening, I used a NSLOG inside my drawRec method, and it is not being called at this time, it is only called when my controller class call the setNeedsDisplay, and when that happens, the update that should have happened before is showed on screen... I don't know why its not updating before...
Here are some code:
My controller asking for update: (works OK!)
- (void)addNodeToNetwork:(DTINode *)node
{
[self.myNetwork addNodeInTheNetwork:node];
self.gridView.nodesToDraw = [self.myNetwork.nodesInNetwork copy];
CGRect tempRec = CGRectMake(node.nodePosition.x, node.nodePosition.y node.nodePosition.x, node.nodePosition.y);
NSValue *rectObj = [NSValue valueWithCGRect:tempRec]; //transforma o cgrect num objeto
[self.gridView.fatherNodes setValue:rectObj forKey:node.nodeName];
[self.gridView setNeedsDisplay];
}
My notification method trying to update my drawing: (Not Working ! )
- (void) receiveTestNotification:(NSNotification *) notification
{
NSDictionary *userInfo = notification.userInfo;
DTINode *notificationNode = [userInfo objectForKey:#"myNode"];
NSLog(#"Im Here!");
for (DTINode *node in self.nodesToDraw)
{
NSLog(#"Here too");
if(node.nodeName == notificationNode.fatherNode)
{
CGRect temp = CGRectMake(notificationNode.nodePosition.x, notificationNode.nodePosition.y, node.nodePosition.x, node.nodePosition.y);
NSValue *tempObj = [NSValue valueWithCGRect:temp];
[self.fatherNodes setObject:tempObj forKey:notificationNode.nodeName];
[self setNeedsDisplay];
NSLog(#"Should REDRAW NOW!"); // It print this but no drawing is made!
}
}
}
I'm not pasting my drawRect here because it works, the problem is that is is not being called from inside my UIView setNeedsDisplay!
Anyone have any idea why this is not working????

After alot of testing, I saw something related to threads and the fact setNeedsDisplay should only be called in the mainThread...besides I never started a separeted thread in this classes, the class that raised the Notification was in a secondary thread...and aparently this was causing the issue...
to Solve it I just forced setNeedsDisplay to be called in the main thread..
dispatch_async(dispatch_get_main_queue(), ^{
[self setNeedsDisplay];
});
Swift 3:
DispatchQueue.main.async { [weak self] in
self?.setNeedsDisplay()
}

I think the correct way to use it is:
[self setNeedsDisplay:YES];
Although I always have problems to get that working :(

Related

How to kill multiple threads in objective-c

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.

iOS - Objective-C - widget texts and images are doubled - old data are not cleared

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.

Objective -c, removeFromSuperview not works every time

I am trying to remove a view, sometimes it works fine , and sometimes not. I am beginner. I don't know what the problem. I am frustrated. Please let me know what the problem.my code :
-(void)hideNotification
{
btnNotification.selected=NO;
btnHome.selected=YES;
[notificationScreen.view removeFromSuperview];
notificationScreen=nil;
isNotificationScreen=NO;
}
I have also tried : dispatch_async(dispatch_get_main_queue(), ^{
[notificationScreen.view removeFromSuperview];
}); and performSelectorOnMainThread:#selector(removeFromSuperview) withObject:nil waitUntilDone:NO]; but not succeeded.
First and foremost thing I would check in this situation is to ensure updating my UI only on main thread. Trusting, you have already tried that out.
Next, per Apple Documentation, I would ensure following things:
If the view’s superview is not nil, the superview releases the view.
Never call this method from inside your view’s drawRect: method.
Finally, unsure how your notificationScreen object looks like, I would try to set a tag on the view to be removed and remove it based on the tag value. Not sure if notificationScreen refers to your current view controller in which your above code will now work. Try this out:
Set the tag of the view that you want to be removed
(myNotificationView.tag = 1) when initializing and adding to its parent view.
When you are ready to remove the notification view, do it like this
for (UIView *view in [self.view subviews] ) {
if (view.tag == 1 ) {
[view removeFromSuperview];
}
}
For step 2, you could have a strong reference to your notification view and call removeFromSuperview on that object.
First you need to check your view is subview or not. Just change code like below:
-(void)hideNotification
{
btnNotification.selected=NO;
btnHome.selected=YES;
if([notificationScreen.view isDescendantOfView:self.view]){
[notificationScreen.view removeFromSuperview];
}
notificationScreen=nil;
isNotificationScreen=NO;
}

Label taking a really long time to change

I'm learning to develop an IOS app. I'm having the following problem. I want to use a label to display a string. It takes a really long time for this string to be displayed (10-15 sec). Is this normal? The following code is inside the viewDidLoad function
NSLog(self.example); //displays almost immediately
_labelOutput.text= [NSString stringWithFormat:#"%#", self.example;//takes 15 seconds
The entire viewDidLoad function:
- (void)viewDidLoad
{
[super viewDidLoad];
double lat = 43.7000;
double lon = -79.4000;
NSArray *users = [[NSArray alloc] initWithObjects:#"user_1",#"user_2",#"user_3", nil];
id prediction = [[Prediction alloc] initWithUsers:users Lat:lat Lon:lon];
[prediction populate:^{
self.resName= [prediction generateRandom][#"id"];
NSLog([NSString stringWithFormat:#"%#", self.resName]);
_labelOutput.text= [NSString stringWithFormat:#"%#", self.resName];
}];
}
What does -[Prediction populate:] do with the block? My guess is it runs the block on a background thread or queue. You aren't allowed to modify the UI from a background thread or queue. Your app might crash or just act unpredictably. Your mysterious delay in updating the screen is a common symptom of this mistake.
You must only modify the UI from the main thread or queue. Try this:
[prediction populate:^{
self.resName= [prediction generateRandom][#"id"];
NSLog([NSString stringWithFormat:#"%#", self.resName]);
dispatch_async(dispatch_get_main_queue(), ^{
_labelOutput.text= [NSString stringWithFormat:#"%#", self.resName];
});
}];
ETA: Rob's answer is probably spot-on...if you'd posted that code initially I would have caught it as well.
Try setting the label's text in viewWillAppear or perhaps viewDidAppear instead.
Setting the "text" property of a label will normally trigger a [setNeedsDisplay] call automatically via key-value observing, and this notifies the system that the label's view needs to be redrawn on the next run loop. However, viewDidLoad is called before your view is actually visible. It's likely that because of this, either [setNeedsDisplay] is not being called, or is being ignored because the label is not yet visible...and thus, you have to wait for some other event to trigger re-drawing of subviews.
You could test this theory by adding a [self.labelOutput setNeedsDisplay] call yourself in viewDidAppear.
For swift 3 you'll want to use
DispatchQueue.main.async(execute: {
_labelOutput.LabelName.text = "Something"
})

Animating CALayers in background with setNeedsDisplay

I am porting my library from Andriod to IOS and I have made a great deal of progress and learned a little about Objective C in the process. I say a little, because there is a lot to learn. The Android library creates a surfaceView and does animation by drawing the images directly to the surfaceView; in Objective C, I am using a UIView with CALayers.
In the UIView I am dynamically adding CALayers loaded with Sprite Sheet images and then moving the CALayers around in the UIView while roating through the Sprite image in the CALayer by moving the contentsRect of the CALayer.
This all works perfectly in a background thread in Android and does not interfere with the host application. In Objective C, I am trying to run it in the background as well using either performSelectorInBackground or the new Grand Central Dispatch. Both seem to work in the background fine, loading images into the CALayers and then I hit the snag: the CALayers are not displaying.
I know the CALayers are there, because when I pause the app in the simulator by clicking on the middle hardware button and then click again to display it the CALayers are there looking great – but static.
Having read posts for days and trying various ways of setting NeedsDisplay and NeedsLayout on the UIView and the individual CALayers, I have given up and I am asking for your help.
So, please take a look at the code below and let me know what I am doing wrong. I am also having problems setting the frame of my CALayers. When I try to set them with [self setFrame: CGRectMake(0,0,spWidth, spHeight)] – both NSIntegers; I get an error saying Sending CGRect to parameter of incompatible type NSInteger. I know this is probably a rookie mistake.
Here are the relevant sections of my code. I will post more, but I am hoping that my error is in the run method.
In my custom UIView
if ((self = [super initWithFrame:CGRectMake(0,0,dw,dh)])){
(initialization and defining variables)
self.bounds = CGRectMake(0, 0, dw, dh);
self.frame = CGRectMake(0,0,dw,dh);
self.backgroundColor = [UIColor whiteColor];
MainDrawLayer = [[CALayer layer]retain];
MainDrawLayer.bounds = CGRectMake(0, 0, dw, dh); //dw=320 dh=50.
MainDrawLayer.frame = CGRectMake(0,0,dw,dh);
[self.layer addSublayer:MainDrawLayer] ;
}
return self;
All of the action is triggered in the run method below which I call from the ViewController.
- (void) run {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
PGM = [[[GadsMonitor alloc] init: #"http://www.javelin21.com/servlets/theloader" :_displaySizeW :_displaySizeH]autorelease];
while(!PGM.adsdone){
[NSThread sleepForTimeInterval:2.0];
NSLog(#"The value of adsdone is %#\n", (PGM.adsdone ? #"YES" : #"NO"));
}
NSLog(#"Into processComponent Number in PGM.PGVarray %d ", PGM.PGVarray.count);
GVarray = [PGM.PGVarray mutableCopy];
NSLog(#"Into RUN after copy GVArray %d ", GVarray.count);
Gad * readyGad = (Gad *) [GVarray objectAtIndex:0];
if (readyGad != nil)
[readyGad getImages];
NSLog(#"The value of the bool is %#\n", (readyGad.gdone ? #"YES" : #"NO"));
while (!readyGad.gdone) {
[NSThread sleepForTimeInterval:2.0];
[readyGad checkImages];
NSLog(#"The value of the readyGad.gdone is %#\n", (readyGad.gdone ? #"YES" : #"NO"));
}
rstop = NO;
while (!rstop) {
NSLog(#"The value of rstop in while is %#\n", (rstop ? #"YES" : #"NO"));
[self rotateGames];
stop = NO;
while (!stop) {
[self gameAction];
[self paint];
dispatch_async(dispatch_get_main_queue(), ^{
[self setNeedsLayout]; //I have also tried self.layer
[self setNeedsDisplay]; //I have also tried self.layer
});
//These are various timers for CALayer action
[NSThread sleepForTimeInterval:stime];
rtime = (rtime +runGad.gslp);
ltime += runGad.gslp;
jtime += runGad.gslp;
vtime += runGad.gslp;
ftime += runGad.gslp;
ttime += runGad.gslp;
Stime += runGad.gslp;
NSLog(#"After Thread Sleep rtime + gslp %f ", rtime);
if ((runTime += runGad.gslp) >= runGad.grun) {
stop = YES;
}
}
stop = NO;
}
});
}
I thank you for your time.
In iOS any UI updating code must be performed in the main thread. You cannot use GCD (unless its running on the mainQueue) or performSelectorInBackground to update the UI.
You can update your data and do IO on background threads, but when you have the data loaded and you want to display it you must switch back to the main thread.
Trying to do otherwise will sometimes work, sometimes not, or crash your app.
The main thread owns UI updates in iOS.
Also when it comes to doing the work on the main thread you have a few options, you can queue a block up with performSelectorOnMainThread
or you can send a block to the main thread GCD queue, sometimes you may need to play with dispatch_async vs dispatch_sync on the main thread if you use GCD they dont both work in every situation. performSelectorInMainthread seems to be more reliable than GCDs main thread queue.
ymmv

Resources