Thread issue where updating a label make a UIImageview invisible - ios

I have a situation that is confusing me lots. I have a class that has 2 completely separate things: An animated UIImage and a UILabel. They have different outlets, are not connected.
When the app runs, it does this:
dispatch_async(dispatch_get_main_queue(), ^{
[self.monsterMachine setHidden:NO]; //monsterMachine is UIImageView
[self.monsterMachine startAnimating];
});
but then when I do this:
[self.futText setText:#"blah"]; // is UILabel
it causes the monsterMachine UIImageView to not animate anymore. Things I have found:
Right after I setText:#blah" I can use NSLog to watch and see that self.monsterMachine.isAnimated suddenly goes from 1 to 0, ie from YES to NO.
If self.futText is ALREADY saying "blah", then I can run setText:#"blah" on it as many times as I want and nothing happens, it is ONLY when I change it to some value other than blah that UIImageView stops animating and disappears
If I don't use main_queue to show and animate monsterMachine, it won't display at ALL, so how do I diagnose or fix this?

That's weird it works for me perfect. Did you call [self.futText setText:#"blah"]; in main queue also?
Here is a small answer to why setting text #"blah" won't stops the animation:
Strings like #"blah" are stored in stacks which have the same reference as long as they have the same context. In this case [self.futText setText:#"blah"] will do nothing because the internal implementions are like:
void setText:(NSString*) text {
if ( _text == text ) then return;//if reference is the same then do nothing
_text = text;
some rendering..
}

Related

How to tell if a button is pressed at a certain time

I have a button with an image, and after a little while the button image will change, then change back after a few seconds. I want to be able to tell if the button is clicked while the image is different. thanks!
You have several options, but I'll detail two of them. The first is more self-contained and foolproof, but the second is arguably easier to read and understand.
Check the background image
The easiest way to do this would probably be to just test against the image itself. The image changes from time to time, but you don't really care when the button is pressed, you just care which background is visible when it is.
In other words, all you really need to know is whether the button's background is the MainBackground or the AlternateBackground, so when the button is pressed, you can simply check which one it is.
Try something like this, for when the button is pressed:
-(void)buttonPressed:(UIButton*)sender {
UIImage *mainBackground = [UIImage imageNamed:#"YOUR_IMAGE_NAME"];
NSData *imgdata1 = UIImagePNGRepresentation(sender.image);
NSData *imgdata2 = UIImagePNGRepresentation(mainBackground);
if ([imgdata1 isEqualToData:imgdata2]) {
// The button's background is the MainBackground, do one thing
} else {
// The button's background is the AlternateBackground, do another thing
}
}
Keep track of which image is currently displayed
Alternatively, you could have a BOOL value flip whenever the image's background is changed. Something like...
#property BOOL isMainBackground;
...in your H file, and then whenever you set the button's background image, you also set self.isMainBackground = YES; or self.isMainBackground = NO;
Then, your method for the button press would look something like this:
-(void)buttonPressed:(UIButton*)sender {
if (self.isMainBackground) {
// The button's background is the MainBackground, do one thing
} else {
// The button's background is the AlternateBackground, do another thing
}
}

RAC and cell reuse: putting deliverOn: in the right place?

I was playing with RAC and, in particular, Colin Eberhardt's Twitter search example, and came across a crash that I could not explain to myself.
Here is a sample project I have created to illustrate the issue and base the question on.
The app uses a UITableView with reusable cells; each cell has a UIImageView on it whose image is downloaded by some URL.
There is also defined a signal for downloading an image on a background queue:
- (RACSignal *)signalForLoadingImage:(NSString *)imageURLString
{
RACScheduler *scheduler = [RACScheduler
schedulerWithPriority:RACSchedulerPriorityBackground];
return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageURLString]];
UIImage *image = [UIImage imageWithData:data];
[subscriber sendNext:image];
[subscriber sendCompleted];
return nil;
}] subscribeOn:scheduler];
}
In cellForRowAtIndexPath:, I bind the loading signal to the image view's image property with RAC macro:
RAC(cell.kittenImageView, image) =
[[[self signalForLoadingImage:self.imageURLs[indexPath.row]]
takeUntil:cell.rac_prepareForReuseSignal] // Crashes on multiple binding assertion!
deliverOn:[RACScheduler mainThreadScheduler]]; // Swap these two lines to 'fix'
Now, when I run the app and start scrolling the table view up and down, the app crashes with the assertion message:
Signal <RACDynamicSignal: 0x7f9110485470> name: is already bound to key path "image" on object <UIImageView: <...>>, adding signal <RACDynamicSignal: 0x7f9110454510> name: is undefined behavior
However, if I wrap the image loading signal into deliverOn: first, and then into takeUntil:, the cell reuse will work just fine:
RAC(cell.kittenImageView, image) =
[[[self signalForLoadingImage:self.imageURLs[indexPath.row]]
deliverOn:[RACScheduler mainThreadScheduler]]
takeUntil:cell.rac_prepareForReuseSignal]; // No issue
So my questions are:
How to explain why the latter works and the former doesn't? There's obviously some race condition causing a new signal bind to the image property before the existing one completes, but I'm totally not sure how exactly it occurs.
What should I remember about to avoid such kind of subtleties in my RAC-powered code? Am I missing some basic principle in the code above, or is there any rule of thumb to apply (assuming there's no bug in RAC itself, of course)?
Thanks for reading up to here :-)
I haven't confirmed this, but here's a possible explanation:
Cell X gets called into use, starts downloading an image.
Cell X scrolls offscreen before the image download has completed.
Cell X gets reused, prepareForReuse is called.
Cell X's rac_prepareForReuseSignal sends a value.
Because of deliverTo:, the value is dispatched to the main queue, introducing a runloop delay. Of note, this prevents synchronous/immediate unbinding of the image property.
Cell X is back being used cellForRowAtIndexPath:
New image binding is invoked and causes warning
… next runloop …
Original binding is finally now destructed, but it's a bit too late.
So basically the signal should be unbound between 4 and 6, but the -deliverTo: reorders the unbinding to come later.

