Number of rows in UILabel per cell to display size

EDIT 2 - I was able to get it to work using a different method found in another question ( stack overflow ), but I also combined it with the code provided by GeneratorOfOne and I posted my result below. However, I am still running into a problem with the ultimate goal of this. The goal is to have line numbers next to each line of text, sort of like in a code editor. The problem is that I need to store the row count, so if there are 3 rows in the first cell, the row numbers in the second cell must start with 4 and so on, but I don't think I can do that because the row count is calculated in the cell so I can't get the value back to the table view to set the text of the second label (row numbers) ... any ideas?

override func layoutSubviews() {
        super.layoutSubviews()
        calculateNumberOfLines()
    }

    var textDetail: String?  {
        didSet {
            gameInfo.text = textDetail
        }
    }

    func calculateNumberOfLines() {

        let layoutManager = NSLayoutManager()

        let textStorage = NSTextStorage(string: self.gameInfo!.text!)
        textStorage.addAttribute(NSFontAttributeName, value: self.gameInfo!.font, range: NSMakeRange(0, textStorage.length))

        let textContainer = NSTextContainer(size: CGSize(width:self.contentView.bounds.size.width - 56.0, height: CGFloat.max))
        layoutManager.addTextContainer(textContainer)
        layoutManager.textStorage = textStorage
        if let text = gameInfo?.text {

            let numberOfLines = getLinesArrayOfStringInLabel(gameInfo)
            lineNumbers.text = "\(startingNum)"
            for index in startingNum+1...startingNum+numberOfLines {

                lineNumbers.text = lineNumbers.text?.stringByAppendingString("\n\(index)")

            }

            endingNum = startingNum+numberOfLines
            //let numberOfLines = totalNumberOfLinesIn(text, currentGlyphIndex:0, currentLineNumber: 1, layoutManager: layoutManager, textContainer: textContainer)
            //lineNumbers.text = "\(numberOfLines)"

        } else { return }


    }

    func getLinesArrayOfStringInLabel(label: UILabel) -> Int {

        var text = label.text as! NSString
        var font = label.font
        var rect = label.frame

        var myFont = CTFontCreateWithName(font.fontName, font.pointSize, nil)
        var attrString = NSMutableAttributedString(string: text as String)
        attrString.addAttribute(String(kCTFontAttributeName), value: myFont, range: NSMakeRange(0, attrString.length))

        let frameSetter = CTFramesetterCreateWithAttributedString(attrString)

        var path = CGPathCreateMutable()
        CGPathAddRect(path, nil, CGRectMake(0,0,rect.size.width,100000))
        var frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil)

        var lines = CTFrameGetLines(frame) as NSArray
        var linesArray = NSMutableArray()

        for line in lines as [AnyObject] {

            var lineRef = line as! CTLineRef
            var lineRange = CTLineGetStringRange(lineRef)
            var range = NSMakeRange(lineRange.location, lineRange.length)

            var lineString = text.substringWithRange(range)
            linesArray.addObject(lineString)

        }

        return linesArray.count
    }

      

I have a custom UITableViewCell subclass that uses cells to self-calibrate. The cell calibration is perfect and the label expands correctly. However, at runtime, I am trying to calculate the number of lines the label will extend to (I need to display the line numbers next to the label) and I cannot get it. I tried the following two methods and it seems that it was not returning the correct number of rows that I see when I start the application:

func lineCountForText(string: String, label: UILabel) -> Int {

        let font: UIFont = UIFont(name: "SourceCodePro-Regular", size: label.font.pointSize)!

        let rect = string.boundingRectWithSize(CGSizeMake(label.frame.width, CGFloat(MAXFLOAT)), options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: [font : NSFontAttributeName], context: nil)

        return Int(ceil(rect.size.height/font.lineHeight))

    }

    func numberOfLinesForString(string: String, size: CGSize, font: UIFont) -> Int {
        let textStorage = NSTextStorage(string: string, attributes: [NSFontAttributeName: font])

        let textContainer = NSTextContainer(size: size)
        textContainer.lineBreakMode = .ByWordWrapping
        textContainer.maximumNumberOfLines = 0
        textContainer.lineFragmentPadding = 0

        let layoutManager = NSLayoutManager()
        layoutManager.textStorage = textStorage
        layoutManager.addTextContainer(textContainer)

        var numberOfLines = 0
        var index = 0
        var lineRange : NSRange = NSMakeRange(0, 0)
        for (; index < layoutManager.numberOfGlyphs; numberOfLines++) {
            layoutManager.lineFragmentRectForGlyphAtIndex(index, effectiveRange: &lineRange)
            index = NSMaxRange(lineRange)
        }

        return numberOfLines
    }

      

