The Weird and Auto-Magical Science of Integrating TextKit with UIPageViewController and Auto Layout


Let's suppose you are using TextKit to layout your text, i.e. the combination of NSTextStorage, NSLayoutManager and NSTextContainer, combined with UITextView. And let's also suppose that you'll be using Auto Layout to anchor a UIPageViewController's view to the views it will be sitting next to (or perhaps be using it within a UIStackView, you brave soul!).

    func textLayoutContainers(size:CGSize, scrolling:Bool, section:String, page:Int) {
         var textViewArr = [UITextView]()

        // Create attr string - content
        guard let textString =  loadHTMLData(section: section, page: page) else {return}
        // Set up text storage and add string
        let textStorage = NSTextStorage(attributedString: textString)
        // Create a layout manager
        let textLayout  = NSLayoutManager()
        // Add layout manager to text storage object
        textStorage.addLayoutManager(textLayout)
        
        // create text containers and views, adding each container to the layout manager
        var i = 0
        let textInset:CGFloat = size.width/25
        let textSize = CGSize(width: size.width*0.6-textInset*2, height: size.height*0.7-textInset*2)
        
        var glyphRange:Int
        var numberOfGlyphs:Int
       
        repeat {
            // Create a text container
            let textContainer = NSTextContainer(size:textSize)
            // Add text container to text layout manager
            textLayout.addTextContainer(textContainer)
            // Instantiate UITextView object using the text container
            let textView:UITextView  = UITextView(frame:CGRect(x:0,y:0,  width:size.width,height:size.height), textContainer:textContainer)
            textView.isScrollEnabled = scrolling
            textView.textContainerInset.bottom = textInset
           
            textView.textContainerInset.left = textInset
            textView.textContainerInset.right = textInset
            // Give the container an identifier tag
            textView.tag = i
            glyphRange = NSMaxRange(textLayout.glyphRange(for: textContainer))
            numberOfGlyphs = textLayout.numberOfGlyphs
            
            textViewArr.append(textView)
            i+=1
        }
        while (glyphRange < numberOfGlyphs-1)
        
        
        textViews = textViewArr
        
    }
    
Now there are a few factors that are a bit of an oddity to me. First, we need to provide a CGSize for the NSTextContainer and a frame for the UITextViews (in the form of CGRect). But those values will be adjusted by Auto Layout, so count for very little (BUT cannot be omitted, and if you make containers too small you'll be slowing your app down creating them all). Second if you choose to leave scroll enabled on your UITextViews then the view will ignore any size requirements and display all text in a single UITextView for you to scroll (no matter the dimensions of the NSTextContainer you set).

If you disable scrolling for your text views then text will be set across pages, as expected. It will also automatically handle re-layout on size changes. But it does not handle how many pages are needed or displayed, so checks need to be put into place within the UIPageViewController DataSource and Delegate methods to make sure all the text you want to show is being shown and that you are not creating blank pages.

For now these are just a few notes on the auto-magical features that get added using the TextKit/UIPageViewController/Auto Layout combo, I'm planning to return to this post with some practical code snippets.

Comments