iosswiftuitextviewtextkit

Replacing layout manager in UITextView using TextKit 1


I have a UITextView created in storyboard. The view uses TextKit 1, and I'm trying to replace the NSLayoutManager. When calling .sizeThatFits() (or any other method requiring layout) on my text view, I'm getting an uncaught exception:

*** -[NSLayoutManager glyphRangeForTextContainer:]: given container does not appear in the list of containers for this NSLayoutManager.

A minimal reproducible example project.

I'm replacing the layout manager in my text view subclass:

let layoutManager = MyLayoutManager()
layoutManager.textStorage = self.textStorage        
layoutManager.addTextContainer(self.textContainer)      
self.textContainer.replaceLayoutManager(layoutManager)
// etc.

I've tried doing it in multiple places, in both initWithCoder and awakeFromNib, and made sure the layout manager actually does contain the NSTextContainer. The crash happens even when using plain NSLayoutManager, not a subclass.

When looking at the stack trace, I'm seeing calls to private TextKit 1 methods: enter image description here

Before entering .sizeThatFits(), the container is included in container array:

if (layoutManager.textContainers.contains(self.textContainer) {
    // true
}

I'm unable to tell from the actual assembly calls if the private methods really use my own layout manager, but glyphRangeForTextContainer: in my subclass is not called from these methods.

Could it be that somehow the private methods find a remnant of the old layout manager in memory, even if it shouldn't be retained by anything?

This also seems to happen in an empty project when using TextKit 1, so it might be a bug, but I want to rule out my user error first.


Solution

  • It turns out you need to override var layoutManager (or -(NSLayoutManager*)layoutManager in ObjC) in your text view. The getter does not return the layout manager associated with the text container, which is a bit weird.

    class TextView:UITextView {
        
        var customLayoutManager:NSLayoutManager = NSLayoutManager()
        
        required init?(coder: NSCoder) {
            super.init(coder: coder)
                    
            customLayoutManager.textStorage = self.textStorage
            customLayoutManager.addTextContainer(self.textContainer)
            
            self.textContainer.replaceLayoutManager(customLayoutManager)
        }
        
        override var layoutManager: NSLayoutManager {
            return customLayoutManager
        }
        
    }