Can't I do this because this is a self-calibration cell?

EDIT. Here is the array of text I'm using:

var randomSizedTexts = [
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
        "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur?",
        "Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem",
        ", sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora",
        "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident",
        "Et harum quidem rerum facilis est et expedita distinctio",
        "Mauris id efficitur sapien. Nunc lobortis nisi ut ultricies scelerisque. Curabitur accumsan sit amet lacus in finibus. Aliquam dolor ante, rhoncus sit amet fermentum et, semper sit amet nisi. Proin pretium velit ut quam mollis fringilla. Nullam neque risus, vestibulum eget tortor sit amet, suscipit ultricies metus. In tortor ipsum, feugiat lacinia leo id, pulvinar lacinia velit. Suspendisse sit amet porta tellus, et scelerisque odio. Nam convallis sodales congue. Proin vel quam id arcu nisi non.",

    ]

      

+3


source to share


4 answers


Trying to use NSAttributedString

:

func lineCountForLabel(label: UILabel) -> Int {

    let font: UIFont = label.font

    let attribtedString = label.attributedText.mutableCopy() as! NSMutableAttributedString
    let paragraphStyle = NSMutableParagraphStyle()
    paragraphStyle.lineBreakMode = label.lineBreakMode
    attribtedString.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: NSRange(location: 0, length: attribtedString.length))

    let rect = attribtedString.boundingRectWithSize(CGSizeMake(label.frame.width, CGFloat(MAXFLOAT)),
        options: .UsesLineFragmentOrigin | .UsesFontLeading,
        context: nil)

    return Int(ceil(rect.size.height/font.lineHeight))
}

      

UPDATE:

You can write the type of the dictionary in shorthand as [Key: Value] , so your string is wrong:

let rect = string.boundingRectWithSize(xx, xx, xx, attributes: [font : NSFontAttributeName], context: nil)

      

changes:

[font : NSFontAttributeName]

      

in

[NSFontAttributeName : font]

      




UPDATE (after updating the question)

You have to store the line numbers in your data source like this:

class MyCell: UITableViewCell {

    @IBOutlet var contentLabel: UILabel!
    @IBOutlet private var numberLabel: UILabel!
}

class MasterViewController: UITableViewController {

    var objects = [AnyObject]()

    override func viewDidLoad() {
        super.viewDidLoad()

        let addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: "insertNewObject:")
        self.navigationItem.rightBarButtonItem = addButton

        self.tableView.estimatedRowHeight = 44
        self.tableView.rowHeight = UITableViewAutomaticDimension

        self.insertNewObject(self)
        self.insertNewObject(self)
        self.insertNewObject(self)
        self.insertNewObject(self)
        self.insertNewObject(self)
        self.insertNewObject(self)
        self.insertNewObject(self)
        self.insertNewObject(self)
        self.insertNewObject(self)
        self.insertNewObject(self)
    }

    func insertNewObject(sender: AnyObject) {
        var count = Int(arc4random_uniform(20)) + 1

        var string = ""
        while (count-- > 0) {
            string += "This is a test."
        }
        objects.insert(["content" : string, "lineNumber" : 0], atIndex: 0)
    }

    // MARK: - Table View

    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return objects.count
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! MyCell

        var object = objects[indexPath.row] as! [String : AnyObject]

        var startLineNumber = 0
        if indexPath.row > 0 {
            var pObject = objects[indexPath.row - 1] as! [String : AnyObject]
            startLineNumber = pObject["lineNumber"] as! Int
        }

        cell.contentView.setNeedsLayout()
        cell.contentView.layoutIfNeeded()

        cell.contentLabel.text = object["content"] as? String
        let lineNumber = lineCountForLabel(cell.contentLabel)  + startLineNumber
        cell.numberLabel.text = "\(lineNumber)"

        object["lineNumber"] = lineNumber
        objects[indexPath.row] = object

        return cell
    }

    func lineCountForLabel(label: UILabel) -> Int {

        let font: UIFont = label.font

        let attribtedString = label.attributedText.mutableCopy() as! NSMutableAttributedString
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.lineBreakMode = label.lineBreakMode
        attribtedString.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: NSRange(location: 0, length: attribtedString.length))

        let rect = attribtedString.boundingRectWithSize(CGSizeMake(label.frame.width, CGFloat(MAXFLOAT)),
            options: .UsesLineFragmentOrigin | .UsesFontLeading,
            context: nil)

        return Int(ceil(rect.size.height/font.lineHeight))
    }

}

      




