15

I have UITableView with dynamic sizing cells that displays list of comments in HTML format and I faced with the problem that NSAttributedString renders HTML content extremely slow!

Here is snapshot from profiler.

enter image description here

I tried to put the NSAttributedString initialization to separate thread, but still slow and user sees empty cells while HTML is being rendered and finally when it finished rendering cell is not layout properly.

    dispatch_async(GlobalQueue, {
       let html = NSAttributedString(
                    data: self.comment.htmlBody.dataUsingEncoding(NSUnicodeStringEncoding, allowLossyConversion: false)!,
                    options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
                    documentAttributes: nil,
                    error: nil)

        dispatch_async(MainQueue, {
            self.commentLabel.attributedText = html
            self.commentLabel.font = UIFont(name: "HelveticaNeue-Light", size: 14.0)!

            if let writer = self.comment.author {
                self.authorLabel.text = writer.name
            }

            self.layoutIfNeeded()
      })
   })

Looks following enter image description here

Please advice how to speed up rendering and fix cell layout.

Thanks!

UPDATE:

Solved with cell delegate and flag indicating that attributed string is initialized. Maybe would help somebody:

// TicketCell    
private var isContentInitialized = false
private var commentAttributedString:NSAttributedString?

var delegate: TicketCommentCellDelegate?
var indexPath: NSIndexPath!
var comment: TicketComment! {
    willSet {
        if newValue != self.comment {
            self.isContentInitialized = false
        }
    }
    didSet{
        self.configure()
    }
}

...
private func configure() {        
    if isContentInitialized {
        // here might be activity indicator stop
        ...
        if let writer = self.comment.author {
            self.authorLabel.text = writer.name
        }
    }
    else {
         // here might be activity indicator start

         dispatch_async(GlobalQueue, {
            self.commentAttributedString = NSAttributedString(
                                data: self.comment.htmlBody.dataUsingEncoding(NSUnicodeStringEncoding, allowLossyConversion: false)!,
                                options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
                                documentAttributes: nil,
                                error: nil)                        

            self.isContentInitialized = true

            // here might be spinner stop
            dispatch_async(MainQueue, {
                self.delegate?.ticketCommentCellDidRenderCommentHTML(self)
            })
        })
    }
}

... 
protocol TicketCommentCellDelegate {
    func ticketCommentCellDidRenderCommentHTML(cell: TicketCommentCell)
}


// TableViewDataSource

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier(kTicketCommentCellIdentifier, forIndexPath: indexPath) as! TicketCommentCell

    cell.indexPath = indexPath
    cell.delegate = self
    cell.comment = self.rows[indexPath.section][indexPath.row]

    return cell
}

// MARK: - TicketCommentCellDelegate

func ticketCommentCellDidRenderCommentHTML(cell: TicketCommentCell) {
    self.tableView.reloadRowsAtIndexPaths([cell.indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
}

// MARK: UITableViewDelegate

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {        
   var cell = self.commentCell

   cell.comment = self.rows[indexPath.section][indexPath.row]
   cell.setNeedsDisplay()
   cell.setNeedsLayout()

   let height = cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height + 1

    return height
}
Madman
  • 3,171
  • 2
  • 32
  • 44
  • how big is your content? – holex Aug 06 '15 at 10:07
  • Not big, simple email template, one sentence, signature about one paragraph with a few href links. About 1000 character – Madman Aug 06 '15 at 10:19
  • no other external resources are loaded from some other source, like _CSS_ or _JavaScript_? – holex Aug 06 '15 at 10:23
  • Thanks guys, solved. Updated with code sample – Madman Aug 06 '15 at 10:35
  • 1
    btw. Simulator parses HTML to NSAttributedString much slower than actual device, you can try yourself – ogres Aug 06 '15 at 10:36
  • If someone is still trying to figure this out please try https://github.com/malcommac/SwiftRichString library. It works like a charm – Sahil Sep 13 '19 at 10:42
  • I was flabbergasted when I realized this. Wasted 6 hours on this. My collection view cells just vanished for no apparent reason. In my case I was doing simple url decoding. Put decoding in global thread then assign the the value back to image loader in main thread. Searched Google how this even made my problem go away. Got this post. Its a real problem. @Arjan got some good points. But loading at launch doesn't really suit my use case. – Rishab Aug 27 '20 at 20:54
  • How are you setting `self.commentCell` inside `heightForRowAtIndexPath`? @Madman – Jalil Mar 14 '22 at 18:04

1 Answers1

17

ABout the slow parsing of the HTML into a string: The first time you create an attributed string of HTML, iOS creates all sorts of extra threads needed to parse the string, among which JavascriptCore engine.

Before parsing the first NSAttributedString from HTML:

Before

And immediately after:

enter image description here

So you can imagine it takes almost a second sometimes to start this all up. Subsequent calls are much faster. My workaround was to parse HTML in the application:didFinishingLaunchingWithOptions: function in the Appdelegate, so that I had all necessary frameworks in memory when needed (Objective-C):

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    NSMutableAttributedString *attrStringFromHtml = [[NSMutableAttributedString alloc]
                                                     initWithData: [@"<span>html enabled</span>" dataUsingEncoding:NSUnicodeStringEncoding
                                                                                allowLossyConversion:NO]
                                                     options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType}
                                                     documentAttributes:nil error:nil];
    NSLog(@"%@",[attrStringFromHtml string]);

    return YES;
}

Also see this answer.

Community
  • 1
  • 1
Arjan
  • 16,210
  • 5
  • 30
  • 40