The target

  1. useTextKitQuick page
  2. useUIPageViewController

Support platform

IOS, iPadOS maybe even Mac Calalyst?

Use the language

Swift

View structure

| - UIViewController / / root view, you can add menu display, gesture, etc. | - UIPageController / / section view, a corresponding chapter | - UIPageController / / chapters content page view, Will be ChanZhang content paging display | | - UIViewController / / single page shows the view, Corresponds to a single page data | | | - UITextView / / literal view | | | | - UIViewController | | | - UITextView | | | |... | | - UIPageController | | - UIViewController | | |- UITextView | | | | - UIViewController | | |- UITextView | | | | ...  | |...Copy the code

In a paginated view of chapter content, you just return nil in the broker that returns the single-page display view, and you can implement the logic of chapter content turning to the last page and continuing to turn the page to the next chapter

Paging implementation

First of all, we must first determine the size of TextView and content spacing, that is, the size of text display area, which will seriously affect the normal display of data after pagination

Second, it is better to indent the first line with a space instead of using the firstLineHeadIndent property of NSParagraphStyle, otherwise one paragraph will be separated and the next page will still be indent

The number of Spaces to indent the first line can be calculated using the following logic:

let normalWidth = "Hello".size(font: textFont).width // Please change the text according to the content languagelet speaceWidth = "".size(font: textFont).width // The width of a spacelet speaceCount = Int(normalWidth / speaceWidth)
let speace = String(repeating: "", count: speaceCount)
Copy the code

Then add a space before each paragraph

let result = content.string.components(separatedBy: "\n").map { "\(speace)\($0)" }
Copy the code

This will allow you to add an appropriate indentation to the first line of each paragraph

Next comes the important pagination


Step 1: Preparation of preliminary parameters:

  1. Prepare an NSAttributedString for processing, preferably with a variety of font, color, and format Settings, so that the paged view doesn’t generate an NSAttributedString after it gets the data and sets the content style repeatedly

  2. Prepare parameters for the size of the text display area


Step 2, start paging: Prepare data:

// Create NSLayoutManager, the start of all paging logicletLayoutManager = NSLayoutManager() // If there is no separate layout for a particular part of the text region, set this tofalseIn order to improve performance layoutManager. AllowsNonContiguousLayout =false// Initialize the NSTextStorage using the prepared NSAttributedStringletTextStorage = NSTextStorage (attributedString: string) textStorage. AddLayoutManager (layoutManager) / / set up the parameter text display arealetViewSize: CGSize = CGSize(width: textAreaWidth, height: textAreaHeight) // Set the spacing of textViewlet textInsets = UIEdgeInsets.zero
letTextViewFrame = CGRect(x: 0, y: 0, width: viewsize.width, height: viewsize.height) Int = 0 var numberOfGlyphs: Int = 0Copy the code

Paging loops:

var ranges: [NSRange] = []
repeat {
    lettextContainer = NSTextContainer(size: ViewSize) layoutManager. AddTextContainer (textContainer) / / continuously create textView let NSLayoutManager content pageslet textView = UITextView(frame: textViewFrame, textContainer: textContainer)
    textView.isEditable = false
    textView.isSelectable = false
    textView.textContainerInset = textInsets
    textView.showsVerticalScrollIndicator = false
    textView.showsHorizontalScrollIndicator = false
    textView.isScrollEnabled = falseTextviet.bounces = // Disable sliding, otherwise the calculation will not be accuratefalse
    textView.bouncesZoom = false// Get the current page content locationlet range = layoutManager.glyphRange(for: TextContainer) ranges. Append (range) // Determine whether paging is complete glyphRange = NSMaxRange(range) numberOfGlyphs = layoutManager.numberOfGlyphs }while glyphRange < numberOfGlyphs - 1
Copy the code

CoreText version:

var ranges: [NSRange] = []
let framesetter = CTFramesetterCreateWithAttributedString(string)
var textPosition = 0
while textPosition < string.length {
    let path = CGPath(rect: textViewFrame.inset(by: textInsets), transform: nil)
    let frame = CTFramesetterCreateFrame(framesetter, .init(location: textPosition, length: 0), path, nil)
    let stringRange = CTFrameGetVisibleStringRange(frame)
    
    let range: NSRange = .init(location: stringRange.location, length: stringRange.length)
    textPosition += stringRange.length
    ranges.append(range)
}
Copy the code

At this point, you have the full text NSAttributedString with formatting, and the ranges for the paging area


In the paging view, assign the NSAttributedString and range assigned to a single chapter to each single page view, In UITextView directly set attributedText to attributedString. AttributedSubstring (from: range)

The UITextView Settings must be the same as the UITextView Settings for paging loops

The basic principle of

NSLayoutManager separates the text from the added NSTextContainer until it runs out, using LayoutManager.glyphrange (for: TextContainer) gets the range of text corresponding to NSTextContainer, and then the text can be divided according to this range

Modify the word color, font

Change the words color

You don’t have to go back to paging to change the color, you just manipulate UITextView’s attributedText and the original NSAttributedString

let attributed = NSMutableAttributedString(attributedString: textView.attributedText!)
attributed.addAttribute(.foregroundColor, value: ChangeColor, range: .init(location: 0, length: attributed.length))
textView.attributedText = NSAttributedString(attributedString: attributed)
Copy the code

Note that the method is addAttribute, not setAttribute, which would cause other information to be emptied

Change the font

Set the font of UITextView attributedText and original NSAttributedString to the new font, and then pagination again, and reset the single page display view

Notes and others

UITextViewThe distance between

Use textContainerInset to set the spacing the same as when paging. Setting contentInset separately does not guarantee correct display

Add click area

Add the click gesture directly to the root view, set the proxy, and determine the behavior based on the click region so that the UIPageViewController’s page-turning gesture is not blocked

Add views such as UISlider with active actions to UIPageViewController

Please feel free to handle the conflict, or it will be a mess

Paging performance

Since the paging process is mainly on the main thread, it is best not to have too much data paged, and single chapter paging is just fine

Text may be out of display area after pagination

The frame value of each NSTextContainer is roughly calculated by the NSLayoutManager, which is slightly different from the size you set for the NSTextContainer. Sometimes it’s bigger, sometimes it’s smaller, But the error should never be more than a character high. Therefore, Apple recommends that we reserve a certain height for the NSTextContainer…… when setting up the UITextView

There is also the font problem, because some fonts in the system do not support Chinese well, and the size of the text may be miscalculated. Please try to use the following fonts that support Chinese, or other customized fonts that support Chinese:

PingFang TC square - PingFang HK square - PingFang SC square - PingFang SC square - PingFangCopy the code

Quick page flipping causes the page to turn to the next chapter before the page is complete

You can add tags in a page and return nil in the next page or previous page agent if there is an identifier

The specific judgment logic can be adjusted according to your project

Why not just use UITextView in a paging loop

I tested it on the simulator, and it went up to 150+ M after a few pages. The current scheme can stabilize the overall memory usage of App at about 50 M on the simulator, while that of the real computer at about 20 M

Of course, it could be that I’m doing it the wrong way, but you can try all kinds of schemes, but the paging logic is always the same