UPDATE

There are several problems with your demo.

Edit

lastNumUsed = 1

      

to

lastNumUsed = 0

      

and change

for index in lastNumUsed+1...lastNumUsed+numLines

      

to

for index in lastNumUsed+1...lastNumUsed+numLines-1

      

Finally, the question is whether the size is gameInfo

correct or not. When cellForRow

supervisor does it, maybe zero, so the cell size is gameInfo

wrong. Instead, you should calculate the number of lines in the willDisplayCell

following way:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    var cell: LineNumberSubtitleCell! = tableView.dequeueReusableCellWithIdentifier("GameCell", forIndexPath: indexPath) as! LineNumberSubtitleCell

    if cell == nil {
        cell = LineNumberSubtitleCell(style: UITableViewCellStyle.Default, reuseIdentifier: "GameCell")
    }

    cell.gameInfo.text = randomSizedTexts[indexPath.row]

    return cell
}

override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
    let cell = cell as! LineNumberSubtitleCell

    cell.contentView.setNeedsLayout()
    cell.contentView.layoutIfNeeded()

    let numLines = getLinesArrayOfStringInLabel(cell.gameInfo)
    cell.lineNumbers.text = "\(lastNumUsed)"

    if !(lineNumbersList.count > indexPath.row) {

        for index in lastNumUsed+1...lastNumUsed+numLines-1 {

            cell.lineNumbers.text = cell.lineNumbers.text?.stringByAppendingString("\n\(index)")

            if index == lastNumUsed+numLines-1 {

                lastNumUsed = index+1
                lineNumbersList.addObject(cell.lineNumbers.text!)
            }
        }
    } else {
        cell.lineNumbers.text = lineNumbersList.objectAtIndex(indexPath.row) as? String
    }
}

      

The method call setNeedsLayout

causes your view to update its layout. The method call layoutIfNeeded

causes the layout system to start now. Therefore, the size gameInfo

must be recalculated to correct.

+3


source


You can use labelHeight / 12.0f for the number of lines that use 12.0f font size.



+1


source


Here's a simple example that counts the number of lines in text.

The method goes recursively to find the index of the glyph, and get the chunk of the string and compare if the character in the index is the last one.

class TableViewCell: UITableViewCell {

    var index: Int = 0

    var titleLabel: UILabel!
    var numberOfLinesLabel: UILabel!

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        titleLabel = UILabel(frame: CGRectZero)
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        titleLabel.numberOfLines = 0
        titleLabel.lineBreakMode = NSLineBreakMode.ByWordWrapping
        contentView.addSubview(titleLabel)

        numberOfLinesLabel = UILabel(frame: CGRectZero)
        numberOfLinesLabel.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(numberOfLinesLabel)

        contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-10-[titleLabel]-10-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["titleLabel": titleLabel]))
        contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-20-[titleLabel]-[numberOfLinesLabel(==28)]|", options: NSLayoutFormatOptions.AlignAllTop, metrics: nil, views: ["titleLabel": titleLabel, "numberOfLinesLabel": numberOfLinesLabel]))

    }

    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    var textDetail: String?  {
        didSet {
            titleLabel.text = textDetail
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        calculateNumberOfLines()

    }

    func calculateNumberOfLines() {

        let layoutManager = NSLayoutManager()

        let textStorage = NSTextStorage(string: self.titleLabel!.text!)
        textStorage.addAttribute(NSFontAttributeName, value: self.titleLabel!.font, range: NSMakeRange(0, textStorage.length))

        let textContainer = NSTextContainer(size: self.titleLabel!.bounds.size)

        layoutManager.addTextContainer(textContainer)
        layoutManager.textStorage = textStorage
        guard let text = titleLabel?.text else { return }

        let numberOfLines = totalNumberOfLinesIn(text: text, currentGlyphIndex:0, currentLineNumber: 1, layoutManager: layoutManager, textContainer: textContainer)
        numberOfLinesLabel.text = "\(numberOfLines)"
    }

    func totalNumberOfLinesIn(text text: String, currentGlyphIndex glyphIndex:Int, currentLineNumber line:Int, layoutManager: NSLayoutManager, textContainer: NSTextContainer) -> Int
    {
        let maxGlyphIndex = layoutManager.glyphRangeForTextContainer(textContainer)

        let lineFragment = layoutManager.lineFragmentRectForGlyphAtIndex(glyphIndex, effectiveRange: nil)
        let glyphRange = layoutManager.glyphRangeForBoundingRect(lineFragment, inTextContainer: textContainer)

        if NSMaxRange(glyphRange) < NSMaxRange(maxGlyphIndex) {
            return totalNumberOfLinesIn(text: text, currentGlyphIndex:glyphIndex + glyphRange.length,  currentLineNumber: (line + 1), layoutManager: layoutManager, textContainer: textContainer)
        }

        return line
    }
}


