Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSAttributedString not rendering on time - Swift

So, I'm having issues in a project where certain strings are not being rendered properly in some old iPhones with certain iOS versions (specifically it does not work for iPhone 5 with iOS10, while it does for iOS9.3, oddly enough). To reduce the issue, I wrote this code:

import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var label: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Long HTML, although we'll make it even larger to prove a point
        let string = "<h1>Thing 1</h1><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. </p><h1>Thing 2</h1><p>Curabitur sodales ligula in libero. Sed dignissim lacinia nunc. Curabitur tortor. Pellentesque nibh. Aenean quam. In scelerisque sem at dolor. Maecenas mattis. Sed convallis tristique sem. Proin ut ligula vel nunc egestas porttitor. Morbi lectus risus, iaculis vel, suscipit quis, luctus non, massa. Fusce ac turpis quis ligula lacinia aliquet. Mauris ipsum. Nulla metus metus, ullamcorper vel, tincidunt sed, euismod in, nibh. </p><h1>Thing 3</h1><p>Quisque volutpat condimentum velit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam nec ante. Sed lacinia, urna non tincidunt mattis, tortor neque adipiscing diam, a cursus ipsum ante quis turpis. Nulla facilisi. Ut fringilla. Suspendisse potenti. Nunc feugiat mi a tellus consequat imperdiet. Vestibulum sapien. Proin quam. Etiam ultrices. Suspendisse in justo eu magna luctus suscipit. </p><h1>Thing 4</h1><p>Sed lectus. Integer euismod lacus luctus magna. Quisque cursus, metus vitae pharetra auctor, sem massa mattis sem, at interdum magna augue eget diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Morbi lacinia molestie dui. Praesent blandit dolor. Sed non quam. In vel mi sit amet augue congue elementum. Morbi in ipsum sit amet pede facilisis laoreet. Donec lacus nunc, viverra nec, blandit vel, egestas et, augue. Vestibulum tincidunt malesuada tellus. Ut ultrices ultrices enim. Curabitur sit amet mauris. Morbi in dui quis est pulvinar ullamcorper. Nulla facilisi. </p><h1>Thing 5</h1><p>Integer lacinia sollicitudin massa. Cras metus. Sed aliquet risus a tortor. Integer id quam. Morbi mi. Quisque nisl felis, venenatis tristique, dignissim in, ultrices sit amet, augue. Proin sodales libero eget ante. Nulla quam. Aenean laoreet. Vestibulum nisi lectus, commodo ac, facilisis ac, ultricies eu, pede. Ut orci risus, accumsan porttitor, cursus quis, aliquet eget, justo. Sed pretium blandit orci. </p><h1>Thing 6</h1><p>Ut eu diam at pede suscipit sodales. Aenean lectus elit, fermentum non, convallis id, sagittis at, neque. Nullam mauris orci, aliquet et, iaculis et, viverra vitae, ligula. Nulla ut felis in purus aliquam imperdiet. Maecenas aliquet mollis lectus. Vivamus consectetuer risus et tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. </p>"

        let attrStr = try! NSAttributedString(
            data: (string + string + string + string + string + string + string + string).data(using: String.Encoding.unicode,allowLossyConversion: true)!,
            options: [ NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
            documentAttributes: nil)

        label.attributedText = attrStr
        label.backgroundColor = UIColor(red:0.00, green:1.00, blue:0.00, alpha:1.0)
    }
}

Simple enough. Of course the real issue is more complicated than this, but the idea is the same.

So, in an iPhone 7, all works fine:

enter image description here

However, when loading it on an iPhone 4s:

enter image description here

When debugging, attrStr seems to be properly set in both cases.

Any ideas? My theory is that older phones are too slow to render this on time, but I'm not quite sure how to workaround this.

Thanks!

