如何優雅的作一個小說閱讀功能

目標

  1. 使用 TextKit 快速分頁
  2. 使用 UIPageViewController

支持平臺

iOS, iPadOS
也許還支持 Mac Calalyst ?bash

使用語言

Swiftapp

視圖結構

|- UIViewController // 根視圖, 可添加菜單顯示, 手勢操做等
    |- UIPageController // 章節視圖, 一頁對應一章
        | - UIPageController // 章節內容分頁視圖, 將單章內容進行分頁顯示
        |   | - UIViewController // 單頁顯示視圖, 對應單頁數據
        |   |   |- UITextView // 文字視圖
        |   |
        |   | - UIViewController
        |   |   |- UITextView
        |   |
        |   | ...
        |
        | - UIPageController
        |   | - UIViewController
        |   |   |- UITextView
        |   |
        |   | - UIViewController
        |   |   |- UITextView
        |   |
        |   | ...
        |
        | ...
複製代碼

章節內容分頁視圖中, 只要在返回單頁顯示視圖的代理中返回 nil, 便可實現章節內容翻到最後一頁時, 繼續翻頁翻到下一章節的邏輯ide

分頁實現

首先, 必定要先肯定好 TextView 的大小與內容間距, 即文字顯示區域的大小, 這將嚴重影響到分頁後的數據能不能正常顯示佈局

其次, 首行縮進最好用空格代替, 而不是用 NSParagraphStylefirstLineHeadIndent 屬性來實現, 不然會出現某段落從中間被分開, 下一頁依然被縮進的狀況性能

首行縮進的空格數量可用如下邏輯計算:測試

let normalWidth = "你好".size(font: textFont).width // 請根據內容語言改變文字
let speaceWidth = " ".size(font: textFont).width // 一個空格的寬
let speaceCount = Int(normalWidth / speaceWidth)
let speace = String(repeating: " ", count: speaceCount)
複製代碼

而後在每段前添加空格字體

let result = content.string.components(separatedBy: "\n").map { "\(speace)\($0)" }
複製代碼

這樣就能夠在每段首行添加一個合適的縮進了ui

接下來就是重點的分頁了spa


第一步, 前期參數準備:線程

  1. 準備好處理完成的 NSAttributedString, 最好包含各類字體, 顏色, 格式等設置信息, 避免分頁視圖拿到數據後再次生成 NSAttributedString , 重複設置內容樣式致使的分頁不許的狀況

  2. 準備好文字顯示區域大小的參數


第二步, 開始分頁:
準備數據:

// 建立 NSLayoutManager, 全部的分頁邏輯開端
let layoutManager = NSLayoutManager()

// 若是沒有給特定部分文字區域設置單獨的佈局, 可設置此項爲 false, 以提升性能
layoutManager.allowsNonContiguousLayout = false

// 使用以前準備好的 NSAttributedString 進行初始化 NSTextStorage
let textStorage = NSTextStorage(attributedString: string)
textStorage.addLayoutManager(layoutManager)

// 設定文字顯示區域參數
let viewSize: CGSize = CGSize(width: textAreaWidth, height:  textAreaHeight)

// 設定 textView 的內間距
let textInsets = UIEdgeInsets.zero
let textViewFrame = CGRect(x: 0, y: 0, width: viewSize.width, height: viewSize.height)

// 開始分頁
var glyphRange: Int = 0
var numberOfGlyphs: Int = 0
複製代碼

分頁循環:

var ranges: [NSRange] = []
repeat {
    let textContainer = NSTextContainer(size: viewSize)
    layoutManager.addTextContainer(textContainer)
    
    // 不斷建立 textView 讓 NSLayoutManager 進行內容分頁
    let textView = UITextView(frame: textViewFrame, textContainer: textContainer)
    textView.isEditable = false
    textView.isSelectable = false
    textView.textContainerInset = textInsets
    textView.showsVerticalScrollIndicator = false
    textView.showsHorizontalScrollIndicator = false
    textView.isScrollEnabled = false // 禁止滑動, 不然計算結果將再也不準確
    textView.bounces = false
    textView.bouncesZoom = false
    
    // 獲取當前分頁內容所在位置
    let range = layoutManager.glyphRange(for: textContainer)
    ranges.append(range)
    
    // 斷定是否分頁完成
    glyphRange = NSMaxRange(range)
    numberOfGlyphs = layoutManager.numberOfGlyphs
} while glyphRange < numberOfGlyphs - 1
複製代碼

