skip to Main Content

I want to have a UIView inside of a UITextView. For that I use the new NSTextAttachmentViewProvider class introduced in iOS 15. The views width should always be equal to the width of the UITextView this width should update when, for example, the screen rotates.

To do that I am using the tracksTextAttachmentViewBounds property inside of a NSTextAttachmentViewProvider subclass. If I understand correctly, if this property is set to true, the function attachmentBounds(for:location:textContainer:proposedLineFragment:position:) of my NSTextAttachmentViewProvider subclass should be used to determine the views bounds. In the code example below I have set it up in that manner, sadly the function is never called. (The storyboard consists of a UIViewController with a UITextView which four constrains (trailing, leading, bottom, top) are set equal to the safe area, nothing special going on). I have also tried to use a NSTextAttachment subclass in which I override the attachmentBounds(for:location:textContainer:proposedLineFragment:position:) function. It is also not called.
The view is appearing, but not with the width and height that I have set in the function (see screenshot below), maybe it is using some default values. When I start typing, the view disappears.

I don’t know what I am doing wrong.
Could somebody help me with that problem?

import UIKit

class SomeNSTextAttachmentViewProvider : NSTextAttachmentViewProvider {
    override func loadView() {
        super.loadView()
        tracksTextAttachmentViewBounds = true
        view = UIView()
        view!.backgroundColor = .purple
    }

    override func attachmentBounds(
        for attributes: [NSAttributedString.Key : Any],
        location: NSTextLocation,
        textContainer: NSTextContainer?,
        proposedLineFragment: CGRect,
        position: CGPoint
    ) -> CGRect {
        return CGRect(x: 0, y: 0, width: proposedLineFragment.width, height: 200)
    }
}

class ViewController: UIViewController {
    @IBOutlet var textView: UITextView?

    override func viewDidLoad() {
        super.viewDidLoad()

        NSTextAttachment.registerViewProviderClass(SomeNSTextAttachmentViewProvider.self, forFileType: "public.data")

        let mutableAttributedString = NSMutableAttributedString()
        mutableAttributedString.append(NSAttributedString("purple box: "))
        mutableAttributedString.append(
            NSAttributedString(
                attachment: NSTextAttachment(data: nil, ofType: "public.data")
            )
        )
        textView?.attributedText = mutableAttributedString
        textView?.font = UIFont.preferredFont(forTextStyle: .body)
    }
}

enter image description here

2

Answers


  1. Chosen as BEST ANSWER

    Works now in iOS 16. Also, tracksTextAttachmentViewBounds = true should be moved into an initialiser.


  2. override init(textAttachment: NSTextAttachment, parentView: UIView?, textLayoutManager: NSTextLayoutManager?, location: NSTextLocation) {
        super.init(textAttachment: textAttachment, parentView: parentView, textLayoutManager: textLayoutManager, location: location)
        tracksTextAttachmentViewBounds = true
    }
    

    Just move tracksTextAttachmentViewBounds setter into init as bryanboateng suggested.

    attachmentBounds(for:location:textContainer:proposedLineFragment:position:) is called before loadView gets called, so it makes sense to set variable before it is being used.

    This behavior is undocumented, hope Apple will add some details into documentation later.

    P.S. I’ve checked it in iOS 15.0 Simulator – the same code didn’t work. This solution works under iOS 16 both on iPhone and in Simulator.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search