Asynchronously loading image into autolayout UIImageView - objective-c

I've searched SO and have not seen an answer specifically for Autolayout.
I have a UITableViewCell using Autolayout. The UIImageView is constrained to have its edges 12 pt from Cell edges and the trailing edge of the Label.
Images are retrieved asynchronously and assigned to the UIImageView's image property.
[self downloadImageWithURL:[NSURL URLWithString:imageUrlString] completionBlock:^(BOOL succeeded, UIImage *image) {
if (succeeded) {
weakSelf.newsImageView.image = image;
// [weakSelf.contentView layoutIfNeeded];
}
}];
If there is a blank placeholder image assigned to the image of the cell in cellForRowAtIndexPath, then the asynchronously retrieved image is loaded and visible. If the blank placeholder assignment is commented out, the asynchronously retrieved image never appears.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
SCTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:tableCellIdentifier];
// cell.newsImageView.image = _blankImage;
return cell;
}
I tried layoutIfNeeded (commented out above) and updateConstraintsIfNeeded to see if the image would appear but it did not work. So for now I've been putting a blank image there first, which seems like a hack, and wouldn't be flexible if relying on intrinsic content size of images who's sizes could be different. Are the constraints being optimized out of existence when the first layout occurs? Anyone know what's going on and how to resolve it?

Try calling: invalidateIntrinsicContentSize on your UIImageView in the completion handler. This should force the auto layout system to recalculate the view intrinsic size. Possibly, you could also need calling updateConstraintsIfNeeded after that (but try first without).
If this does not work and your UIImageViews have a known size, you could try subclassing UIImageView and override the -intrinsicContentSize method. Return the known size from it.

Turns out the nib somehow had two outlets referring to the same UIImageView element. One outlet didn't exist in code, yet still linked in the nib UI. Must have been a mishap in refactoring, but removing it fixes it.

Related

How to use Autolayout and self-sizing cells in iOS8

I have setup the auto-sizing cells with UILabels with no problem in iOS8 following this tutorial:
http://useyourloaf.com/blog/2014/08/07/self-sizing-table-view-cells.html
But I am having problems setting up a UIImageView using auto-sizing. I need the images to be different sizes in Landscape compared to portrait (so they retain the same aspect ratio). But each time I get a broken layout constraints error
"<NSLayoutConstraint:0x14554680 'UIView-Encapsulated-Layout-Height' V:[UITableViewCellContentView:0x14564440(200)]>
As the actual code has quite a few different types of cell (that all work with auto-sizing cells) I have setup a simple example of the issue with a blue image view here:
https://github.com/AdrianMW/ios8autosizingCells
I have setup the constraints to the superview and am setting up the height like so:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
ImageTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kImageCell forIndexPath:indexPath];
cell.heightConstraint.constant = [self heightForOrientation:self.interfaceOrientation];
return cell;
}
Solutions Tried
I have tried turning off setTranslatesAutosizingMask: on the cell as per some of the other suggestions but when I do I just get a blank space instead of a image. I have also tried adding the following:
[cell setNeedsDisplay];
[cell layoutIfNeeded];
I have seen mention of overriding sizethatfits: to get it to work on this cell using the old way but that doesn't seem to be called on the cell.
Cheers
In your ImageTableViewCell.xib, try changing the constraint:
Image View.Bottom EQUAL Superview.Bottom
Change the priority from 1000 to 999.

How can I override auto layout (in UICollectionView)

