Add custom Button with AsyncDisplayKit - ios

I am developing an IOS application. I use Facebook AsyncDisplayKit library. I want to a button in ASNodeCell Bu I got "Variable 'node' is uninitialized when captured by block. How can I add UIButton or UIWebView control in ASNodeCell. Please help me
dispatch_queue_t _backgroundContentFetchingQueue;
_backgroundContentFetchingQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_async(_backgroundContentFetchingQueue, ^{
ASDisplayNode *node = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
[button sizeToFit];
node.frame = button.frame;
return button;
}];
// Use `node` as you normally would...
node.backgroundColor = [UIColor redColor];
[self.view addSubview:node.view];
});

Note that in your case there's no need to use UIButton, you can use ASTextNode as a button since it inherits from ASControlNode (same goes for ASImageNode). This is described at the bottom of the first page of the guide: http://asyncdisplaykit.org/guide/. That will also allow you to do the text sizing on the background thread instead of the main thread (the block you provide in your example is executed on the main queue).
For completeness I'll also comment on the code you provided.
You're trying to set the frame of the node in the block when you're creating it, so you're trying to set the frame on it during its initialization. That causes your problem. I don't think you actually need to set the frame on the node when you're using initWithViewBlock: because internally ASDisplayNode uses the block to directly create its _view property which is added to the view hierarchy in the end.
I also noticed you're calling addSubview: from the background queue, you should always dispatch back to the main queue before you call that method. AsyncDisplayKit also adds addSubNode: to UIView for convenience.
I've changed you're code to reflect the changes though I recommend you use ASTextNode here.
dispatch_queue_t _backgroundContentFetchingQueue;
_backgroundContentFetchingQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_async(_backgroundContentFetchingQueue, ^{
ASDisplayNode *node = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
[button sizeToFit];
//node.frame = button.frame; <-- this caused the problem
return button;
}];
// Use `node` as you normally would...
node.backgroundColor = [UIColor redColor];
// dispatch to main queue to add to view
dispatch_async(dispatch_get_main_queue(),
[self.view addSubview:node.view];
// or use [self.view addSubnode:node];
);
});

Related

On iOS, is it okay to read the UI outside of the main thread?

With iOS, I know that all updates to the UI must be done on the main thread. Are reads from the UI safe to do from outside of the main thread?
I have some computationally expensive logic that needs to use MKMapView's convertCoordinate method, which needs toPointToView: UIView. Is this safe to do this from outside of the main thread?
yes you can do frame calculation or whatever logic in back ground. But UI operations like add subview , animation must be in main thread.
CGPoint annPoint = [self.mapView convertCoordinate:coord toPointToView:self.mapView];
mapPic = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"pic.png"]];
mapPic.frame = CGRectMake(annPoint.x, annPoint.y, 32, 32);
[self.view addSubview:macPic];
it can be written as per your requirement:
- (void)doCalculation
{
//you can use any string instead "com.mycompany.myqueue"
dispatch_queue_t backgroundQueue = dispatch_queue_create("com.mycompany.myqueue", 0);
dispatch_async(backgroundQueue, ^{
CGPoint annPoint = [self.mapView convertCoordinate:coord toPointToView:self.mapView];
mapPic = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"pic.png"]];
mapPic.frame = CGRectMake(annPoint.x, annPoint.y, 32, 32);
dispatch_async(dispatch_get_main_queue(), ^{
[self.view addSubview:macPic];
});
});
}
hope it will help

Adding larg number of Buttons to UIScrollView in background thread - Buttons not visible

