Creating an Instagram-like photo feed - ios

I'm trying to make a photo feed for an app I'm making, similar to Instagram's:
Instagram photo feed
I've created a preliminary version using a UITableView for the feed, but it's becoming a hassle dealing with the dynamic nature of each cell. The number of likes and comments along with the comment text itself will determine how tall the cell is. I used UITextViews to draw the comment and like text, and approximate the cell height and positioning using the UITextViews' text with sizeWithFont. This solution seems very imprecise and has a lot of downsides.
I was thinking about using a UIWebView for the entire feed as an alternate solution. It would make positioning the like and comment text extremely simple, along with the ability to have variable font in the text as seen in Instagram. I haven't really used UIWebViews extensively, so I'm not sure how easy or hard it would be to create the whole feed this way.
Should I continue using my UITableView solution or look into redoing it all using a UIWebView?

In my app, I've got a UITableViewCell that dynamically adjusts its height based on the amount of text in the cell. I think it might help answer your question.
Here's the trick: There is no need to "approximate text sizes" --- check out the documentation for the method [NSString sizeWithFont: constrainedToSize: lineBreakMode:]
maximumSize = CGSizeMake(self.contentView.bounds.size.width - TEXT_LEFT_MARGIN * 2.0,
MAXIMUM_TEXT_HEIGHT);
textStringSize = [textLabel.text sizeWithFont:textLabel.font
constrainedToSize:maximumSize
lineBreakMode:textLabel.lineBreakMode];
So let's take a simple example where you want 10 pixels of whitespace above the label, and 15 pixels of whitespace below the label. You'd then generate a CGRect with the measurements above, with a height of 25 pixels added to the textStringSize.height value generated above.
Using this method means that your UITableViewCells will scale up and down nicely, regardless of the size of the comments in the different labels.

try this..
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleIdendifer=#"item";
UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:simpleIdendifer];
if(cell==nil)
{
cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:simpleIdendifer];
}
[[cell textLabel] setText:self.str];
[[cell textLabel] setNumberOfLines:0];
[[cell textLabel] setLineBreakMode:NSLineBreakByWordWrapping];
[[cell textLabel] setFont:[UIFont preferredFontForTextStyle:UIFontTextStyleBody]];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
//add your table view cell content here
NSString *string = self.str;
NSDictionary *attributes = #{NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleBody]};
CGRect frame = [string boundingRectWithSize:CGSizeMake(CGRectGetWidth(tableView.bounds), CGFLOAT_MAX) options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading) attributes:attributes context:nil];
return ceilf(CGRectGetHeight(frame)+TableViewCellPadding);
}

Related

UITableView repeating cells over and over