class TestViewController: UIViewController{

    static let CellIdentifier = "CellIdentifier"

    var randomSizedTexts = [
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
        "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur?",
        "Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem",
        ", sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora",
        "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident",
        "Et harum quidem rerum facilis est et expedita distinctio",
        "Mauris id efficitur sapien. Nunc lobortis nisi ut ultricies scelerisque. Curabitur accumsan sit amet lacus in finibus. Aliquam dolor ante, rhoncus sit amet fermentum et, semper sit amet nisi. Proin pretium velit ut quam mollis fringilla. Nullam neque risus, vestibulum eget tortor sit amet, suscipit ultricies metus. In tortor ipsum, feugiat lacinia leo id, pulvinar lacinia velit. Suspendisse sit amet porta tellus, et scelerisque odio. Nam convallis sodales congue. Proin vel quam id arcu nisi non.",
    ]

    lazy var tableView: UITableView! = {
        let tableView = UITableView(frame: CGRectZero, style: .Plain)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.delegate = self
        tableView.dataSource = self
        tableView.rowHeight = UITableViewAutomaticDimension
        tableView.estimatedRowHeight = 44
        self.view.addSubview(tableView)
        self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[tableView]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["tableView": tableView]))
        self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[tableView]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["tableView": tableView]))

        return tableView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = UIColor.whiteColor()
        tableView.registerClass(TableViewCell.self, forCellReuseIdentifier: TestViewController.CellIdentifier)
    }

}

extension TestViewController: UITableViewDataSource, UITableViewDelegate {

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier(TestViewController.CellIdentifier, forIndexPath: indexPath) as! TableViewCell
        cell.index = indexPath.row
        cell.textDetail = randomSizedTexts[indexPath.row]
        return cell
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return randomSizedTexts.count
    }
}

      

Update: It looks like the size of the NSTextContainer based on the size of the titleLabel doesn't fit. As the size of the titleLabel is currently not entirely appropriate. It seems that manually calculating the size for the borders available to display the label gives the correct result. See Changes.

+1


source


Implement dynamic height for all cells. Here's an example. In this example, the cell has a label.

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {

    cellHeight = 40.0f; // Height excluding label height

    // Calculate label height
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 288, 0)];
    label.font = [UIFont systemFontOfSize:13];
    label.text = [details objectAtIndex:indexPath.row];
    label.numberOfLines = 0;
    [label sizeToFit];
    cellHeight += [PSIUtility heightForLabel:label];

    label.text = [tips objectAtIndex:indexPath.row];
    label.numberOfLines = 0;
    [label sizeToFit];

    cellHeight += [self heightForLabel:label];

    //Return cell Height
    return cellHeight;

}

- (CGFloat)heightForLabel:(UILabel *)label{

    //Calculate the expected size based on the font and linebreak mode of your label
    // FLT_MAX here simply means no constraint in height
    label.numberOfLines = 0;

    CGSize maximumLabelSize = CGSizeMake(label.frame.size.width, FLT_MAX);

    //    CGSize expectedLabelSize = [label.text sizeWithFont:label.font constrainedToSize:maximumLabelSize lineBreakMode:label.lineBreakMode];


    CGRect expectedLabelSize = [label.text boundingRectWithSize:maximumLabelSize
                                                        options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
                                                     attributes:@{NSFontAttributeName:label.font}
                                                        context:nil];

    return expectedLabelSize.size.height;

}

      

0


source







All Articles