EDIT: For those that asked, this is the Storyboard:

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11201" systemVersion="16A323" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="JzW-qG-LFG">
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11161"/>
        <capability name="Constraints to layout margins" minToolsVersion="6.0"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>
    <scenes>
        <!--View Controller-->
        <scene sceneID="9PR-d2-hNU">
            <objects>
                <viewController id="JzW-qG-LFG" customClass="ViewController" customModule="things" customModuleProvider="target" sceneMemberID="viewController">
                    <layoutGuides>
                        <viewControllerLayoutGuide type="top" id="VGp-YT-QQN"/>
                        <viewControllerLayoutGuide type="bottom" id="SeT-t7-CD0"/>
                    </layoutGuides>
                    <view key="view" contentMode="scaleToFill" id="gtg-CO-E9d">
                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                        <subviews>
                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Id1-A1-RaT">
                                <fontDescription key="fontDescription" type="system" pointSize="17"/>
                                <nil key="textColor"/>
                                <nil key="highlightedColor"/>
                            </label>
                        </subviews>
                        <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
                        <constraints>
                            <constraint firstAttribute="trailingMargin" secondItem="Id1-A1-RaT" secondAttribute="trailing" id="0qp-Rk-PC2"/>
                            <constraint firstItem="Id1-A1-RaT" firstAttribute="leading" secondItem="gtg-CO-E9d" secondAttribute="leadingMargin" id="3Vc-he-HqH"/>
                            <constraint firstItem="Id1-A1-RaT" firstAttribute="top" secondItem="VGp-YT-QQN" secondAttribute="bottom" id="7IE-fY-4H0"/>
                        </constraints>
                    </view>
                    <navigationItem key="navigationItem" id="Opz-px-hsl"/>
                    <connections>
                        <outlet property="label" destination="Id1-A1-RaT" id="TNv-G9-ASE"/>
                    </connections>
                </viewController>
                <placeholder placeholderIdentifier="IBFirstResponder" id="qlV-6d-ISJ" userLabel="First Responder" sceneMemberID="firstResponder"/>
            </objects>
            <point key="canvasLocation" x="2848.8000000000002" y="-280.20989505247377"/>
        </scene>
    </scenes>
</document>
like image 326
pyriku Avatar asked Nov 27 '16 18:11

pyriku


3 Answers

You are most definitely right. This is a time issue.

NSHTMLTextDocumentType of NSDocumentTypeDocumentAttribute is notorious for taking very long times. As you increase the attrStr string length (adding a couple of + string in data ) you'll be able to replicate the bug for higher end devises after a certain string length. I can reproduce this in an iPhone 6 plus emulator using iOS 10.1, since emulator works with less processing power than the actual devise.

If it's viable to you, you may try using DTCoreText to solve this issue. This library handles html string with methods other than the built in ones, reducing the rendering times. Which will make the bug less noticeable in lower end mobiles.

like image 36
Penkey Suresh Avatar answered Oct 13 '22 00:10

Penkey Suresh


