11

Given is a (html) String with x-number of characters. The String will get formatted into an attributed String. Then displayed in a UILabel.

The UILabel has a height of >= 25 and <= 50 to limit the number of lines to 2.

Since the String has characters not visible in the formatted attributed String, like <b> / <i>, the best approach would be to limit the character count of the attributed String.

UILabel property .lineBreakMode = .byTruncatingTail causes words to get cut.

enter image description here

The goal, in case the character count would exceed the limit of space in the UILabel, is to cut between words. Desired maxCharacterCount = 50. Determine the last space before maxCharacterCount. Cut the String and append ... as last characters of the UILabel.

What's the best approach to limit the characters? Help is very appreciate.

David Seek
  • 16,783
  • 19
  • 105
  • 136
  • Have you even tried anything yourself? Or maybe I don't understand the question correctly. If you limit the size of a label the string will automatically be *truncated*. – LinusGeffarth Apr 12 '17 at 20:57
  • Let `UILabel` handle it by setting the `lineBreakMode` property on your label. See "Customizing the Label's Appearance" in [UILabel documentation](https://developer.apple.com/reference/uikit/uilabel) – Code Different Apr 12 '17 at 20:58
  • @LinusG. i sure know that, but the point, for example the string has 200 characters, to cut it at 47 and append 3x `.`, but maybe the `lineBreakMode` would do it.. checkin it out right now – David Seek Apr 12 '17 at 21:00
  • But why manually add the three dots if the label automatically does that for you? Or do you need the dots to not only visually but actually be a part of the string? – LinusGeffarth Apr 12 '17 at 21:04
  • See my edit. First, I didn't really knew about the automatic possibility, but the cut word is not perfectly my wanting. I would like to have it cut between words. so the last `"space"` before the desired number of character replaced by 3x `...` – David Seek Apr 12 '17 at 21:06
  • In your question you described a simple algorithm to get the effect you want. Why not use that? Just loop through the string and cut it off at a space that is less than your `maxCharacterCount`. – nathangitter Apr 15 '17 at 01:07

4 Answers4

12

Start with the full string and the known two-line height of the label and its known width, and keep cutting words off the end of the string until, at that width, the string's height is less than the label's height. Then cut one more word off the end for good measure, append the ellipsis, and put the resulting string into the label.

In that way, I got this:

enter image description here

Notice that the word after "time" never starts; we stop at a precise word-end with the inserted ellipsis. Here's how I did that:

    lab.numberOfLines = 2
    let s = "Little poltergeists make up the principle form of material " +
        "manifestation. Now is the time for all good men to come to the " +
        "aid of the country."
    let atts = [NSFontAttributeName: UIFont(name: "Georgia", size: 18)!]
    let arr = s.components(separatedBy: " ")
    for max in (1..<arr.count).reversed() {
        let s = arr[0..<max].joined(separator: " ")
        let attrib = NSMutableAttributedString(string: s, attributes: atts)
        let height = attrib.boundingRect(with: CGSize(width:lab.bounds.width, 
                                                      height:10000),
                                         options: [.usesLineFragmentOrigin],
                                         context: nil).height
        if height < lab.bounds.height {
            let s = arr[0..<max-1].joined(separator: " ") + "…"
            let attrib = NSMutableAttributedString(string: s, attributes: atts)
            lab.attributedText = attrib
            break
        }
    }

It is of course possible to be much more sophisticated about what constitutes a "word" and in the conditions for measurement, but the above demonstrates the general common technique for this sort of thing and should suffice to get you started.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • that's pretty amazing. thank you very much for your effort – David Seek Apr 15 '17 at 04:54
  • This way you lose the information about the original string. The appearance would be fine as long the UILabel does not change. If you rotate your device for example you will have the three dots in the wrong place. – Giuseppe Lanza Apr 19 '17 at 11:19
  • @GiuseppeLanza I completely agree that you'd have to do this again any time the label changes bounds. Basically the OP is asking to perform layout manually, in place of what the label and Text Kit already do. — You don't lose the original string if you keep it. My code is not "the final answer", it just demonstrates a simple technique for altering the string in the way the OP described. It isn't even the _right_ answer; the _real_ solution is to use a non-editable UITextView with a custom Text Kit stack and perform layout yourself there. – matt Apr 19 '17 at 15:32
  • But your answer, answers my question and solves my problem and therefore you have answered it, even tho you wouldn't call it the "right answer" :-) – David Seek Apr 21 '17 at 20:16
5

try this:

import UIKit

class ViewController: UIViewController {
var str = "Hello, playground"
var thisInt = 10
@IBOutlet weak var lbl: UILabel!
var lblWidth : CGFloat = 0.0

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
    lblWidth = lbl.bounds.width

    while str.characters.count <= thisInt - 3 {
        str.remove(at: str.index(before: str.endIndex))
        str.append("...")
    }
    lbl.text = str
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}


}

string width calculation link

Community
  • 1
  • 1
Mikael Weiss
  • 839
  • 2
  • 14
  • 25
  • 1
    That's a short and quick idea, but since the String has elements like ``, the final content of the label might appear way too short than the desired idea. – David Seek Apr 21 '17 at 20:12
2

Just for the record Matt's answer is correct, but you can remove any possible issues by creating your own subclass of UILabel which will store original value in variable. Than you can update its layout from view controller when the orientation was changed.

Community
  • 1
  • 1
Luzo
  • 1,336
  • 10
  • 15
  • I don't really get the advantage of your approach right now. Could you be a little bit more specific in what you consider "possible issues"? And what values you would store – David Seek Apr 21 '17 at 20:13
  • Look you are going to have one original value which you will store in the variable, for instance "Marry had a little lamb". Other part is setting the text which will be cut e.g. "Marry had...". By the possible issues I mean that you are not able to retrieve original text from default UILabel whereas you are with your custom label, because you stored the original value. That was all I wanted to say. You will not have to store it in your VC or wherever else, but your custom label will hold it for you. So there will not be any problem with rotation or resizing, because you can always recompute. – Luzo Apr 22 '17 at 08:06
0

Quick correction to Mikael Weiss 's answer.

if str.count > thisInt +3{
    while str.count >= thisInt {
        str.remove(at: str.index(before: str.endIndex))
    }
    str.append("...")
}
girish_vr
  • 3,041
  • 1
  • 24
  • 27