Search code examples
iosswiftios-pdfkitnsmutableparagraphstyle

How to get the height of a paragraph using PDFKit


I am writing a pdf using iOS PDFKit. Typically I can get the height of a single text item such as a title by doing the following:

return titleStringRect.origin.y + titleStringRect.size.height

Where titleStringRect is the CGRect containing the string. The returned value is the y-coordinate for the bottom of that text so that I know where to start writing the next line of text. I have not found a way to know where a paragraph ends. The solutions I have found have been to just make a big enough CGRect that the paragraph will definitely fit in. I need to know exactly what the height of the CGRect should be based on the String that will be written into it. Here is my code:

    func addParagraph(pageRect: CGRect, textTop: CGFloat, text: String) {

        let textFont = UIFont(name: "Helvetica", size: 12)
        let backupFont = UIFont.systemFont(ofSize: 12, weight: .regular)

        // Set paragraph information. (wraps at word breaks)
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.alignment = .natural
        paragraphStyle.lineBreakMode = .byWordWrapping

        // Set the text attributes
        let textAttributes = [
            NSAttributedString.Key.paragraphStyle: paragraphStyle,
            NSAttributedString.Key.font: textFont ?? backupFont
        ]

        let attributedText = NSAttributedString(
            string: text,
            attributes: textAttributes
        )

        let textRect = CGRect(
            x: 50.0,
            y: textTop,
            width: pageRect.width - 100,
            height: pageRect.height - textTop - pageRect.height / 5.0
        )
        attributedText.draw(in: textRect)
    }

As you can see the above code just makes a CGRect that is 1/5th of the space below the previous text regardless of how many lines the paragraph will actually be. I have tried averaging the character count per line in order to estimate how many lines the paragraph will be but this is unreliable and definitely a hack. What I need is for the addParagraph function to return the y-coordinate for the bottom of the paragraph so that I know where to start writing the next piece of content.


Solution

  • I ended up finding the solution to this and it is pretty simple. I'll post the code and then explain it for anyone else who has this problem.

    let paragraphSize = CGSize(width: pageRect.width - 100, height: pageRect.height)
    let paragraphRect = attributedText.boundingRect(with: paragraphSize, options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil)
    

    First define a CGSize that is a certain width and height. Set the width to the width you want the paragraph to be and set the height to a large value that will fit the content. Then call

    attributedText.boundingRect(with: paragraphSize, options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil)

    Where attributedText is the paragraph content. The boundingRect method returns a CGRect which is the size required to fit the content into, but no more. Now you can return the bottom of the paragraph. This method will not change the width unless it cannot fit the String into the height you provided. For my purpose this was perfect. Here is the full code:

     func addParagraph(pageRect: CGRect, textTop: CGFloat, paragraphText: String) -> CGFloat {
    
            let textFont = UIFont(name: "Helvetica", size: 12)
            let backupFont = UIFont.systemFont(ofSize: 12, weight: .regular)
    
            // Set paragraph information. (wraps at word breaks)
            let paragraphStyle = NSMutableParagraphStyle()
            paragraphStyle.alignment = .natural
            paragraphStyle.lineBreakMode = .byWordWrapping
    
            // Set the text attributes
            let textAttributes = [
                NSAttributedString.Key.paragraphStyle: paragraphStyle,
                NSAttributedString.Key.font: textFont ?? backupFont
            ]
    
            let attributedText = NSAttributedString(
                string: paragraphText,
                attributes: textAttributes
            )
    
            // determine the size of CGRect needed for the string that was given by caller
            let paragraphSize = CGSize(width: pageRect.width - 100, height: pageRect.height)
            let paragraphRect = attributedText.boundingRect(with: paragraphSize, options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil)
    
            // Create a CGRect that is the same size as paragraphRect but positioned on the pdf where we want to draw the paragraph
            let positionedParagraphRect = CGRect(
                x: 50,
                y: textTop,
                width: paragraphRect.width,
                height: paragraphRect.height
            )
    
            // draw the paragraph into that CGRect
            attributedText.draw(in: positionedParagraphRect)
    
            // return the bottom of the paragraph
            return positionedParagraphRect.origin.y + positionedParagraphRect.size.height
        }