Calling a few [UIVIew setFrame:] in a row doesn't execute all of them

I've a strange problem with sliding in a UIView into another one while sliding out the other content of the view.
This is the code:
- (void)moveAddStudentViewIntoSuperView
{
CGRect viewFrame1 = self.viewThatShoudlDisappear1.frame;
CGRect viewFrame2 = self.viewThatShoudlDisappear2.frame;
CGRect newFrame = self.viewControllerThatshouldAppear.view.frame;
viewFrame1.origin.x = -300;
viewFrame2.origin.x = -300;
newStudentFrame.origin.x = 0;
NSLog(#"\n 1: %# \n 2: %# \n 3: %#", self.viewThatShoudlDisappear1, self.viewThatShoudlDisappear2, self.viewControllerThatshouldAppear.view);
[self.viewThatShoudlDisappear1 setFrame:viewFrame1];
[self.viewThatShoudlDisappear2 setFrame:viewFrame2];
[self.viewControllerThatshouldAppear.view setFrame:newFrame];
NSLog(#"\n 1: %# \n 2: %# \n 3: %#", self.viewThatShoudlDisappear1, self.viewThatShoudlDisappear2, self.viewControllerThatshouldAppear.view);
}
(Usually I'd do the setFrames in a UIView animation but it doesn't matter for this example as it doesn't work anyway)
What happens is that the view that should slide in or rather is now located at (0,0) appears but the other views dont move away. And are still displayed under the new view.
What is strange, is that the log outputs that all views are in the correct position. [self.view setNeedsLayout] between the setFrame calls doesn't change anything either and even more interesting is that if I remove the setFrame from the new view the views that should disappear do actually disappear correctly.
There is a typo on this line:
CGRect viewFrame2 = self.viewThatShoudlDisappear1.frame;
Presumably, this should be:
CGRect viewFrame2 = self.viewThatShoudlDisappear2.frame;
I've seen odd things like this when I've accidentally used UIKit from a background thread. UIKit should only be used from the main thread.
Ok so I think I don't fully understand AutoLayout. Due to a tip from Toto, I tried to test it under iOS 5 and had to deactivate AutoLayout everywhere to run it.
It instantly worked with iOS 5. Afterwards I switched back to iOS 6 without activating AutoLayout again and it worked there perfectly too.
My solution is: Deactivating AutoLayout in the XIB file(s).
But I can't think of a way where this should be a default behavior. Maybe I miss a point of AutoLayout or this is a weird bug. As soon as I've talked to devs who are more into it, I'll probably submit a bugreport if necessary.

iOS UIScrollView performance

I'm trying to increase the scrolling performance of my UIScrollView. I have a lot of UIButtons on it (they could be hundreds): every button has a png image set as background.
If I try to load the entire scroll when it appears, it takes too much time. Searching on the web, I've found a way to optimize it (loading and unloading pages while scrolling), but there's a little pause in scrolling everytime I have to load a new page.
Do you have any advice to make it scroll smoothly?
Below you can find my code.
- (void)scrollViewDidScroll:(UIScrollView *)tmpScrollView {
CGPoint offset = tmpScrollView.contentOffset;
//322 is the height of 2*2 buttons (a page for me)
int currentPage=(int)(offset.y / 322.0f);
if(lastContentOffset>offset.y){
pageToRemove = currentPage+3;
pageToAdd = currentPage-3;
}
else{
pageToRemove = currentPage-3;
pageToAdd = currentPage+3;
}
//remove the buttons outside the range of the visible pages
if(pageToRemove>=0 && pageToRemove<=numberOfPages && currentPage<=numberOfPages){
for (UIView *view in scrollView.subviews)
{
if ([view isKindOfClass:[UIButton class]]){
if(lastContentOffset<offset.y && view.frame.origin.y<pageToRemove*322){
[view removeFromSuperview];
}
else if(lastContentOffset>offset.y && view.frame.origin.y>pageToRemove*322){
[view removeFromSuperview];
}
}
}
}
if(((lastContentOffset<offset.y && lastPageToAdd+1==pageToAdd) || (lastContentOffset>offset.y && lastPageToAdd-1==pageToAdd)) && pageToAdd>=0 && pageToAdd<=numberOfPages){
int tmpPage=0;
if((lastContentOffset<offset.y && lastPageToAdd+1==pageToAdd)){
tmpPage=pageToAdd-1;
}
else{
tmpPage=pageToAdd;
}
//the images are inside the application folder
NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
for(int i=0;i<4;i++){
UIButton* addButton=[[UIButton alloc] init];
addButton.layer.cornerRadius=10.0;
if(i + (tmpPage*4)<[imagesCatalogList count]){
UIImage* image=[UIImage imageWithContentsOfFile:[NSString stringWithFormat: #"%#/%#",docDir,[imagesCatalogList objectAtIndex:i + (tmpPage*4)]]];
if(image.size.width>image.size.height){
image=[image scaleToSize:CGSizeMake(image.size.width/(image.size.height/200), 200.0)];
CGImageRef ref = CGImageCreateWithImageInRect(image.CGImage, CGRectMake((image.size.width-159.5)/2,(image.size.height-159.5)/2, 159.5, 159.5));
image = [UIImage imageWithCGImage:ref];
}
else if(image.size.width<image.size.height){
image=[image scaleToSize:CGSizeMake(200.0, image.size.height/(image.size.width/200))];
CGImageRef ref = CGImageCreateWithImageInRect(image.CGImage, CGRectMake((image.size.width-159.5)/2, (image.size.height-159.5)/2, 159.5, 159.5));
image = [UIImage imageWithCGImage:ref];
}
else{
image=[image scaleToSize:CGSizeMake(159.5, 159.5)];
}
[addButton setBackgroundImage:image forState:UIControlStateNormal];
image=nil;
addButton.frame=CGRectMake(width, height, 159.5, 159.5);
NSLog(#"width %i height %i", width, height);
addButton.tag=i + (tmpPage*4);
[addButton addTarget:self action:#selector(modifyImage:) forControlEvents:UIControlEventTouchUpInside];
[tmpScrollView addSubview:addButton];
addButton=nil;
photos++;
}
}
}
lastPageToAdd=pageToAdd;
lastContentOffset=offset.y;
}
Here's a few recommendations:
1) First, understand that scrollViewDidScroll: will get called continuously, as the user scrolls. Not just once per page. So, I would make sure that you have logic that ensures that the real work involved in your loading is only triggered once per page.
Typically, I will keep a class ivar like int lastPage. Then, as scrollViewDidScroll: is called, I calculate the new current page. Only if it differs from the ivar do I trigger loading. Of course, then you need to save the dynamically calculated index (currentPage in your code) in your ivar.
2) The other thing is that I try not to do all the intensive work in the scrollViewDidScroll: method. I only trigger it there.
So, for example, if you take most of the code you posted and put it in a method called loadAndReleasePages, then you could do this in the scrollViewDidScroll: method, which defers the execution until after scrollViewDidScroll: finishes:
- (void)scrollViewDidScroll:(UIScrollView *)tmpScrollView {
CGPoint offset = tmpScrollView.contentOffset;
//322 is the height of 2*2 buttons (a page for me)
int currentPage = (int)(offset.y / 322.0f);
if (currentPage != lastPage) {
lastPage = currentPage;
// we've changed pages, so load and release new content ...
// defer execution to keep scrolling responsive
[self performSelector: #selector(loadAndReleasePages) withObject: nil afterDelay:0];
}
}
This is code that I've used since early iOS versions, so you can certainly replace the performSelector: call with an asynchronous GCD method call, too. The point is not to do it inside the scroll view delegate callback.
3) Finally, you might want to experiment with slightly different algorithms for calculating when the scroll view has actually scrolled far enough that you want to load and release content. You currently use:
int currentPage=(int)(offset.y / 322.0f);
which will yield integer page numbers based on the way the / operator, and the float to int cast works. That may be fine. However, you might find that you want a slightly different algorithm, to trigger the loading at a slightly different point. For example, you might want to trigger the content load as the page has scrolled exactly 50% from one page to the next. Or you might want to trigger it only when you're almost completely off the first page (maybe 90%).
I believe that one scrolling intensive app I wrote actually did require me to tune the precise moment in the page scroll when I did the heavy resource loading. So, I used a slightly different rounding function to determine when the current page has changed.
You might play around with that, too.
Edit: after looking at your code a little more, I also see that the work you're doing is loading and scaling images. This is actually also a candidate for a background thread. You can load the UIImage from the filesystem, and do your scaling, on the background thread, and use GCD to finally set the button's background image (to the loaded image) and change its frame back on the UI thread.
UIImage is safe to use in background threads since iOS 4.0.
Don't touch a line of code until you've profiled. Xcode includes excellent tools for exactly this purpose.
First, in Xcode, make sure you are building to a real device, not the simulator
In Xcode, choose Profile from the Product menu
Once Instruments opens, choose the Core Animation instrument
In your app, scroll around in the scroll view you're looking to profile
You'll see the real time FPS at the top, and in the bottom, you'll see a breakdown of all function and method calls based on total time ran. Start drilling down the highest times until you hit methods in your own code. Hit Command + E to see the panel on the right, which will show you full stack traces for each function and method call you click on.
Now all you have to do is eliminate or optimize the calls to the most "expensive" functions and methods and verify your higher FPS.
That way you don't waste time optimizing blind, and potentially making changes that have no real effect on the performance.
My answer is really a more general approach to improving scroll view and table view performance. To address some of your particular concerns, I highly recommend watching this WWDC video on advanced scroll view use: https://developer.apple.com/videos/wwdc/2011/includes/advanced-scrollview-techniques.html#advanced-scrollview-techniques
The line that is likely killing your performance is:
addButton.layer.cornerRadius=10.0;
Why? Turns out the performance for cornerRadius is AWFUL! Take it out... guaranteed huge speedup.
Edit: This answer sums up what you should do quite clearly.
https://stackoverflow.com/a/6254531/537213
My most common solution is to rasterize the Views:
_backgroundView.layer.shouldRasterize = YES;
_backgroundView.layer.rasterizationScale = [[UIScreen mainScreen] scale];
But it works not in every situation.. Just try it

Reloading a non-table view (iOS)

I'm working on a painting app, and I want to user to be able to switch through paintings that are saved as PNGs to the documents directory. I've made an IBAction that does this:
-(IBAction)switchPage:(id)sender{
if ([sender tag] ==1)
{
currentImage=currentImage-1;
}
else
{
currentImage++;
}
[[NSUserDefaults standardUserDefaults] setInteger:currentImage forKey:#"currentDoc"];
[self.view setNeedsDisplay];
}
Just to let you know, I have 2 buttons, a back and forward, and they're connected to the same IBAction that's pasted above. The back button has a tag of 1, and the forward button doesn't have a tag. currentImage is an int that is used to set an image's name, so if currentImage =1, then it would set the image to image1. But that's not what I'm having trouble with, I'm having trouble "reloading" the view. I'm using setNeedsDisplay, but it's not working, and I know that reloadData is only for UITables, so what else could I use?
I've been searching for an answer, but none of the existing questions about this don't have a clear answer, or are under different circumstances.
Thanks for your time and/or dealing with a stupid question, as I'm new to Xcode.
-Karl
I see no view code in your post.
Assuming you have a UIImageView that is displaying the image you would just do:
myImageView.image = whateverUIImage;

Resources