So I have this UITableView and it is repeating cells over and over and I don't get why, I have already tried googling this and looking at other similar stack overflow questions but none of the solutions in them seem to fix the problem I am having.
My code is the following:
NSArray in the ViewDidLoad method:
tableViewContents = #[
#"Infosecurity Netherlands:4th November:Jaarbeurs Utrecht-Utrecht,Netherlands:www.infosecurity.nl/",
#"MiCS Monaco International Clubbing Show:4th - 6th November:Grimaldi Forum-Monaco, Monaco:www.mics.mc/",
#"New York Audio Show 2015:6th - 8th November:Hilton Westchester-Rye Brook, NY:www.chestergroup.org/newyorkaudioshow/2015",
#"Tecno Multimedia InfoComm 2015:10th - 12th November:Corferias-Bogota, Columbia:www.tecnomultimedia.com/",
#"Productronica 2015:10th - 13th November:New Munich Trade Show Center-Munich, Germany:productronica.com/en/home",
#"CCW featuring SATCON co-located InfoComm Connections:11th November:Jacob Javits Center, New York, NY:www.ccwexpo.com/",
#"IAAPA Attractions Expo 2015:16th - 20th November:Orange County Convention Center-Orlando, FL:www.iaapa.org/expos/iaapa-attractions-expo/",
#"Streaming Media West 2015:17th November:Hyatt Regency Huntington Beach-Huntington Beach, CA:www.streamingmedia.com/Conferences/",
#"SATIS:17th - 19th November:Porte de Versailles-Paris, France:www.satis-expo.com/",
#"ISC East Expo 2015:18th November:Jacob K. Javits Convention Center-New York, NY:www.isceast.com/",
#"WFX Worship Facilities Conference and Expo:18th November:Music City Center-Nashville, TN:wfxweb.com/",
#"Inter BEE 2015:18th - 20th November:Makuhari Messe-Chiba, Japan:www.inter-bee.com/en/",
#"AV Executive Conference:18th - 20th November:TBA-Amelia Island, FL:www.infocomm.org/cps/rde/xchg/infocomm/hs.xsl/39221.htm",
#"JTSE Performance & Entertainment Technical Show & Convention 2015:24th November:Dock Pullman, Porte de la Chapelle-Paris, France:www.jtse.fr/en/index.php",
#"Government Video Expo 2015:1st - 3rd December:Walter E. Washington Convention Center-Washington, DC:www.gvexpo.com/",
#"IIDEX Canada:2nd December:Metro Toronto Convention Centre North-Toronto, ON, Canada:www.iidexcanada.com/",
#"IABM Annual International Business Conference 2015:3rd December:TBA:www.theiabm.org/",
#"SMSS The Social Media Strategies Summit Dallas:8th December:The Magnolia Hotel-Dallas, TX:www.socialmediastrategiessummit.com/dallas-2015/",
#"CineAsia:8th - 10th December:Hong Kong Convention & Exhibition Centre-Hong Kong, China:www.vnufilmgroup.com/cineasia",
#"CeBIT Bilisim Eurasia:17th - 19th December:Istanbul Expo Center-Istanbul, Turkey:www.cebitbilisim.com/en/index.php"
];
Table View Methods
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
//Amount of sections
return 1;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
//Amount of rows in each section
return [tableViewContents count];
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *contentsString = tableViewContents[indexPath.row];
NSArray *contentsArray = [contentsString componentsSeparatedByString:#":"];
//Cell Identifier
static NSString *simpleIdentifier = #"simpleIdentifier";
UILabel *eventNameLabel;
UILabel *eventDateLabel;
UILabel *eventLocationLabel;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:simpleIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:simpleIdentifier];
eventNameLabel = [[UILabel alloc]init];
eventDateLabel = [[UILabel alloc]init];
eventLocationLabel = [[UILabel alloc]init];
eventNameLabel.frame = CGRectMake(10, 10, 300, 20);
eventNameLabel.font = [UIFont boldSystemFontOfSize:14];
eventNameLabel.textColor = [UIColor colorWithRed:231.0f/255.0f
green:123.0f/255.0f
blue:50.0f/255.0f
alpha:1.0f];
eventDateLabel.frame = CGRectMake(10, 40, 300, 20);
eventDateLabel.font = [UIFont systemFontOfSize:12];
eventDateLabel.textColor = [UIColor grayColor];
eventLocationLabel.frame = CGRectMake(10, 70, 300, 20);
eventLocationLabel.font = [UIFont systemFontOfSize:12];
eventLocationLabel.textColor = [UIColor grayColor];
[cell addSubview:eventNameLabel];
[cell addSubview:eventDateLabel];
[cell addSubview:eventLocationLabel];
}
eventNameLabel.text = contentsArray[0];
eventDateLabel.text = contentsArray[1];
eventLocationLabel.text = contentsArray[2];
//Return Cell
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 100;
}
Thank you in advance for your help.
eventNameLabel and the other two labels are nil for reused cells so you never update the labels as desired.
You need an else statement added to your if that sets the three label variables to the labels in the existing cell.
Of course the better approach is to use a custom cell class with three properties for the three labels. Don't put all the logic in the view controller. It belongs in the cell class.
Obvious answer to your requirement is Custom Cell. Instead of doing all coding in cellforrowaatIndexpath method, customize the cell as per your requirement and use it for the table. It would be much easier to handle a Custom Cell. Refer these links for how to customize tableViewCell. http://www.appcoda.com/customize-table-view-cells-for-uitableview/ or http://www.appcoda.com/ios-programming-customize-uitableview-storyboard/ for customize cell.
Your code can't work as written. Think about it. Your cellForRowAtIndexPath method has 3 local variables eventNameLabel, eventDateLabel, and eventLocationLabel. If you create a new cell then those variables get pointed to your labels.
If you recycle an existing cell, though, those variables will be nil.
You need another way to get to the labels. How about assigning tags to them when you create the cells, and then in the code that deals with a recycled cell find the labels using those tags.
EDIT:
Or, as rmaddy suggests in his answer that beat mine by 1 minute, use a custom cell class where the cell has IBOutlet properties linked to the labels.
You can try putting 3 labels in your interface and this will let you access the labels in your cell. put it in
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
-
NSArray *subviews = [[cell contentView] subviews];
UILabel *eventNameLabel = [subviews objectAtIndex:0];
UILabel *eventDateLabel = [subviews objectAtIndex:1];
UILabel *eventLocationLabel = [subviews objectAtIndex:2];
with this you don't have to add subViews either. another way is u can make a UITableViewCell class which you can link to the cell and then control drag labels to the cell class file as you would in a view controller.