The problem here is that UIView has an undocumented height limit of 8192 points. Once the label is 8192 points tall (or taller), it no longer updates the frame buffer. (Presumably the window server simply ignores the label's CALayer once the layer is too large.) Whatever was already in the frame buffer remains there. Presumably the behavior is undefined and may vary across devices and versions of iOS.

(Edit: Having thought about this more, I suspect the limit is 16,384 pixels, not 8,192 points. I don't think the window server deals in points.)

To demonstrate, I took your code and storyboard and added three things:

  1. I constrained the height of your label to be less than or equal to 8185 points.
  2. I added a slider that updates the constant of that height constraint. The slider allows the range 8185 to 8200.
  3. I added another label that displays the constant of the height constraint.

Here's what happens in the iPhone SE simulator running iOS 10.2:

demo of 8192 point height problems

In the demo, you can see that as soon as the height limit crosses 8192 points, the thumb of the slider starts to look smeared, and the height label's text visibly overwrites itself. This is because the green label is no longer updating the frame buffer, so whatever was previously there (as drawn by the slider and the height label) remains, only overwritten where the slider and the height label redraw themselves. As soon as the height goes back down across the 8192 point threshold, the green label draws itself again, cleaning up the mess.

I think you're not seeing this in your iPhone 7 test because the iPhone 7 has a wider screen than the iPhone SE. All iPhones with 3.5 inch and 4 inch screens are 320 points wide. iPhones with 4.7 inch screens are 375 points wide, and iPhones with 5.5 inch screens are 414 points wide.

A wider screen means a wider UILabel. That means more text fits on each line, so the label doesn't have to be as tall to fit all the text. I suspect that on the larger screens, your labels are shorter than 8192 points so they don't hit undefined behavior.

Workarounds:

  • If you don't intend to let the user scroll to see the entire text, just put a height constraint on the label.

  • If you want to let the user scroll, try using a UIWebView or WKWebView instead of a UILabel to show the text. I believe these views can handle content more than 8192 points tall.

Incidentally, you're not the first person to run into this problem: UILabel view disappear when the height greater than 8192. However, your question currently has an open bounty, which prevents it from being closed as a duplicate.

like image 58
rob mayoff Avatar answered Oct 12 '22 23:10

rob mayoff


You haven't provided any details about your Storyboard, or the settings or constraints for your label.

So I have initiated the label programmatically, and this solution works on the iPhone 4s running iOS 9.3:

enter image description here

Here is the working source code:

override func viewDidLoad() {
    super.viewDidLoad()

    label = UILabel(frame: CGRect(x: 0.0, y: 44.0, width: view.frame.size.width, height: view.frame.size.height - 88.0))
    label.numberOfLines = 0
    label.isUserInteractionEnabled = false
    label.contentMode = .left
    label.textAlignment = .natural
    label.lineBreakMode = .byTruncatingTail
    label.baselineAdjustment = .alignBaselines
    label.adjustsFontSizeToFitWidth = false
//  label.translatesAutoresizingMaskIntoConstraints = false //setting this causes the text to be layout wrong
    label.autoresizingMask = [UIViewAutoresizing.flexibleWidth, UIViewAutoresizing.flexibleHeight]

    view.addSubview(label)

    // Long HTML, although we'll make it even larger to prove a point
    let string = "<h1>Thing 1</h1><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. </p><h1>Thing 2</h1><p>Curabitur sodales ligula in libero. Sed dignissim lacinia nunc. Curabitur tortor. Pellentesque nibh. Aenean quam. In scelerisque sem at dolor. Maecenas mattis. Sed convallis tristique sem. Proin ut ligula vel nunc egestas porttitor. Morbi lectus risus, iaculis vel, suscipit quis, luctus non, massa. Fusce ac turpis quis ligula lacinia aliquet. Mauris ipsum. Nulla metus metus, ullamcorper vel, tincidunt sed, euismod in, nibh. </p><h1>Thing 3</h1><p>Quisque volutpat condimentum velit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam nec ante. Sed lacinia, urna non tincidunt mattis, tortor neque adipiscing diam, a cursus ipsum ante quis turpis. Nulla facilisi. Ut fringilla. Suspendisse potenti. Nunc feugiat mi a tellus consequat imperdiet. Vestibulum sapien. Proin quam. Etiam ultrices. Suspendisse in justo eu magna luctus suscipit. </p><h1>Thing 4</h1><p>Sed lectus. Integer euismod lacus luctus magna. Quisque cursus, metus vitae pharetra auctor, sem massa mattis sem, at interdum magna augue eget diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Morbi lacinia molestie dui. Praesent blandit dolor. Sed non quam. In vel mi sit amet augue congue elementum. Morbi in ipsum sit amet pede facilisis laoreet. Donec lacus nunc, viverra nec, blandit vel, egestas et, augue. Vestibulum tincidunt malesuada tellus. Ut ultrices ultrices enim. Curabitur sit amet mauris. Morbi in dui quis est pulvinar ullamcorper. Nulla facilisi. </p><h1>Thing 5</h1><p>Integer lacinia sollicitudin massa. Cras metus. Sed aliquet risus a tortor. Integer id quam. Morbi mi. Quisque nisl felis, venenatis tristique, dignissim in, ultrices sit amet, augue. Proin sodales libero eget ante. Nulla quam. Aenean laoreet. Vestibulum nisi lectus, commodo ac, facilisis ac, ultricies eu, pede. Ut orci risus, accumsan porttitor, cursus quis, aliquet eget, justo. Sed pretium blandit orci. </p><h1>Thing 6</h1><p>Ut eu diam at pede suscipit sodales. Aenean lectus elit, fermentum non, convallis id, sagittis at, neque. Nullam mauris orci, aliquet et, iaculis et, viverra vitae, ligula. Nulla ut felis in purus aliquam imperdiet. Maecenas aliquet mollis lectus. Vivamus consectetuer risus et tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. </p>"

    let attrStr = try! NSAttributedString(
        data: (string + string + string + string + string + string + string + string).data(using: String.Encoding.unicode,allowLossyConversion: true)!,
        options: [ NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
        documentAttributes: nil)

    label.attributedText = attrStr
    label.backgroundColor = UIColor(red:0.00, green:1.00, blue:0.00, alpha:1.0)
}

A complete working project may be downloaded from here.

like image 38
Jeshua Lacock Avatar answered Oct 13 '22 00:10

Jeshua Lacock