the user should be able to select one icon from a large number of different icons. I have created a picker dialog that allows the user to make his selection. The ViewController that is used for this picker only holds one UIScrollView. In viewDidLoad for each icon a button is added to the ScrollView. To select an icon the user just has to click the corresponding button...
This works fine, but the ViewController/picker needs several seconds to be displayed. This is because of the many alloc / add operations within viewDidLoad. Because of this I tried to move these options into a background thread. This workes fine, but the created buttons are not visible any more:
- (void)viewDidLoad {
[super viewDidLoad];
self.iconsScrollView.hidden = true;
[self.activityIndicator startAnimating];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
iconContainer = [[UIView alloc] init];
iconContainer.backgroundColor = [UIColor clearColor];
iconButtons = [[NSMutableDictionary alloc] init];
CGRect buttonRect = CGRectMake(5, 5, 40, 40);
selectedButton = nil;
NSArray *iconInfos = [[StoreController sharedController] allIcons];
for (IconInfo* iconInfo in iconInfos) {
NSString *iconName = iconInfo.name;
UIButton *iconButton = [UIButton buttonWithType:UIButtonTypeCustom];
iconButton.frame = buttonRect;
[iconButton addTarget:self action:#selector(iconSelectionClick:) forControlEvents: UIControlEventTouchUpInside];
[iconButton setImage:[UIImage imageNamed:iconName] forState:UIControlStateNormal];
[iconContainer addSubview:iconButton];
[iconButtons setObject:iconButton forKey:iconInfo.guid];
buttonRect.origin.x += 50;
if (buttonRect.origin.x > 205) {
buttonRect.origin.x = 5;
buttonRect.origin.y += 50;
}
}
iconContainer.frame = CGRectMake(0, 0, self.iconsScrollView.frame.size.width, ceil([iconButtons count] / 5.0) * 50);
dispatch_async(dispatch_get_main_queue(), ^{
[self.iconsScrollView addSubview:iconContainer];
self.iconsScrollView.contentSize = iconContainer.frame.size;
[self.activityIndicator stopAnimating];
self.iconsScrollView.hidden = false;
[self.view setNeedsDisplay];
});
});
}
This works (almost) without any problem:
Picker ViewController is presented
ActivityIndicator is visible while buttons are created
Once all buttons are ready ActivityIndicator stops and the ScrollView becomes visible.
Only Problem: The Buttons are not visible. The ScrollView can be used normally (content size correct) and when I touch inside the ScrollView and hit an invisible button the click selector is called. Thus all buttons are there but not visible. Eventually after 10-15 seconds all Buttons become visible at once.
Using setNeedsDisplay or setNeedsLayout for the View, the ScrollView, or the buttons does not change anything.
Any idea what I can do?
You are adding buttons to a subview while you aren't on the main thread.
Generally, UIKit code should only be run on the main queue.
UIKit can only be updated from the main thread
dispatch_async(dispatch_get_main_queue(), ^(void){
//Run UI Updates
});

Is the creation of an UIKit object in background using GCD a bad practice?

As pointed out by bbum here,
the doc says: "For the most part, UIKit classes should only be used from the main thread of an application This is especially true for derived classes UIResponder or involve the manipulation of user interface of your application in any way. ".
I thought I understood that the methods of drawings could not be called in a background thread, so that the creation could be done in the background, as the drawRect method is only called when the view is added. But maybe i am wrong.
In summary, does that this kind of code is risky?
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
NSString *fileName = [pathToModel stringByAppendingPathComponent:[[compDico valueForKey:#"fileName"] lastPathComponent]];
UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageWithContentsOfFile:
fileName]];
UILabel *label=[[UILabel alloc]initWithFrame:CGRectMake(10, 62, 190, 20)];
[label setText:[[someArray objectAtIndex:i-1] someText]];
[label setNumberOfLines:0];
label.font=[UIFont fontWithName:#"arial" size:10.0f];
[label setBackgroundColor:[UIColor clearColor]];
// Create some other view here
// ...
dispatch_async(dispatch_get_main_queue(), ^{
[self.view addSubview:imageView];
[self.view addSubview:label];
//Add other view here
// ...
});
});
Thanks for in advance your responses!
Yes, this is risky. How risky it is only Apple developers can say.
If the documentation says "don't use it", just don't use it.
Note that many UI objects can (and do) use shared resources. If you use them in a background thread, you'll get a race condition on the shared resource and anything can happen.