I have an UICollectionView and a custom UICollectionViewCell containing subviews.
I am doing some "esoteric" stuff inside this CollectionViewCell and would like do rearrange
all subviews by using setFrame manually. How can I override all autolayout behaviour?
I tried to override "-(void) layoutSubviews" and draw rect. Sometimes this works, sometimes not.
If I get a cell by using
UIMyCell *cell = (UIMyCell*)[collectionView dequeueReusableCellWithReuseIdentifier:#"UIMyCell" forIndexPath:indexPath];
I get my custom cell. I can assign custom properties and everything is fine. But the frame of this cell is initially (0, 0, 0, 0).
Some time later the layout will be set. I would like to be "notified" if the subsystem is setting the size.
Then I would like to set all subviews frames WITHOUT autolayout.
What is the best practice here?
Finally I found the answer.
On OS X (NSView)
-(void) setFrame:(CGRect)frame
will be called after all constraints have been updated to set new view positions and sizes.
On iOS (UIView)
-(void) setCenter:(CGPoint)center
and
-(void)setBounds:(CGRect)bounds
will be called instead. So overwriting setFrame is not working for iOS framework.

Resize UICollectionView cells after their data has been set

My UICollectionView cells contain UILabels with multiline text. I don't know the height of the cells until the text has been set on the label.
-(CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout*)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
This was the initial method I looked at to size the cells. However, this is called BEFORE the cells are created out of the storyboard.
Is there a way to layout the collection and size the cells AFTER they have been rendered, and I know the actual size of the cell?
I think your are looking for the invalidateLayout method you can call on the .collectionViewLayout property of your UICollectionView. This method regenerates your layout, which in your case means also calling -collectionView: layout: sizeForItemAtIndexPath:, which is the right place to reflect your desired item size. Jirune points the right direction on how to calculate them.
An example for the usage of invalidateLayout can be found here. Also consult the UICollectionViewLayout documentation on that method:
Invalidates the current layout and triggers a layout update.
Discussion:
You can call this method at any time to update the layout information. This method invalidates the layout of the collection view itself and returns right away. Thus, you can call this method multiple times from the same block of code without triggering multiple layout updates. The actual layout update occurs during the next view layout update cycle.
Edit:
For storyboard collection view which contains auto layout constraints, you need to override viewDidLayoutSubviews method of UIViewController and call invalidateLayout collection view layout in this method.
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[yourCollectionView.collectionViewLayout invalidateLayout];
}
subclass UICollectionViewCell and override layoutSubviews like this
hereby you will anchor cell leading and trailing edge to collectionView
override func layoutSubviews() {
super.layoutSubviews()
self.frame = CGRectMake(0, self.frame.origin.y, self.superview!.frame.size.width, self.frame.size.height)
}
Hey in the above delegate method itself, you can calculate the UILabel size using the below tricky way and return the UICollectionViewCell size based on that calculation.
// Calculate the expected size based on the font and
// linebreak mode of your label
CGSize maximumLabelSize = CGSizeMake(9999,9999);
CGSize expectedLabelSize =
[[self.dataSource objectAtIndex:indexPath.item]
sizeWithFont:[UIFont fontWithName:#"Arial" size:18.0f]
constrainedToSize:maximumLabelSize
lineBreakMode:NSLineBreakByWordWrapping];
- (void)viewDidLoad {
self.collectionView.prefetchingEnabled = NO;
}
In iOS 10, prefetchingEnabled is YES by default. When YES, the collection view requests cells in advance of when they will be displayed. It leads to crash in iOS 10

iCarousel and UITextView -- blank text view

I'm using iCarousel to display editable question cards. The cards contain a UITextView for entering the question (or already contain text as you swipe through filled cards). However, when the carousel is presented and scrolled, sometimes text views appear empty.
This is due to a UITextView optimization of not drawing text offscreen. But text views in a UITableView will not suffer from this.
As many know, using setNeedsDisplay will NOT work due to the optimization, so it doesn't redraw the text.
I currently change the text view's frame by adding and then removing 1px. This forces a redraw. However, I can only do this when the item changes. iCarousel does not have a willDisplayCell delegate method. (Nick, can you add one easily? The code baffles me)
Because iCarousel is preloading many views for smoothness (which is necessary, setting iCarouselOptionVisibleItems doesn't fix anything) there doesn't seem to be anything else I can do but know when the view is about to come on screen. Suggestions?
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSUInteger)index reusingView:(UIView *)view
{
MIQuestionView *questionView = (MIQuestionView *)view;
if (questionView == nil)
{
MIQuestionType type = [self.testSection.questionType integerValue];
questionView = [[MIQuestionView alloc] initWithFrame:CGRectNull questionType:type];
questionView.delegate = self;
}
questionView.question = [self.testSection.questions objectAtIndex:index];
return questionView;
}
The text view is embedded in the MIQuestionView. The text is set in the question setter. There's no way for me to know when it's coming onto the screen. To be clear, I don't want to resize. The text is not drawn offscreen and appear blank when coming on-screen.
Sorry, I didn't look for ambiguous code above:
- (id)initWithFrame:(CGRect)frame questionType:(MIQuestionType)type
{
if (CGRectEqualToRect(frame, CGRectNull))
frame = CGRectMake(0, 0, 650, 244);
self = [super initWithFrame:frame];
...
It's an odd bug. I'm not sure if it's an issue with iCarousel or just a quirk of iOS that you need to deal with when dynamically adding and removing UITextViews from the view hierarchy.
I have a solution that's maybe a bit cleaner than the ones you've found though; Just add this to your MICardView:
- (void)didMoveToSuperview
{
if (self.superview)
{
_textView.frame = self.bounds;
}
}
This basically forces the textView to re-layout every time the cardView is recycled, and it avoids you having to do anything special in your view controller to work around the issue.

Preload cells of uitableview

I acknowledge that UITableview load dynamically a cell when user scrolls. I wonder if there is a way to preload all cells in order not to load each one while scrolling. I need to preload 10 cells. Is this possible?
You can initialize table view cells, put them into an array precomputedCells and in the data source delegate method tableView:cellForRowAtIndexPath: return the precomputed cells from the array instead of calling dequeueReusableCellWithIdentifier:. (Similar to what you would do in a static table view.)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [self.precomputedCells objectAtIndex:indexPath.row];
}
It might also be useful to have a look at the
WWDC 2012 Session 211 "Building Concurrent User Interfaces on iOS"
where it is shown how to fill the contents of table view cells in background threads while keeping the user interface responsive.
I was looking for a solution to the original question and I thought I'd share my solution.
In my case, I only needed to preload the next cell (I won't go into the reason why, but there was a good reason).
It seems the UITableView renders as many cells as will fit into the UITableView frame assigned to it.
Therefore, I oversized the UITableView frame by the height of 1 extra cell, pushing the oversized region offscreen (or it could be into a clipped UIView if needed). Of course, this would now mean that when I scrolled the table view, the last cell wouldn't be visible (because the UITableView frame is bigger than it's superview). Therefore I added an additional UIView to the tableFooterView of the height of a cell. This means that when the table is scrolled to the bottom, the last cells sits nicely at the bottom of it's superview, while the added tableFooterView remains offscreen.
This can of course be applied to any number of cells. It should even be possible to apply it to preload ALL cells if needed by oversizing the UITableView frame to the contentSize iOS originally calculates, then adding a tableFooterView of the same size.
Hopefully this helps someone else with the same problem.
As suggested by Mark I also changed the height of my UITableView temporarily so that the table view creates enough reusable cells. Then I reset the height of my table view so that it stops creating reusable cells while scrolling.
To accomplish that I create a helper bool which is set to false by default:
var didPreloadCells = false
It is set to true when my table view first reloaded data and therefore created the first reusable cells.
resultsHandler.doSearch { (resultDict, error) -> Void in
[...]
self.tableView.reloadData()
self.didPreloadCells = true
[...]
}
The real trick happens in my viewDidLayoutSubviews Method. Here I set the frame of my table view depending on my boolean. If the reusable cells were not created yet I increase the frame of the table view. In the other case I set the normal frame
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.tableView.frame = self.view.bounds
if !didPreloadCells
{
self.tableView.frame.size.height += ResultCellHeight
}
}
With the help of that the table view creates more initial reusable cells than normal and the scrolling is smooth and fluent because no additional cells need to be created.
Change the dequeueReusableCellWithIdentifier when you are reusing the table view otherwise it will load the old data
Solution for autoresizing cells. Change 'estimatedRowHeight' to lower value
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 32; //Actual is 64
}
Actual estimated height is 64. 32 is used to add more cells for reuse to avoid lagging when scrolling begins