至此, 就獲得了帶有格式的全文 NSAttributedString, 和分頁區域的 ranges


第三步, 顯示分頁數據 章節內容分頁視圖中, 將單章的 NSAttributedString 和分到的 range 分配給每個單頁顯示視圖, 在 UITextView 中直接設置 attributedTextattributedString.attributedSubstring(from: range)

UITextView 的設置務必於分頁循環時的 UITextView 保持一致

基本原理

NSLayoutManager 會根據加入的 NSTextContainer 不斷分走文字, 直到分完爲止, 這時候可使用 layoutManager.glyphRange(for: textContainer) 獲取 NSTextContainer 對應的文字範圍 range, 以後就能夠根據這個 range 進行文字分割

修改字色, 字體

改變字色

改變顏色不須要從新盡心分頁操做, 直接操做 UITextViewattributedText 和原始 NSAttributedString 就行

let attributed = NSMutableAttributedString(attributedString: textView.attributedText!)
attributed.addAttribute(.foregroundColor, value: ChangeColor, range: .init(location: 0, length: attributed.length))
textView.attributedText = NSAttributedString(attributedString: attributed)
複製代碼

注意, 方法爲 addAttribute, 而不是 setAttribute, 後者會致使其餘信息被清空

改變字體

UITextViewattributedText 和原始 NSAttributedStringfont 設置爲新字體, 再從新進行分頁操做, 從新設置單頁顯示視圖便可

注意事項與其餘

UITextView 內間距

請經過 textContainerInset 設置間距, 與分頁時的參數保持一致, 單獨設置 contentInset 不保證顯示正確

添加點擊區域

直接在根視圖添加點擊手勢, 設置代理後, 根據點擊區域判斷行爲 這樣能夠避免 UIPageViewController 的翻頁手勢被遮擋

在 UIPageViewController 中添加 UISlider 等帶有活動操做的視圖

請自主作好手勢衝突的處理, 否則就是一片亂

分頁性能

因爲分頁流程主要在主線程上, 因此被分頁的數據最好不要過大, 單章單章分頁就剛恰好

分頁後文字可能超出顯示區域

每一個 NSTextContainer 的 frame 值都是被 NSLayoutManager 粗略計算過的, 與你設置 NSTextContainer 的 size 值略有出入, 有時候大些, 有時候小些, 但偏差絕度不會超過一個字符的高度. 因此, 蘋果建議咱們在設置 UITextView 的時候, 給這個 NSTextContainer 預留必定的高度......

還有字體問題, 由於系統有些字體對中文支持不太好, 可能會對文字的大小計算失誤, 請儘可能使用如下支持中文的字體, 或其餘支持中文的自定義字體:

Heiti SC              黑體-簡
Heiti TC              黑體-繁
PingFang TC           平方-簡
PingFang HK           平方-繁
PingFang SC           平方-繁
複製代碼

快速翻頁致使未分頁完成就翻到下一章

能夠添加分頁中標記, 存在標識時, 下一頁上一頁代理中返回 nil

具體判斷邏輯請根據自身項目調整

爲什麼不直接使用分頁循環中的 UITextView

能夠嘗試一下, 內存的飆升絕對酸爽, 我在模擬器上測試, 翻了幾頁直接飆到 150+ M, 目前的方案在模擬器上 App 總體內存佔用最高穩定在 50 M 左右, 真機能夠穩定在 20 M 左右

固然, 也有多是個人方式有錯誤, 各位能夠嘗試各類方案, 但分頁邏輯萬變不離其宗

相關文章
相關標籤/搜索