Programatically generated UILabel origin point incorrectly to (0.0) set on first load

I am trying to programatically generate two UILabels in my application for each UIImageView on my storyboard. The code runs and works correctly, however, on first load the two UILabels form in the (0.0) coordinate of the main view, as opposed to the UIImageView frame origin.x,origin.y. I can't understand why this is happening.
If I then click on a different tab and return to the page, the labels generate in the correct location.
Why is this? How can I get it to initially generate the labels in the correct location?
-(void) viewWillAppear:(BOOL)animated
{
//removed unneccessary code above...
int i = 0;
for (UIImageView *plantScreen in self.view.subviews)
{
if ([plantScreen isMemberOfClass:[Plant class]])
{
#try
{
//the label which will hold the name
UILabel *plantName = [[UILabel alloc] initWithFrame:CGRectMake((plantScreen.frame.origin.x), (plantScreen.frame.origin.y+ plantScreen.frame.size.height), 160, 30.0)];
plantName.numberOfLines = 1;
plantName.minimumScaleFactor = .5;
plantName.adjustsFontSizeToFitWidth = YES;
[plantName setTextAlignment:NSTextAlignmentCenter];
[plantName setBackgroundColor:[UIColor clearColor]];
[self.view addSubview:plantName];
plantName.hidden = false;
[self.view bringSubviewToFront:plantName];
//create the label which will hold the quantity
UILabel *quantity = [[UILabel alloc] initWithFrame:CGRectMake((plantScreen.frame.origin.x), (plantScreen.frame.origin.y+ plantScreen.frame.size.height + 20), 160, 30.0)];
[quantity setTextAlignment:NSTextAlignmentCenter];
[quantity setBackgroundColor:[UIColor clearColor]];
quantity.text = [NSString stringWithFormat:#"%d",plant.quantity];
[self.view addSubview:quantity];
quantity.hidden = false;
[self.view bringSubviewToFront:quantity];
i++;
}
#catch (NSException *exception)
{
NSLog(#"An exception occured: %#", [exception reason]);
}
#finally
{
}
}
}
}
Frame of the UIImageView depends on the image being drawn and its contentMode property. You can try setting the contentMode to UIViewContentModeScaleAspectFill to see if it forces to keep its assigned frame.
First things first, you're missing a call to [super viewWillAppear:animated]. You need to give the superclass (including the UIViewController base class) a chance to "do its magic".
Never forget about giving the parent class a chance to do its magic, unless you know really really well what you're doing.
Second, UI creation should be done in -loadView, not in viewWillAppear:.
Try these two things first.
Alright. Now I'm curious about how you moved things to -loadView. Did you add [super loadView];?
In fact, now that I think about it, moving to -loadView is wrong in this case; you obviously instantiate some views through a nib. UIViewController's implementation of -loadView typically just loads the nib file. Once that's done, UIViewController's -loadView calls -viewDidLoad.
So when you're not creating all UI programmatically but are instead allowing UIViewController to load it from nib, you actually probably want to move code into -viewDidLoad. (See template generated by Xcode when you tell it to create a new UIViewController subclass.)
Moving on, let's consider what the frame depends on. It depends on some view class you called Plant.
Please don't call it that way; it's confusing. Call it PlantView, so a casual reader of your code is aware of what the class is supposed to do. Similarly, you might want to call the variables plantView instead of plantScreen, and plantNameLabel instead of plantName. plantScreen implies a variable containing UIScreen, and plantName implies an NSString more than it implies a UILabel. Same applies to quantity; call this variable quantityLabel.
Next, let's consider what the variables are depending on -- their origin's x and y do not change based on the counter, variable i. Perhaps you meant to write:
UILabel *plantName = [[UILabel alloc] initWithFrame:CGRectMake((plantScreen.frame.origin.x), (plantScreen.frame.origin.y+ plantScreen.frame.size.height * i), 160, 30.0)];
and later on:
UILabel *quantity = [[UILabel alloc] initWithFrame:CGRectMake((plantScreen.frame.origin.x), (plantScreen.frame.origin.y+ plantScreen.frame.size.height*i + 20), 160, 30.0)];
Next, avoid exceptions and exception handling. Ensure that the exception does not occur via other forms of checking; Apple highly recommends you fix exceptions while writing the application and not handle them when they run:
Important: You should reserve the use of exceptions for programming or
unexpected runtime errors such as out-of-bounds collection access,
attempts to mutate immutable objects, sending an invalid message, and
losing the connection to the window server. You usually take care of
these sorts of errors with exceptions when an application is being
created rather than at runtime.
Next, a small stylistic remark (not very important): you're mixing calling setters via properties and calling setters directly. Nothing wrong (they end up doing exactly the same), but stylistically not very nice.
Next, unless you're using ARC (automatic reference counting), don't forget to release the views once they're added as subviews.
Next, plantScreen (which I named plantView below) can have type set to Plant (which I named PlantView below) when declared inside the loop.
Last but highly important and extremely easy to miss: you call the function isMemberOfClass: instead of isKindOfClass:.
Reworked version of your code (untested):
-(void)viewDidLoad
{
[super viewDidLoad];
int i = 0;
for (PlantView *plantView in self.view.subviews)
{
if ([plantView isKindOfClass:[PlantView class]])
{
//the label which will hold the name
CGRect plantNameLabelFrame = CGRectMake((plantScreenView.frame.origin.x),
(plantScreenView.frame.origin.y + plantScreen.frame.size.height*i),
160,
30.0);
UILabel *plantNameLabel = [[UILabel alloc] initWithFrame: plantNameLabelFrame];
plantNameLabel.numberOfLines = 1;
plantNameLabel.minimumScaleFactor = .5;
plantNameLabel.adjustsFontSizeToFitWidth = YES;
plantNameLabel.textAlignment = NSTextAlignmentCenter;
plantNameLabel.backgroundColor:[UIColor clearColor];
[self.view addSubview:plantNameLabel];
[plantNameLabel release];
//create the label which will hold the quantity
CGRect quantityLabelFrame = CGRectMake((plantScreenView.frame.origin.x),
(plantScreenView.frame.origin.y + plantScreen.frame.size.height*i + 20),
160,
30.0);
UILabel *quantityLabel = [[UILabel alloc] initWithFrame: quantityLabelFrame];
quantityLabel.textAlignment = NSTextAlignmentCenter;
quantityLabel.backgroundColor = [UIColor clearColor];
quantityLabel.text = [NSString stringWithFormat:#"%d", plant.quantity]; // NB: what's "plant"?
[self.view addSubview:quantityLabel];
i++;
}
}
}
I've also removed .hidden = false (which should actually read .hidden = NO; this is Objective-C, and not C++), and bringSubviewToFront: (it's already in front, having just been added by addSubview:).

Update UIview with new objects

I have a method makeButtons (posted here), which is removing all buttons in the screen and adding them again. This works fine in viewDidLoad and viewDidAppear. I am accessing information from a webservice which is telling me I need a new button. When I am calling [self makebuttons] from that method, nothing happends, until I move forth and back with my NavigationController forcing viewDidAppear to do the work again. My question is why? I am doing exactly the same, unless it's not called from viewDidAppear, but from doneGettingInformation.
- (void) viewDidAppear:(bool) animated {
[self makebuttons]; // Works great!
}
- (void) doneGettingInformation : (ASIFormDataRequest *) request {
NSString *response = [request responseString];
[[self.temp.userInfo objectForKey:#"spillider"] addObject:response];
[self makebuttons]; // This gets called, but nothing changes in the view itself.
}
- (void) makeButtons {
NSLog(#"kjort");
int newAntall = [[self.temp.userInfo objectForKey:#"spillider"] count];
for (UIButton * button in gameButtons) {
NSString *tag = [NSString stringWithFormat:#"%i",button.tag];
[button removeFromSuperview];
if ([webviews objectForKey:tag]) {
[[webviews objectForKey:tag] removeFromSuperview];
[webviews removeObjectForKey:tag];
}
}
[gameButtons removeAllObjects];
scroller.contentSize = CGSizeMake(320, 480);
if (newAntall > 3) {
CGSize scrollContent = self.scroller.contentSize;
scrollContent.height = scrollContent.height+((newAntall-3)*BUTTON_HEIGTH);
self.scroller.contentSize = scrollContent;
}
int y = 163;
self.nyttSpillKnapp.frame = CGRectMake(BUTTON_X, y, BUTTON_WIDTH, 65);
for (int i=0; i<newAntall; i++) {
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setBackgroundImage:[UIImage imageNamed:#"knapp_midt"] forState:UIControlStateNormal];
[button setBackgroundImage:[UIImage imageNamed:#"knapp_midt"] forState:UIControlStateHighlighted];
[button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[button.titleLabel setFont:[UIFont systemFontOfSize:15]];
button.frame = CGRectMake(BUTTON_X, y, BUTTON_WIDTH, BUTTON_HEIGTH);
button.enabled = YES;
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self
action:#selector(deleteButton:)];
swipe.direction = UISwipeGestureRecognizerDirectionRight;
[button addGestureRecognizer:swipe];
button.tag = [[[self.temp.userInfo objectForKey:#"spillider"] objectAtIndex:i] intValue];
NSString * tittel = [NSString stringWithFormat:#"spill %#",[[self.temp.userInfo objectForKey:#"spillider"] objectAtIndex:i]];
[button setTitle:tittel forState:UIControlStateNormal];
UIButton *subButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
subButton.transform = CGAffineTransformMakeRotation(M_PI_2);
subButton.tag = i;
CGRect subframe = CGRectMake(230, 5, subButton.frame.size.width, subButton.frame.size.height);
subButton.frame = subframe;
CGRect myframe = self.nyttSpillKnapp.frame;
myframe.origin.y = myframe.origin.y+BUTTON_HEIGTH;
self.nyttSpillKnapp.frame = myframe;
[subButton addTarget:self action:#selector(clickGameButton:) forControlEvents:UIControlEventTouchUpInside];
[button addSubview:subButton];
[gameButtons addObject:button];
[self.scroller addSubview:button];
y += BUTTON_HEIGTH;
}
}
To sum up, it only works if I am changing viewcontrollers back and forth causing viewWillAppear to get called. Why is that?
I am sorry for my messy methods.
Thanks
If you change the contents of the view outside of the initial view appearing process or layout changes, it's your responsibility to call setNeedsDisplay and inform the run loop that it needs to be redrawn.
The system will ask the view to draw it's contents initially or during layout changes which is why it works as part of the process to first show the view. During that initial process, the viewWill/DidAppear delegates will get called.
From the UIView class reference:
The View Drawing Cycle
View drawing occurs on an as-needed basis. When a view is first shown,
or when all or part of it becomes visible due to layout changes, the
system asks the view to draw its contents. For views that contain
custom content using UIKit or Core Graphics, the system calls the
view’s drawRect: method. Your implementation of this method is
responsible for drawing the view’s content into the current graphics
context, which is set up by the system automatically prior to calling
this method. This creates a static visual representation of your
view’s content that can then be displayed on the screen.
When the actual content of your view changes, it is your
responsibility to notify the system that your view needs to be
redrawn. You do this by calling your view’s setNeedsDisplay or
setNeedsDisplayInRect: method of the view. These methods let the
system know that it should update the view during the next drawing
cycle. Because it waits until the next drawing cycle to update the
view, you can call these methods on multiple views to update them at
the same time.
EDIT:
Also, make sure done getting images is not called on a background thread. You can't edit views on a background thread. If it is you can prepare all the data on a bg thread but then call makeButtons on on the main thread (performSelectorOnMainThread or use blocks.
See GCD, Threads, Program Flow and UI Updating

Resources