iOS UITableView with dynamic text and images rendered together (NSAttributedString + images)
Asked Answered
U

2

4

My problem is this: I have dynamic content in an iOS app (such as twits - although this is not a twitter app) that include both text and images (mostly icons/emoticons and thumbnails). I want to render both text and images together in a table row. The main difficulty here is that each row will have a different size (making caching harder), and I need to calculate image size dynamically to fit text around on each image occurrence (I can have like 20 emoticons one next to another + text + more emoticons + etc.).

I've been looking around and I've tried a few approaches. My initial idea was to use UIWebView. I was able to create a sample app with one UIWebView per row, and even with some "smart" NSCache for pre-rendered cells performance was not very good, plus I had to deal with UIWebView javascript callbacks to figure out when content was properly loaded.

My second attempt is to overwrite drawRect for a new UITableViewCell. Inside the drawRect method I'm using NSAttributedString and the NSSelectorFromString to set the ranges for each kind of content (regular text, bold, italic, different color, images). To fit the images I'm using the CTRunDelegateCallbacks callbacks (getAscent, getDescent, getWidth). Something like this:

            CTRunDelegateCallbacks callbacks;
            callbacks.version = kCTRunDelegateVersion1;
            callbacks.getAscent = ascentCallback;
            callbacks.getDescent = descentCallback;
            callbacks.getWidth = widthCallback;

            CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)imgAttr); // img Attr is a Dictionary with all image info

For more details on this approach please check this excellent post: http://www.raywenderlich.com/4147/how-to-create-a-simple-magazine-app-with-core-text#

Finally, two questions:

  • Is this the best approach, can I make this with good performance? I'm wondering if having to call drawRect for each cell for every scroll wont be cumbersome - is there anything I could do in advance? I've been playing around for a while, and it's still a big lagged - but I haven't yet tried to cache every single row in a separate thread and use that in cellForRowAtIndexPath (although that may exceed memory use);

  • I can't figure out the best way to obtain the row height for each tableview cell. How can I know the height used by my drawRect function after NSAttributedString has properly processed all my content?

--- Adding more info

After my drawRect creates all content (strings and images) I'm trying to use boundingRectWithSize to get the proper height:

CGRect frame = [self.attString boundingRectWithSize:CGSizeMake(320, 10000) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading | NSStringDrawingUsesDeviceMetrics context:nil];

This works fine if it's text only, but when I add the image glyphs to the content it does not calculate the height properly...

Unready answered 16/5, 2013 at 11:25 Comment(0)
I
2

This post may get you going in the right direction for calculating row height:

Retrieve custom prototype cell height from storyboard?

As for performance, have you run Time Profiler to narrow down what is causing lag?

Introductory answered 16/5, 2013 at 14:16 Comment(3)
I'll check performance with the time profiler as soon as I figure how to calculate the height. That post is great, but it does not tell how the height is calculated... I've been trying to use: CGRect frame = [self.attString boundingRectWithSize:CGSizeMake(320, 10000) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading | NSStringDrawingUsesDeviceMetrics context:nil]; but it does not get the proper value... it does not take my inline images into account..Unready
My experience with attributed strings is limited, but asking it to calculate its rect sounds like the right approach. But if it's giving a wrong answer, it may be a dead end unless you can think of a workaround. Can you put the string in a UILabel and call sizeToFit? This is what I've done in the past, though only with plain strings.Introductory
sizeToFit works, but then I would loose support for inline images of different sizes... I may have found an approach by using CTFrameGetLineOrigins and using the y origin for the last item in the array. I'll post here the final result if it works.. Thanks!Unready
D
3

I ran into the same issue und solved it today. I am drawing custom objects between the text with the CTRunDelegateCallbacks, but boundingRectWithSize:options:context:did not give me the correct height. (It ignores my custom objects).

Instead I found a lower level API in Core Text, which fulfils all requirements and is totally correct. I created a category for that issue. Have fun using it! :-)

Header file:

#import <Foundation/Foundation.h>

@interface NSAttributedString (boundsForWidth)

- (CGRect)boundsForWidth:(float)width;

@end

Implementation file:

#import "NSAttributedString+boundsForWidth.h"
#import <CoreText/CoreText.h>

@implementation NSAttributedString (boundsForWidth)

- (CGRect)boundsForWidth:(float)width
{
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)self);

    CGSize maxSize = CGSizeMake(width, 0);

    CGSize size = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), NULL, maxSize, nil);

    CFRelease(framesetter);

    return CGRectMake(0, 0, ceilf(size.width), ceilf(size.height));
}

@end
Digestive answered 28/2, 2014 at 19:48 Comment(0)
I
2

This post may get you going in the right direction for calculating row height:

Retrieve custom prototype cell height from storyboard?

As for performance, have you run Time Profiler to narrow down what is causing lag?

Introductory answered 16/5, 2013 at 14:16 Comment(3)
I'll check performance with the time profiler as soon as I figure how to calculate the height. That post is great, but it does not tell how the height is calculated... I've been trying to use: CGRect frame = [self.attString boundingRectWithSize:CGSizeMake(320, 10000) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading | NSStringDrawingUsesDeviceMetrics context:nil]; but it does not get the proper value... it does not take my inline images into account..Unready
My experience with attributed strings is limited, but asking it to calculate its rect sounds like the right approach. But if it's giving a wrong answer, it may be a dead end unless you can think of a workaround. Can you put the string in a UILabel and call sizeToFit? This is what I've done in the past, though only with plain strings.Introductory
sizeToFit works, but then I would loose support for inline images of different sizes... I may have found an approach by using CTFrameGetLineOrigins and using the y origin for the last item in the array. I'll post here the final result if it works.. Thanks!Unready

© 2022 - 2024 — McMap. All rights reserved.