App runs out of memory due to a huge UILabel

521 views Asked by At

I have to show a huge text file in my app. An UITextView do not fit my requirements because it forces line wrapping so I had to use an UILabel. Since very big labels do not get rendered, I'm using several UILabels inside an UIScrollView to make it work.

Everything works on the simulator but the required memory for the UILabels is about 300MB. When I run it on an iPad 2, it gets out of memory and the application crash.

The problem is that I'm not getting any memory warning. I would like to dismiss the view controller in didReceiveMemoryWarning but it is not been called, the app crashes without any warning.

What am I missing?

1

There are 1 answers

0
Guy Kogus On

Here's an example of using a UITableView to solve your problem.

HSViewController.h

@interface HSViewController : UITableViewController

@end

HSViewController.m

#import "HSViewController.h"

//#define USE_LABEL

static NSString *const kCellIdentifier = @"kCellIdentifier";

@interface HSViewController ()

@property (atomic, strong) NSArray *linesOfText;
@property (nonatomic, strong) UIFont *font;
@property (nonatomic, assign) NSLineBreakMode lineBreakMode;

- (CGSize)sizeForString:(NSString *)text;

@end

@implementation HSViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;

    self.font = [UIFont systemFontOfSize:14.0f];
    self.lineBreakMode = NSLineBreakByWordWrapping;

    [self.tableView registerClass:[UITableViewCell class]
           forCellReuseIdentifier:kCellIdentifier];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSData *draculaData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"Dracula" withExtension:@"txt"]];
        NSString *text = [[NSString alloc] initWithData:draculaData encoding:NSASCIIStringEncoding];
#ifndef USE_LABEL
        self.linesOfText = [text componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
#endif
        dispatch_async(dispatch_get_main_queue(), ^{
#ifdef USE_LABEL
            UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0.0f, 0.0f,
                                                                       self.view.bounds.size.width, self.view.bounds.size.height)];
            label.font = self.font;
            label.lineBreakMode = self.lineBreakMode;
            label.numberOfLines = 0;
            label.text = text;
            [self.view addSubview:label];
#else
            NSLog(@"Starting reloading %lu rows", (unsigned long)[self.linesOfText count]);
            [self.tableView reloadData];
            NSLog(@"Reload finished");
#endif
        });
    });
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
#ifdef USE_LABEL
    return 0;
#else
    return [self.linesOfText count];
#endif
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    @autoreleasepool {
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier forIndexPath:indexPath];
        cell.textLabel.numberOfLines = 0;
        cell.textLabel.font = self.font;
        cell.textLabel.lineBreakMode = self.lineBreakMode;
        cell.textLabel.text = self.linesOfText[indexPath.row];
        cell.userInteractionEnabled = NO;
        return cell;
    }
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    @autoreleasepool {
        NSString *text = self.linesOfText[indexPath.row];
        // Don't let the line height be 0
        if ([text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length == 0)
        {
            text = @"A";
        }
        return ceil([self sizeForString:text].height);
    }
}

#pragma mark - Private

- (CGSize)sizeForString:(NSString *)text
{
    @autoreleasepool {
        if ([text respondsToSelector:@selector(boundingRectWithSize:options:attributes:context:)])
        {
            NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
            style.lineBreakMode = self.lineBreakMode;
            return [text boundingRectWithSize:CGSizeMake(self.tableView.bounds.size.width, CGFLOAT_MAX)
                                      options:NSStringDrawingUsesLineFragmentOrigin
                                   attributes:@{NSFontAttributeName : self.font,
                                                NSParagraphStyleAttributeName : [style copy]}
                                      context:[NSStringDrawingContext new]].size;
        }
        else
        {
            return [text sizeWithFont:self.font
                    constrainedToSize:CGSizeMake(self.tableView.bounds.size.width, CGFLOAT_MAX)
                        lineBreakMode:self.lineBreakMode];
        }
    }
}

@end

You'll find that there's actually not a big difference in memory usage. The UITableView version only saves approx. 10% on memory.

However, this is a starting point for dynamically loading up table view cells. So when the user scrolls down (to let's say 90% of the screen) the next X cells can be loaded by changing the return value of tableView:numberOfRowsInSection:.

Good luck.