Search code examples
iosswifttextview

How to get the range of Attributed text that can fit in a constant size textview


I'm trying to determine the amount of attributed text that can fit in the constant size textview.I have tried with the CTFrameSetter but I think that's only helpful when we already know the text that we want to add. So far I have tried this

 func numberOfCharactersThatFitTextView() -> Int {
        let fontRef = CTFontCreateWithName(font!.fontName as CFString, font!.pointSize, nil)
        let attributes = [kCTFontAttributeName : fontRef]
    let attributedString = NSAttributedString(string: text!, attributes: attributes as [NSAttributedString.Key : Any])
        let frameSetterRef = CTFramesetterCreateWithAttributedString(attributedString as CFAttributedString)

        var characterFitRange: CFRange = CFRange()

        CTFramesetterSuggestFrameSizeWithConstraints(frameSetterRef, CFRangeMake(0, 0), nil, CGSize(width: bounds.size.width, height: bounds.size.height), &characterFitRange)
        return Int(characterFitRange.length)

    }

Solution

  • Edit

    Sometimes, I overthink things...

    If all you want to do is get the max number of lines that will fit, you can use the font's .lineHeight property:

        // the height of your text view
        let h: CGFloat = 160.0
    
        // whatever your font is
        let font: UIFont = .systemFont(ofSize: 24.0)
        
        let maxLines: Int = Int(floor(h / font.lineHeight))
        
        print("Max Number of Lines:", maxLines)
        
    

    Original Answer

    If you want the number of lines that will fit for a given textView height, you can do it this way...

    First, a convenient extension:

    extension NSAttributedString {
        
        func height(containerWidth: CGFloat) -> CGFloat {
            
            let rect = self.boundingRect(with: CGSize.init(width: containerWidth, height: CGFloat.greatestFiniteMagnitude),
                                         options: [.usesLineFragmentOrigin, .usesFontLeading],
                                         context: nil)
            return ceil(rect.size.height)
        }
        
        func width(containerHeight: CGFloat) -> CGFloat {
            
            let rect = self.boundingRect(with: CGSize.init(width: CGFloat.greatestFiniteMagnitude, height: containerHeight),
                                         options: [.usesLineFragmentOrigin, .usesFontLeading],
                                         context: nil)
            return ceil(rect.size.width)
        }
        
    }
    

    Then, use this func:

    func numberOfLinesThatFit(inHeight height: CGFloat, withFont font: UIFont) -> Int {
    
        let attributes: [NSAttributedString.Key : Any] = [.font : font]
        
        var n: Int = 0
        
        var str: String = "A"
        
        var attStr: NSAttributedString = NSAttributedString(string: str, attributes: attributes)
        
        // width just needs to be greater than one character width
        var h: CGFloat = attStr.height(containerWidth: 200.0)
        
        while h < height {
            n += 1
            str += "\nA"
            attStr = NSAttributedString(string: str, attributes: attributes)
            h = attStr.height(containerWidth: 200.0)
        }
        
        return n
    
    }
    

    and call it like this:

        // whatever your font is
        let font: UIFont = .systemFont(ofSize: 24.0)
        
        // the height of your text view
        let h: CGFloat = 160.0
        
        let maxLines: Int = numberOfLinesThatFit(inHeight: h, withFont: font)
        
        print("max lines:", maxLines)