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.
Related
I'm doing a pretty simple iOS/ObjC program. Click a button, it's loops thru a for..next and displays counters and pics.
for (int i=0; i <= numberOfExercises; i++) {
exerciseName = [exercises objectAtIndex:j][0];
[self.lastLabel setText:exerciseName];
[self.lastLabel setNeedsDisplay];
usleep(1000000);
NSLog(#"Count: %d Name:%#", i, exerciseName);
}
However, it's not updating the actual textfield on the screen.
I've tried everything I know of and there's just something I can't see.
(IBAction)btnExerciseClicked:(id)sender {
for (int i=0; i <= numberOfExercises; i++) {
exerciseName = [exercises objectAtIndex:j][0];
exerciseImageName = [exercises objectAtIndex:j][1];
exerciseImageName = [exerciseImageName stringByAppendingString:#".jpg"];
self.exerciseImage.image = [UIImage imageNamed:exerciseImageName];
[self.ExerciseName setText:exerciseName];
[self.ExerciseName setNeedsDisplay];
[self.exerciseImage setNeedsDisplay];
}
I've put the block in there:
double delayInSeconds = 0.1;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
NSLog(#"inside the block");
[self.ExerciseName setText:exerciseName];
[self.ExerciseName setNeedsDisplay];
[self.exerciseImage setNeedsDisplay];
});
And I've done a direct assign before the loop starts:
exerciseName = [exercises objectAtIndex:5][0];
[self.ExerciseName setText:exerciseName];
[self.ExerciseName setNeedsDisplay];
NSLog(#"Name: %#", self.ExerciseName.text);
I've even written a separate method for it:
-(void) myCycleDisplay: (NSString *) imageName
nameOfExercise: (NSString *) exerciseName
voiceOver: (BOOL) useVoiceOver
countDown: (BOOL) useCountDown
beep: (BOOL) useBeep{
[self.ExerciseName setText:exerciseName];
[self.ExerciseName setNeedsDisplay];
self.exerciseImage.image = [UIImage imageNamed:imageName];
}
And after all that, STILL no display until AFTER the method is done. Put in a delay: usleep(1000000);
I've confirmed that the data is in the element. I've setup the element in code and in IB.
The app is pretty simple, a button is pressed on the screen, data is loaded, an array is walked and display items are updated based on elements in the array.
The data is in the textfield, this has been confirmed. It even display them, but only after the method is exited.
Once the method is exited, the text/pic are displayed properly.
So, as a test, I made the for..next loop run twice and clicked the button over and over. Sure enough it displayed properly AFTER leaving the method.
I can't get it to update the display while IN the method. (this also includes the slider).
Why do I have to exit a method to get the display to update?
I think the easiest solution to your problem is to dump the whole for-loop into another queue.
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
for (int i=0; i <= numberOfExercises; i++) {
NSString *exerciseName = [exercises objectAtIndex:i][0];
dispatch_async(dispatch_get_main_queue(), ^{
[self.lastLabel setText:exerciseName];
[self.lastLabel setNeedsDisplay];
});
usleep(1000000);
NSLog(#"Count: %d Name:%#", i, exerciseName);
}
});
There are three additional changes.
Changed [exercises objectAtIndex:j] to [exercises objectAtIndex:i]
I think this was a mistake on your part
Made exerciseName local to the block.
I could have made the declaration of exerciseName __block, but it's easier to just make the whole thing local.
Wrapped setting the label in dispatch_async(dispatch_get_main_queue(), ^{ … });
UI updates must be made on the main queue.
Note, this is a bad solution. You should rethink your approach entirely. I would move this logic into it's own separate class, then use notifications to get the UI to update.
When you call usleep(1000000) you are blocking the main queue. UI updates happen on the main queue, but you're blocking that queue, so they don't happen. Also, UI updates in general don't happen until you finish the current pass through the event loop-- from the user tapping your button, through your method doing whatever it needs to do, continuing until your method finishes. Then UIKit updates the UI. You need to let your method finish because that's how UIKit works.
I'm not exactly sure what you're trying to do. If you want to update your UI at intervals, look into NSTimer.
I hope I understood the problem well enough to give a relevant answer and I apologize if I didn't. Here's how I see it:
There is a set of exercises that have to be done one after another;
Exercises take certain amount of time;
The name of the exercise and a relevant image are displayed when exercise starts.
The current implementation does precisely that: display is updated and the system sleeps for a certain amount of time before updating the display again.
The problem with that approach is that UIKit frameworks that is commonly used when doing interactive things on iOS devices is what could be called "indirect". For instance, when .text property of a label is updated, text is not drawn, instead, the system is notified that display should be updated. UIKit periodically checks whether updates are requested and if they are, the display is redrawn.
All of this is done on the "main queue" by sequentially executing blocks of code added to it (as a side note, this is to unlike the way Javascript work in a browser). This means that as long as some block of code is executed, everything else of the main queue will not be and the app will stop being interactive and updating display. Thus, the way to use UIKit is by quickly notifying it of necessary changes and finishing the function. Consequently, one never does long calculations or "sleeps" on on the main queue.
In context of this question, this leaves a problem of timed updates: we want to update the UI after some time has expired. There are 2 solution that come to mind:
The "typical" one would to be use the NSTimer class;
If constant updates to the UI are necessary, there is a CADisplayLink class and a convenient [UIScreen displayLinkWithTarget:selector:] method to create one on, say, [UIScreen mainScreen]. What it does is, it calls the selector in time for screen updates (currently, that's about 60 times per second but you can configure it to be called less frequently). It is ideal for things like count-down timers and frequently updating UI elements such as progress bars.
(I've switched to Swift and the latest API so the method names might not be exactly right in Objective-C).
Please let me know if I misunderstood the question or if you need sample code or further clarification.
Good luck!
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..
}
So currently I am working on a camera app in iOS. In general, when "Capture" button is clicked on the screen, it will do the following:
Display UILabel "Saving.." on the screen
[camManager captureStillImage] //capturing the image
Remove UILabel "Saving.." from the screen
The problem was, the "Saving.." label never appear on the screen. But, when I remove step 3, the label will actually appear on the screen, but after capturing the image.
So based on my understanding, this was caused either because step 2 was executed too fast or by multithreading such that these steps are not guaranteed to execute in the order as I wrote them. Is this correct?
If so, how can I guarantee that this label appear right before capturing and disappear immediately after capturing?
Code
- (IBAction)captureImage:(id)sender {
[self showLabel];
[manager captureMultipleImg];
[self hideLabel];
}
You're blocking the main thread.
The main thread is responsible for UI stuff. When you're doing a long operation like [manager captureMultipleImg]; probably is, the UI will not get updated. You need to use multi-threading in cases like this.
You can use GCD here:
- (IBAction)captureImage:(id)sender {
[self showLabel];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
[manager captureMultipleImg];
// Dispatch back on main for UI stuff
dispatch_async(dispatch_get_main_queue(), ^{
[self hideLabel];
});
});
}
First of all this is not a duplicate. I have seen some identical questions but they didn't help me as my problem varies a little bit.
Using the following code i am download the images asynchronously in my project.
{
NSURL *imageURL = [NSURL URLWithString:imageURLString];
[self downloadThumbnails:imageURL];
}
- (void) downloadThumbnails:(NSURL *)finalUrl
{
dispatch_group_async(((RSSChannel *)self.parentParserDelegate).imageDownloadGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *tempData = [NSData dataWithContentsOfURL:finalUrl];
dispatch_async(dispatch_get_main_queue(), ^{
thumbnail = [UIImage imageWithData:tempData];
});
});
}
Due to the logic of the program, i have used the above code in files other than the tableview controller which is showing all the data after getting it from the web service.
PROBLEM: On screen images does not show up until i scroll. The off screen images are refreshed first. What can i do to solve my problem.
Apple's lazy loading project is using scrollViewDidEndDragging and scrollViewDidEndDecelerating to load the images but the project is way too big to understand plus my code is in files other than the tableview controller.
NOTE: Kindly do not recommend third party libraries like SDWebImage etc.
UPDATE: As most of people are unable to get the problem, i must clarify that this problem is not associated with downloading, caching and re-loading the images in tableview. So kindly do not recommend third party libraries. The problem is that images are only showing when the user scrolls the tableview instead of loading the on screen ones.
Thanks in advance
I think what you have to do is:
display some placeholder image in your table cell while the image is being downloaded (otherwise your table will look empty);
when the downloaded image is there, send a refresh message to your table.
For 2, you have two approaches:
easy one: send reloadData to your table view (and check performance of your app);
send your table view the message:
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
Using reloadRowsAtIndexPaths is much better, but it will require you to keep track of which image is associated to which table row.
Keep in mind that if you use Core Data to store your images, then this workflow would be made much much easier by integrating NSFetchedResultController with your table view. See here for an example.
Again another approach would be using KVO:
declare this observe method in ItemsViewCell:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ([keyPath isEqual:#"thumbnail"]) {
UIImage* newImage = [change objectForKey:NSKeyValueChangeNewKey];
if (newImage != (id)[NSNull null]) {
self.thumbContainer.image = newImage;
[self.thumbContainer setNeedsLayout];
}
}
}
then, when you configure the cell do:
RSSItem *item = [[channel items] objectAtIndex:[indexPath row]];
cell.titleLabel.text = [item title];
cell.thumbContainer.image = [item thumbnail];
[item addObserver:cell forKeyPath:#"thumbnail" options:NSKeyValueObservingOptionNew context:NULL];
By doing this, cell will be notified whenever the given item "thumbnail" keypath changes.
Another necessary change is doing the assignment like this:
self.thumbnail = [UIImage imageWithData:tempData];
(i.e., using self.).
ANOTHER EDIT:
I wanted to download and load the images just like in the LazyTableImages example by Apple. When its not decelerating and dragging, then only onscreen images are loaded, not all images are loaded at once.
I suspect we are talking different problems here.
I thought your issue here was that the downloaded images were not displayed correctly (if you do not scroll the table). This is what I understand from your question and my suggestion fixes that issue.
As to lazy loading, there is some kind of mismatch between what you are doing (downloading the whole feed and then archiving it as a whole) and what you would like (lazy loading). The two things do not match together, so you should rethink what you are doing.
Besides this, if you want lazy loading of images, you could follow these steps:
do not load the image in parser:foundCDATA:, just store the image URL;
start downloading the image in tableView:cellForRowAtIndexPath: (if you know the URL, you can use dataWithContentOfURL as you are doing on a separate thread);
the code I posted above will make the table update when the image is there;
at first, do not worry about scrolling/dragging, just make 1-2-3 work;
once it works, use the scrolling/dragging delegate to prevent the image from being downloaded (point 2) during scrolling/dragging; you can add a flag to your table view and make tableView:cellForRowAtIndexPath: download the image only if the flag says "no scrolling/dragging".
I hope this is enough for you to get to the end result. I will not write code for this, since it is pretty trivial.
PS: if you lazy load the images, your feed will be stored on disk without the images; you could as well remove the CGD group and CGD wait. as I said, there is not way out of this: you cannot do lazy loading and at the same time archive the images with the feed (unless each time you get a new image you archive the whole feed). you should find another way to cache the images.
Try using SDWebImage, it's great for using images from the web in UITableViews and handles most of the work for you.
The best idea is caching the image and use them. I have written the code for table view.
Image on top of Cell
It is a great solution.
Try downloading images using this code,
- (void) downloadThumbnails:(NSURL *)finalUrl
{
NSURLRequest* request = [NSURLRequest requestWithURL:finalUrl];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse * response,
NSData * data, NSError * error)
{
if (!error)
{
thumbnail = [UIImage imageWithData:data];
}
}];
}
I am completely new to implementing custom drawRect method (and Core Graphics) but am doing so to improve the scrolling performance for my UITableView. Please do let me know if I am doing anything stupid.
In my cell, I have a UIImage and over the bottom part of it I would like to print the caption of the image. However, in order for the caption text to show up clearly regardless of the image , I would like to have a black rectangle with opacity of ~75% on top of the UIImage and below the caption text.
I tried the following
[self.picture drawAtPoint:point];
[[UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.75] setFill];
UIRectFill(CGRectMake(rect));
but that resulting fill actually eat into the UIImage (excuse my poor description sorry) and the part showing below the slightly transparent fill is the background of my UITableView...
I guess I could have made another image for the rectangle and then draw it on top of the self.picture but I am wondering whether this is an easier way to use UIRectFill to achieve this instead...
as mentioned, I am completely new to Core Graphics so any hints would be much appreciated. thanks in advance!
Also, I have a second question... the dimension (in pixel) of the image downloaded is twice that of the rect (in points) that it will fit in, to account for retina display. However, it is now currently going over that rect, even on an iPhone4 device... How can I fix that (including for pre-iPhone4 devices too?)
I don't do much custom drawRect stuff, so I'll defer that portion of the question to someone else, but usually tableview performance issues are solved much more easily by moving the expensive calculations into a background queue and then asynchronously updating cell from the main queue when that background operation is done. Thus, something like:
First, define an operation queue property for the tableview:
#property (nonatomic, strong) NSOperationQueue *queue;
Then in viewDidLoad, initialize this:
self.queue = [[NSOperationQueue alloc] init];
self.queue.maxConcurrentOperationQueue = 4;
And then in cellForRowAtIndexPath, you could then:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"MyCellIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// Do the quick, computationally inexpensive stuff first, stuff here.
// Examples might include setting the labels adding/setting various controls
// using any images that you might already have cached, clearing any of the
// image stuff you might be recalculating in the background queue in case you're
// dealing with a dequeued cell, etc.
// Now send the slower stuff to the background queue.
[self.queue addOperationWithBlock:^{
// Do the slower stuff (like complex image processing) here.
// If you're doing caching, update the cache here, too.
// When done with the slow stuff, send the UI update back
// to the main queue...
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// see if the cell is still visible, and if so ...
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
if (cell)
{
// now update the UI back in the main queue
}
}];
}];
return cell;
}
You can optimize this further by making sure that you cache the results of your computationally-expensive stuff into something like a NSCache, and perhaps to Documents or elsewhere as well, thus as you can optimize how often that complex stuff has to be done and really optimize the UI.
And, by the way, when you do that, you can now just have your UILabel (with backgroundColor using that UIColor for black with 0.75 alpha) on top of the the UIImageView, and iOS takes care of it for you. As easy as it gets.
On the final question about image resolution, you can either:
use the view's contentScaleFactor to figure out whether you're dealing with retina or not and resize the thumbnail image accordingly; or
just use the imageview's contentMode of UIViewContentModeScaleAspectFill which will make sure that your thumbnail images are rendered correctly regardless ... if you're using small thumbnail images (even 2x images), the performance is generally fine.
this is the right way to do it, using the kCGBlendModeNormal option, per another stackoverflow question
[[UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.75] setFill];
UIRectFillUsingBlendMode(rect, kCGBlendModeNormal);