AutoLayout row height miscalculating for NSAttributedString

My app pulls HTML from an API, converts it into a NSAttributedString (in order to allow for tappable links) and writes it to a row in an AutoLayout table. Trouble is, any time I invoke this type of cell, the height is miscalculated and the content is cut off. I have tried different implementations of row height calculations, none of which work correctly.
How can I accurately, and dynamically, calculate the height of one of these rows, while still maintaining the ability to tap HTML links?
Example of undesired behavior
My code is below.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
switch(indexPath.section) {
...
case kContent:
{
FlexibleTextViewTableViewCell* cell = (FlexibleTextViewTableViewCell*)[TableFactory getCellForIdentifier:#"content" cellClass:FlexibleTextViewTableViewCell.class forTable:tableView withStyle:UITableViewCellStyleDefault];
[self configureContentCellForIndexPath:cell atIndexPath:indexPath];
[cell.contentView setNeedsLayout];
[cell.contentView layoutIfNeeded];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.desc.font = [UIFont fontWithName:[StringFactory defaultFontType] size:14.0f];
return cell;
}
...
default:
return nil;
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
UIFont *contentFont = [UIFont fontWithName:[StringFactory defaultFontType] size:14.0f];
switch(indexPath.section) {
...
case kContent:
return [self textViewHeightForAttributedText:[self convertHTMLtoAttributedString:myHTMLString] andFont:contentFont andWidth:self.tappableCell.width];
break;
...
default:
return 0.0f;
}
}
-(NSAttributedString*) convertHTMLtoAttributedString: (NSString *) html {
return [[NSAttributedString alloc] initWithData:[html dataUsingEncoding:NSUTF8StringEncoding]
options:#{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
NSCharacterEncodingDocumentAttribute: #(NSUTF8StringEncoding)}
documentAttributes:nil
error:nil];
}
- (CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andFont:(UIFont *)font andWidth:(CGFloat)width {
NSMutableAttributedString *mutableText = [[NSMutableAttributedString alloc] initWithAttributedString:text];
[mutableText addAttribute:NSFontAttributeName value:font range:NSMakeRange(0, text.length)];
UITextView *calculationView = [[UITextView alloc] init];
[calculationView setAttributedText:mutableText];
CGSize size = [self text:mutableText.string sizeWithFont:font constrainedToSize:CGSizeMake(width,FLT_MAX)];
CGSize sizeThatFits = [calculationView sizeThatFits:CGSizeMake(width, FLT_MAX)];
return sizeThatFits.height;
}
In the app I'm working on, the app pulls terrible HTML strings from a lousy API written by other people and converts HTML strings to NSAttributedString objects. I have no choice but to use this lousy API. Very sad. Anyone who has to parse terrible HTML string knows my pain. I use Text Kit. Here is how:
parse html string to get DOM object. I use libxml with a light wrapper, hpple. This combination is super fast and easy to use. Strongly recommended.
traverse the DOM object recursively to construct NSAttributedString object, use custom attribute to mark links, use NSTextAttachment to mark images. I call it rich text.
create or reuse primary Text Kit objects. i.e. NSLayoutManager, NSTextStorage, NSTextContainer. Hook them up after allocation.
layout process
Pass the rich text constructed in step 2 to the NSTextStorage object in step 3. with [NSTextStorage setAttributedString:]
use method [NSLayoutManager ensureLayoutForTextContainer:] to force layout to happen
calculate the frame needed to draw the rich text with method [NSLayoutManager usedRectForTextContainer:]. Add padding or margin if needed.
rendering process
return the height calculated in step 5 in [tableView: heightForRowAtIndexPath:]
draw the rich text in step 2 with [NSLayoutManager drawGlyphsForGlyphRange:atPoint:]. I use off-screen drawing technique here so the result is an UIImage object.
use an UIImageView to render the final result image. Or pass the result image object to the contents property of layer property of contentView property of UITableViewCell object in [tableView:cellForRowAtIndexPath:].
event handling
capture touch event. I use a tap gesture recognizer attached with the table view.
get the location of touch event. Use this location to check if user tapped a link or an image with [NSLayoutManager glyphIndexForPoint:inTextContainer:fractionOfDistanceThroughGlyph] and [NSAttributedString attribute:atIndex:effectiveRange:].
Event handling code snippet:
CGPoint location = [tap locationInView:self.tableView];
// tap is a tap gesture recognizer
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:location];
if (!indexPath) {
return;
}
CustomDataModel *post = [self getPostWithIndexPath:indexPath];
// CustomDataModel is a subclass of NSObject class.
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
location = [tap locationInView:cell.contentView];
// the rich text is drawn into a bitmap context and rendered with
// cell.contentView.layer.contents
// The `Text Kit` objects can be accessed with the model object.
NSUInteger index = [post.layoutManager
glyphIndexForPoint:location
inTextContainer:post.textContainer
fractionOfDistanceThroughGlyph:NULL];
CustomLinkAttribute *link = [post.content.richText
attribute:CustomLinkAttributeName
atIndex:index
effectiveRange:NULL];
// CustomLinkAttributeName is a string constant defined in other file
// CustomLinkAttribute is a subclass of NSObject class. The instance of
// this class contains information of a link
if (link) {
// handle tap on link
}
// same technique can be used to handle tap on image
This approach is much faster and more customizable than [NSAttributedString initWithData:options:documentAttributes:error:] when rendering same html string. Even without profiling I can tell the Text Kit approach is faster. It's very fast and satisfying even though I have to parse html and construct attributed string myself. The NSDocumentTypeDocumentAttribute approach is too slow thus is not acceptable. With Text Kit, I can also create complex layout like text block with variable indentation, border, any-depth nested text block, etc. But it does need to write more code to construct NSAttributedString and to control layout process. I don't know how to calculate the bounding rect of an attributed string created with NSDocumentTypeDocumentAttribute. I believe attributed strings created with NSDocumentTypeDocumentAttribute are handled by Web Kit instead of Text Kit. Thus is not meant for variable height table view cells.
EDIT:
If you must use NSDocumentTypeDocumentAttribute, I think you have to figure out how the layout process happens. Maybe you can set some breakpoints to see what object is responsible for layout process. Then maybe you can query that object or use another approach to simulate the layout process to get the layout information. Some people use an ad-hoc cell or a UITextView object to calculate height which I think is not a good solution. Because in this way, the app has to layout the same chunk of text at least twice. Whether you know or not, somewhere in your app, some object has to layout the text just so you can get information of layout like bounding rect. Since you mentioned NSAttributedString class, the best solution is Text Kit after iOS 7. Or Core Text if your app is targeted on earlier iOS version.
I strongly recommend Text Kit because in this way, for every html string pulled from API, the layout process only happens once and layout information like bounding rect and positions of every glyph are cached by NSLayoutManager object. As long as the Text Kit objects are kept, you can always reuse them. This is extremely efficient when using table view to render arbitrary length text because text are laid out only once and drawn every time a cell is needed to display. I also recommend use Text Kit without UITextView as the official apple docs suggested. Because one must cache every UITextView if he wants to reuse the Text Kit objects attached with that UITextView. Attach Text Kit objects to model objects like I do and only update NSTextStorage and force NSLayoutManager to layout when a new html string is pulled from API. If the number of rows of table view is fixed, one can also use a fixed list of placeholder model objects to avoid repeat allocation and configuration. And because drawRect: causes Core Animation to create useless backing bitmap which must be avoided, do not use UIView and drawRect:. Either use CALayer drawing technique or draw text into a bitmap context. I use the latter approach because that can be done in a background thread with GCD, thus the main thread is free to respond to user's operation. The result in my app is really satisfying, it's fast, the typesetting is nice, the scrolling of table view is very smooth (60 fps) since all the drawing process are done in background threads with GCD. Every app needs to draw some text with table view should use Text Kit.
You need to update intrinsic content size.
I assume that you set attributed text to label in this code [self configureContentCellForIndexPath:cell atIndexPath:indexPath];
So, it should look like this
cell.youLabel.attributedText = NSAttributedString(...)
cell.youLabel.invalidateIntrinsicContentSize()
cell.youLabel.layoutIfNeeded()
You height calculation code (CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andFont:(UIFont *)font andWidth:(CGFloat)width should be replaced with cell height calculation using prototyping cell.
I'm assuming you are using a UILabel to display the string?
If you are, I have had countless issues with multiline labels with autoLayout. I provided an answer here
Table View Cell AutoLayout in iOS8
which also references another answer of mine that has a breakdown of how i've solved all my issues. Similar issues have cropped up again in iOS 8 that require a similar fix in a different area.
All comes down to the idea of setting the UILabel's preferredMaxLayoutWidth every time is bounds change. What also helped is setting the cells width to be the width of the tableview before running:
CGSize size = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
I ran into a very similar issue on another project where fields using NSAttributedString weren't rendering with the correct height. Unfortunately, there are two bugs with it that made us completely drop using it in our project.
The first is a bug that you've noticed here, where some HTML will cause an incorrect size calculation. This is usually from the space between the p tags. Injecting CSS sort of solved the issue, but we had no control over the incoming format. This behaves differently between iOS7 and iOS8 where it's wrong on one and right on the other.
The second (and more serious) bug is that NSAttributedString is absurdly slow in iOS 8. I outlined it here: NSAttributedString performance is worse under iOS 8
Rather than making a bunch of hacks to have everything perform as we wanted, the suggestion of using https://github.com/Cocoanetics/DTCoreText worked out really well for the project.
If you can target iOS 8 using dynamic cell sizing is the ideal solution to your problem.
To use dynamic cell sizing, delete heightForRowAtIndexPath: and set self.tableView.rowHeight to UITableViewAutomaticDimension.
Here is a video with more details:
https://developer.apple.com/videos/wwdc/2014/?include=226#226
You can replace this method to calculate the height of attributed string:
- (CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andFont:(UIFont *)font andWidth:(CGFloat)width {
CGFloat result = font.pointSize + 4;
if (text)
result = (ceilf(CGRectGetHeight([text boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading context:nil])) + 1);
return result;
}
Maybe the font you changed doesnt matches with the font of content on html pages. So, use this method to create attributed string with appropriate font:
// HTML -> NSAttributedString
-(NSAttributedString*) convertHTMLtoAttributedString: (NSString *) html {
NSError *error;
NSDictionary *options = #{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType};
NSAttributedString *attrString = [[NSAttributedString alloc] initWithData:[html dataUsingEncoding:NSUTF8StringEncoding] options:options documentAttributes:nil error:&error];
if(!attrString) {
NSLog(#"creating attributed string from HTML failed: %#", error.debugDescription);
}
return attrString;
}
// force font thrugh & css
- (NSAttributedString *)attributedStringFromHTML:(NSString *)html withFont:(UIFont *)font {
return [self convertHTMLtoAttributedString:[NSString stringWithFormat:#"<span style=\"font-family: %#; font-size: %f\";>%#</span>", font.fontName, font.pointSize, html]];
}
and in your tableView:heightForRowAtIndexPath: replace it with this:
case kContent:
return [self textViewHeightForAttributedText:[self attributedStringFromHTML:myHTMLString withFont:contentFont] andFont:contentFont andWidth:self.tappableCell.width];
break;
You should be able to convert to an NSString to calculate the height like this.
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
UIFont * font = [UIFont systemFontOfSize:15.0f];
NSString *text = [getYourAttributedTextArray objectAtIndex:indexPath.row] string];
CGFloat height = [text boundingRectWithSize:CGSizeMake(self.tableView.frame.size.width, maxHeight) options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading) attributes:#{NSFontAttributeName: font} context:nil].size.height;
return height + additionalHeightBuffer;
}
[cell.descriptionLabel setPreferredMaxLayoutWidth:375.0];

UITableView scrolling is not smooth

I have the smooth scrolling issue at my UITableView with UITableViewCell which contains UIImageView. Similar issues could be found all over the StrackOverflow but none of the proposed solutions helped me to completely get rid of the lag.
My case is quite common:
images are stored at application storage (in my sample at app bundle)
images could have different size (500x500, 1000x1000, 1500x1500)
I need to display those images in UITableView where UIImageView size is 120x120 (retina)
I have followed multiple optimization tips and managed to optimize scrolling a lot.
Unfortunately it is still not perfect. This is my scenario:
first I moved all the image loading/processing/resizing logic to the background thread
UITableViewCell reuse is enabled
once UITableViewCell is in view I clear old values (settings to null) and start background thread to load the image
at this point we are in background thread and I'm adding 500 ms delay to avoid settings new image to often (in case we are scrolling fast) (see below explanation)
if UIImage exists at static image cache (regular dictionary with UIImage instances) - fetch that one and go to the step 9.
if not - load new image from bundle (imageWithName) using url to app bundle (in real world scenario images will be stored to application storage, not bundle)
once image is loaded resize it to 120x120 using graphics context
save resized image to the static image cache
at this point we have instance to UIImage and process is in the background thread. From here we move back to UI Thread with the given image
if data context was cleared (for example UITableViewCell disappeared or was reused to display another image) we skip processing of the currently available image.
if data context is the same - assign UIImage to UIImageView with an alpha animation (UIView.Animate)
once UITableViewCell is out of view - clear the data context
Originally before starting new background thread to fetch the image here (step 1) was UIImage cache check without background thread. In this case if we have the image in the cache we assign it instantly and this introduces a great lag during fast scrolling (we assign images to often as long as we fetch them instantly). Those lines are commented at my example attached below.
There are still two issues:
at some point during scrolling I still have a small lag (at the
moment when I'm assign new UIImage to UIImageView.
(this one is more noticeable) when you tap on item and go back from details there is a lag right before back navigation animation is finished.
Any suggest how to deal with those two issues or how to optimize my scenario are appreciated
Please take into account that sample written in Xamarin but I don't believe that Xamarin is the cause of the problem as long as I have the same issue for the app written in ObjectiveC as well.
Smooth Scrolling Test App
Did you every tried to populate your TableView with only one 120x120 Image which is saved in your Bundle? This way you can check, if the problem occurs of your Image rendering
Instead of resizing all your images to 120x120 and save them in cache, I would recommend creating and using a thumbnail of all your images. You are somehow already doing this, but you are doing this couple of times (everytime you are scrolling or if your cache is full).
In our last project we had a UICollectionView with book covers. Most of the covers were between 400-800kb big and the feeling while scrolling was really bad. So we created a thumbnail for each image (thumbails about 40-50kb) and used the thumbnails instead of real covers. Works like a charm! I attached the thumbnail creation function
- (BOOL) createThumbnailForImageAtFilePath:(NSString *)sourcePath withName:(NSString *)name {
UIImage* sourceImage = [UIImage imageWithContentsOfFile:sourcePath];
if (!sourceImage) {
//...
return NO;
}
CGSize thumbnailSize = CGSizeMake(128,198);
float imgAspectRatio = sourceImage.size.height / sourceImage.size.width;
float thumbnailAspectRatio = thumbnailSize.height/thumbnailSize.width;
CGSize scaledSize = thumbnailSize;
if(imgAspectRatio >= thumbnailAspectRatio){
//image is higher than thumbnail
scaledSize.width = scaledSize.height * thumbnailSize.width / thumbnailSize.height;
}
else{
//image is broader than thumbnail
scaledSize.height = scaledSize.width * imgAspectRatio;
}
UIGraphicsBeginImageContextWithOptions( scaledSize, NO, 0.0 );
CGRect scaledImageRect = CGRectMake( 0.0, 0.0, scaledSize.width, scaledSize.height );
[sourceImage drawInRect:scaledImageRect];
UIImage* destImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
NSString* thumbnailFilePath = [[self SOMEDIRECTORY] stringByAppendingPathComponent:name];
BOOL success = [UIImageJPEGRepresentation(destImage, 0.9) writeToFile:thumbnailFilePath atomically:NO];
return success;
}
Try facebook's Async Display library.
https://github.com/facebook/AsyncDisplayKit
Really easy to use.. from their guide: http://asyncdisplaykit.org/guide/
_imageNode = [[ASImageNode alloc] init];
_imageNode.backgroundColor = [UIColor lightGrayColor];
_imageNode.image = [UIImage imageNamed:#"hello"];
_imageNode.frame = CGRectMake(10.0f, 10.0f, 40.0f, 40.0f);
[self.view addSubview:_imageNode.view];
This decodes the image on a background thread.
I'm not sure if it's easy to use iOS libraries on Xamarin but if it's easy, give this a shot.
I sub-class Paul Hegarty's CoreDataTableViewController and employ thumbnails of my photos in the CoreDataTableView.
Look for the examples in Lecture 14 titled FlickrFetcher and Photomania. You will also need to download the CoreDataTableViewController at that same link.
Make a CoreData Entity with an appropriate title and define whatever attributes (data variables) you want. You will need to define two "Transformable" attributes, one for the photo and one for the thumbnail.
Then load your thumbnail in the CoreDataTableView:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSArray *exceptions = [NSArray arrayWithObjects:#"SCR", #"DNS", #"NT", #"ND", #"NH", nil];
static NSString *CellIdentifier = #"resultsDisplayCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
}
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
MarksFromMeets *athleteMarks = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSString* date = [ITrackHelperMethods dateToAbbreviatedString:athleteMarks.meetDate];
NSMutableString *title = [NSMutableString stringWithFormat:#"%#", athleteMarks.markInEvent];
NSMutableString *subTitle = [NSMutableString stringWithFormat:#"%# - %#",date, athleteMarks.meetName];
[title replaceOccurrencesOfString:#"(null)"
withString:#""
options:0
range:NSMakeRange(0, [title length])];
// cell.imageView.image = athleteMarks.photoThumbNail; // Don't like image in front of record.
[cell.textLabel setFont:[UIFont
fontWithName:#"Helvetica Neue" size:18]];
[cell.detailTextLabel setFont:[UIFont fontWithName:#"Helvetica Neue" size:16]];
[cell.detailTextLabel setTextColor:[UIColor grayColor]];
// make selected items orange
if ([athleteMarks.eventPR integerValue] != 0
&& (![exceptions containsObject:athleteMarks.markInEvent])) {
title = [NSMutableString stringWithFormat:#"%# (PR)",title];
[cell.textLabel setTextColor:[UIColor redColor]];
}
else if ([athleteMarks.eventSB integerValue] != 0
&& (![exceptions containsObject:athleteMarks.markInEvent])) {
title = [NSMutableString stringWithFormat:#"%# (SB)",title];
[cell.textLabel setTextColor:[UIColor orangeColor]];
} else {
[cell.textLabel setTextColor:[UIColor grayColor]];
}
cell.textLabel.text = title;
cell.detailTextLabel.text = subTitle;
cell.indentationLevel = indentationLevelOne;
cell.indentationWidth = indentationForCell;
return cell;
}
If you want, I can send you an example of a Category for an Entity's NSManagedObject Sub-Class. This Category loads the photo and the thumbnail into CoreData Entity. The first time will be slow. However, after that the user should be able to scroll through TableView smoothly and then all the updated results will load automatically. Let me know.
One nice thing is that CoreData handles all the memory management.
Good luck!
I don't have enough rep to comment, So here's an answer which helped my tableview scrolling performance:
Make the tableview height larger than the viewable window. Cells will load "off screen" and helps improve scroll smoothness.
Do your image processing in the following method:
-(void)tableView:(UITableView *)tableView
willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
Those two tricks got my table flowing really nice. I'm getting my image data from an API service and AFNETWORKING has an awesome image loader, but not necessary for you since images are in the bundle.
Maybe you could try SDWebImage instead. It is also a xamarin component
which fashions an asynchronous image downloader and asynchronous memory and disk image caching with automatic cache expiration handling. Using it would probably mean throwing away a lot of hard written code, but it might be worth it -plus your code will become a lot simpler. In iOS you can also setup a SDWebImageManager inside the viewDidLoad of a controller:
- (void)viewDidLoad{
...
SDWebImageManager *manager = [SDWebImageManager sharedManager];
manager.delegate = self;
...
}
and set the view controller as the delegate. Then, when the following delegate method is called:
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL
you could scale your images to thumbs of the appropriate size before caching them.
Hope that helps.
Weel I had a similar problem, my scroll was not smooth. I am inserting in the table a variable UIImageView with inside labelViews.
What I did was to change the method HeightforRowAtIndexPath for estimatedHeightforRowAtIndexPath and now scroll is smooth.

Resizing table cells dynamically in iOS application

I am trying to make a Twitter client with XCode 4.2. (iOS version 5.) I want my application's main timeline to look similar to the Twitter iOS app's timeline:
I am using a UITableView with a prototype cell containing a label and three buttons. Below is the code I'm using to set the height:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString* cellIdentifier = #"TweetContainerCell";
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
#try {
UILabel* tweetLabel;
if(cell != nil) {
tweetLabel = (UILabel*)[cell.contentView viewWithTag:1];
NSString* tweetText = tweetLabel.text;
CGSize expectedLabelSize = [tweetText sizeWithFont:[UIFont fontWithName:tweetLabel.font.fontName size:20] constrainedToSize:CGSizeMake(CGFLOAT_MIN, CGFLOAT_MAX) lineBreakMode:UILineBreakModeWordWrap];
//[tweetLabel setFrame:CGRectMake(tweetLabel.frame.origin.x, tweetLabel.frame.origin.y, cell.frame.size.width, expectedLabelSize.height)];
//[cell.textLabel setFrame:tweetLabel.bounds];
//[tweetLabel sizeToFit];
//[cell setFrame:CGRectMake(cell.frame.origin.x, cell.frame.origin.y, cell.frame.size.width, expectedLabelSize.height)];
//[cell sizeToFit];
NSLog(#"Font size: %f", expectedLabelSize.height);
return (expectedLabelSize.height * 2);
}
}
/* Imgur URL: http://i.imgur.com/lHnsAsP.png */
/* http://i.imgur.com/hA9EKfI.png */
#catch (NSException* exception) {
NSLog(#"Exception: %#", exception);
}
return 0;
}
However, this is what my app ends up looking like:
The problems are:
1) Each cell seems to have the same height as the tallest cell in the whole table, instead of having different heights.
2) Because of this, the space between the cell's top border and the text is different for each cell (because iOS centers the text vertically).
I am learning iOS development and being unable to do such a simple thing, even after doing a lot of research and spending a lot of hours, seems really discouraging. Any help is greatly appreciated.
(In case the information I have given is not enough, here's the ZIP file containing the whole project: https://db.tt/m5suxWCj)
Your problem is that your label has not been created, since tableView:heightForRowAtIndexPath: is initially called before tableView:cellForRowAtIndexPath:, which is where your cell is created. In tableView:heightForRowAtIndexPath: you should determine the height of the cell as efficiently as possible, without involving UIViews.
To achieve this, you should store the NSString elsewhere in your table view data source, and calculate expectedLabelSize based on that.
Note also that sizeWithFont: is deprecated in IOS 7, so for IOS 7 and beyond you should use sizeWithAttributes: instead.

my chat system looks a little wierd, cant get dynamic height for cell

Ive got a chat system in my app, and im attempting to make dynamic cells to have dynamic height according to how much text is in the cell, pretty common thing people try to do, however i cant get to get mine working properly.
Also the messages align to the right, the sender is supposed to be on the left and the reciever should be on the right... heres what i have done with the storyboard.
created a TableView with 2 dynamic prototypes, inside a UIViewControllerhere is the viewController for that... each cell has a label, one left one right, the whole right and left thing work... heres my issue. Its only pulling to the right for all, so basically my if isnt happening and my else is overruling. Heres a SS.
So i have two issues... Text wont have multiple lines... along with wont do dynamic height, also... if someone can point me i the right dirrection for getting sender and reciever to show on different sides.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSArray *myWords = [[getMessage objectAtIndex:indexPath.row] componentsSeparatedByString:#":oyr4:"];
if (myWords[1] == [MyClass str]){
static NSString *sender = #"sender";
UITableViewCell* cellSender = [_tableView dequeueReusableCellWithIdentifier:sender];
messageContentTo = (UILabel *)[cellSender viewWithTag:83];
self->messageContentTo.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.8];
self->messageContentTo.lineBreakMode = NSLineBreakByWordWrapping;
[self->messageContentTo sizeToFit];
messageContentTo.text = myWords[4];
return cellSender;
} else {
static NSString *reciever = #"reciever";
UITableViewCell* cellReciever = [_tableView dequeueReusableCellWithIdentifier:reciever];
messageContentFrom = (UILabel *)[cellReciever viewWithTag:84];
messageContentFrom.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.8];
messageContentFrom.lineBreakMode = NSLineBreakByWordWrapping;
messageContentFrom.font = [UIFont systemFontOfSize:22];
messageContentFrom.numberOfLines = 0;
messageContentFrom.text = myWords[4];
return cellReciever;
}
}
#pragma mark - UITableViewDelegate methods
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGSize size = [[getMessage objectAtIndex:indexPath.row]
sizeWithFont:[UIFont systemFontOfSize:22]
constrainedToSize:CGSizeMake(1000, CGFLOAT_MAX)];
return size.height + 15;
}
The left-right problem might be due to this:
if (myWords[1] == [MyClass str])
If myWords[1] is a string, you need to use isEqualToString: not "==" to compare it.
if ([myWords[1] isEqualToString:[MyClass str]])
As far as the label height not adjusting properly, it's hard to tell what's going on without knowing how your labels are set up. I usually do it by making constraints between the label and the top and bottom of the cell in IB. That way, when you change the height of the cell, the label will follow (and of course, set numberOfLines to 0). Also, in your sizeWithFont:constrainedToSize: method, the width you pass into CGSizeMake() should be the width of the label, not 1000.

Resources