Episode #217

Text Attachments

18 minutes
Published on April 14, 2016

This video is only available to subscribers. Get access to this video and 572 others.

In this episode Sam shows us how to mix rich content such as images into a text view. Using Text attachments we can flow text around an image, select the image and delete or replace it, and more. Sam shows how to respond to layout changes and some advice on performance. Text Attachments are not for the faint of heart!

Episode Links

Setting up the Attachment Character


    override func viewDidLoad() {
        super.viewDidLoad()

        title = "Attachment"

        let attachmentCharacter = String(Character(UnicodeScalar(NSAttachmentCharacter)))
        let text = NSMutableAttributedString(string: "This is our image:\n\n\(attachmentCharacter)\n\nAfter the image.", attributes: [
            NSFontAttributeName: UIFont.systemFontOfSize(18)
        ])

        let attachment = NSTextAttachment()
        attachment.image = UIImage(named: "Photo")
        text.addAttribute(NSAttachmentAttributeName, value: attachment, range: NSRange(location: 20, length: 1))

        textView.attributedText = text
    }

Responding to layout changes

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        let textRange = NSRange(location: 0, length: textView.attributedText.length)
        textView.attributedText.enumerateAttribute(NSAttachmentAttributeName, inRange: textRange, options: []) { [weak self] value, range, _ in
            guard let attachment = value as? NSTextAttachment else { return }

            if let bounds = self?.boundsForAttachment(attachment), layoutManger = self?.textView.layoutManager {
                attachment.bounds = bounds

                let invalidRange = NSRange(location: range.location, length: textRange.length - range.location)
                layoutManger.invalidateLayoutForCharacterRange(invalidRange, actualCharacterRange: nil)
            }
        }
    }


    // MARK: - Private

    private func boundsForAttachment(attachment: NSTextAttachment) -> CGRect? {
        guard let image = attachment.image else { return nil }

        // Assuming landscape
        let width = textView.textContainer.size.width - (textView.textContainer.lineFragmentPadding * 2)
        return CGRect(x: 0, y: 0, width: width, height: image.size.height / image.size.width * width)
    }