BSText - YY大神的富文字框架 YYText 的 Swift 版本

Bruce_Liu發表於2019-03-04

鼠奎特

原文連結

前言

  度過春節期間的安逸期,需要從慵懶的狀態儘快恢復過來,節前有幾個月時間,趁著公司業務線不怎麼繁忙,抱著學習的態度,嘗試將 YY 大神的 YYText 用 Swift 重新實現一下(之前用 Swift 最多寫寫 Demo,沒有用來做專案)。但是由於年前還有個 bug 沒有修復,怕大家的 issue 雪花般飛過來,果斷沒敢開源出來。新年新氣象,改完 bug 趕緊和大家分享一下啦 ?

  目前專案已經實現了 YYText 中的所有功能,如果大家遇到什麼問題,歡迎提 issue,或者郵箱聯絡 a1049145827@hotmail.com ,如果覺得有用,請不要吝惜您的 star ✨。

  用過 YYText 的同學應該已經相當熟悉了,傳送門

一些挑戰

  由於近年來 Swift 發展迅速,ABI 一直不能穩定下來,導致開發者們都在吐槽:“每年學習一門新語言”,這樣就直接導致在網上查資料非常困難,好不容易查到的資料,demo 程式碼甚至都不能通過編譯,這樣推進的效率大打折扣,確實非常痛苦,眼看 Swift 就要釋出 Swift5,心裡似乎又有了希望。於是決心要用 Swift 把 YYText 的功能實現一遍,一來練習 Swift 語法,二來以後也可以持續維護,希望好的輪子可以被更多的開發者認可和採用,目前本專案已經可以在 Swift5 (Xcode10 beta3)環境下正常編譯執行。

專案介紹

功能強大的 iOS 富文字編輯與顯示框架。
(該專案是 YYText 的 Swift 版本,專案的字首 'BS' 來自於 BlueSky,就是創作了《冰河世紀》系列電影的 BlueSky 工作室)

特性

  • API 相容 UILabel 和 UITextView
  • 支援高效能的非同步排版和渲染
  • 擴充套件了 CoreText 的屬性以支援更多文字效果
  • 支援 UIImage、UIView、CALayer 作為圖文混排元素
  • 支援新增自定義樣式的、可點選的文字高亮範圍
  • 支援自定義文字解析 (內建簡單的 Markdown/表情解析)
  • 支援文字容器路徑、內部留空路徑的控制
  • 支援文字豎排版,可用於編輯和顯示中日韓文字
  • 支援圖片和富文字的複製貼上
  • 文字編輯時,支援富文字佔位符
  • 支援自定義鍵盤檢視
  • 撤銷和重做次數的控制
  • 富文字的序列化與反序列化支援
  • 支援多語言,支援 VoiceOver
  • 全部程式碼都有文件註釋

架構

本專案架構與 YYText 保持一致

文字屬性

BSText 原生支援的屬性

DemoAttribute NameClass
BSText - YY大神的富文字框架 YYText 的 Swift 版本TextAttachmentTextAttachment
BSText - YY大神的富文字框架 YYText 的 Swift 版本TextHighlightTextHighlight
BSText - YY大神的富文字框架 YYText 的 Swift 版本TextBindingTextBinding
BSText - YY大神的富文字框架 YYText 的 Swift 版本TextShadow
TextInnerShadow
TextShadow
BSText - YY大神的富文字框架 YYText 的 Swift 版本TextBorderTextBorder
BSText - YY大神的富文字框架 YYText 的 Swift 版本TextBackgroundBorderTextBorder
BSText - YY大神的富文字框架 YYText 的 Swift 版本TextBlockBorderTextBorder
BSText - YY大神的富文字框架 YYText 的 Swift 版本TextGlyphTransform NSValue(CGAffineTransform)
BSText - YY大神的富文字框架 YYText 的 Swift 版本TextUnderlineTextDecoration
BSText - YY大神的富文字框架 YYText 的 Swift 版本TextStrickthroughTextDecoration
BSText - YY大神的富文字框架 YYText 的 Swift 版本TextBackedStringTextBackedString

BSText 支援的 CoreText 屬性

DemoAttribute NameClass
BSText - YY大神的富文字框架 YYText 的 Swift 版本 Font UIFont(CTFontRef)
BSText - YY大神的富文字框架 YYText 的 Swift 版本 Kern NSNumber
BSText - YY大神的富文字框架 YYText 的 Swift 版本 StrokeWidth NSNumber
BSText - YY大神的富文字框架 YYText 的 Swift 版本 StrokeColor CGColorRef
BSText - YY大神的富文字框架 YYText 的 Swift 版本 Shadow NSShadow
BSText - YY大神的富文字框架 YYText 的 Swift 版本 Ligature NSNumber
BSText - YY大神的富文字框架 YYText 的 Swift 版本 VerticalGlyphForm NSNumber(BOOL)
BSText - YY大神的富文字框架 YYText 的 Swift 版本 WritingDirection NSArray(NSNumber)
BSText - YY大神的富文字框架 YYText 的 Swift 版本 RunDelegate CTRunDelegateRef
BSText - YY大神的富文字框架 YYText 的 Swift 版本 TextAlignment NSParagraphStyle
(NSTextAlignment)
BSText - YY大神的富文字框架 YYText 的 Swift 版本 LineBreakMode NSParagraphStyle
(NSLineBreakMode)
BSText - YY大神的富文字框架 YYText 的 Swift 版本 LineSpacing NSParagraphStyle
(CGFloat)
BSText - YY大神的富文字框架 YYText 的 Swift 版本 ParagraphSpacing
ParagraphSpacingBefore
NSParagraphStyle
(CGFloat)
BSText - YY大神的富文字框架 YYText 的 Swift 版本 FirstLineHeadIndent NSParagraphStyle
(CGFloat)
BSText - YY大神的富文字框架 YYText 的 Swift 版本 HeadIndent NSParagraphStyle
(CGFloat)
BSText - YY大神的富文字框架 YYText 的 Swift 版本 TailIndent NSParagraphStyle
(CGFloat)
BSText - YY大神的富文字框架 YYText 的 Swift 版本 MinimumLineHeight NSParagraphStyle
(CGFloat)
BSText - YY大神的富文字框架 YYText 的 Swift 版本 MaximumLineHeight NSParagraphStyle
(CGFloat)
BSText - YY大神的富文字框架 YYText 的 Swift 版本 LineHeightMultiple NSParagraphStyle
(CGFloat)
BSText - YY大神的富文字框架 YYText 的 Swift 版本 BaseWritingDirection NSParagraphStyle
(NSWritingDirection)
BSText - YY大神的富文字框架 YYText 的 Swift 版本 DefaultTabInterval
TabStops
NSParagraphStyle
CGFloat/NSArray(NSTextTab)

用法

基本用法

// BSLabel (和 UILabel 用法一致)
let label = BSLabel()
label.frame = ...
label.font = ...
label.textColor = ...
label.textAlignment = ...
label.lineBreakMode = ...
label.numberOfLines = ...
label.text = ...
    
// BSTextView (和 UITextView 用法一致)
let textView = BSTextView()
textView.frame = ...
textView.font = ...
textView.textColor = ...
textView.dataDetectorTypes = ...
textView.placeHolderText = ...
textView.placeHolderTextColor = ...
textView.delegate = ...
複製程式碼

屬性文字

// 1. 建立一個屬性文字
let text = NSMutableAttributedString(string: "Some Text, blabla...")
    
// 2. 為文字設定屬性
text.bs_font = UIFont.boldSystemFont(ofSize:30)
text.bs_color = UIColor.blue
text.bs_set(color: UIColor.red, range: NSRange(location: 0, length: 4))
text.bs_lineSpacing = 10
    
// 3. 賦值到 BSLabel 或 BSTextView
let label = BSLabel()
label.frame = CGRect(x: 15, y: 100, width: 200, height: 80)
label.attributedText = text
    
let textView = BSTextView()
textView.frame = CGRect(x: 15, y: 200, width: 200, height: 80)
textView.attributedText = text
複製程式碼

文字高亮

你可以用一些已經封裝好的簡便方法來設定文字高亮:

text.bs_set(textHighlightRange: range,
            color: UIColor.blue,
            backgroundColor: UIColor.gray) { (view, text, range, rect) in
    print("tap text range:...")
}
複製程式碼

或者用更復雜的辦法來調節文字高亮的細節:

// 1. 建立一個"高亮"屬性,當使用者點選了高亮區域的文字時,"高亮"屬性會替換掉原本的屬性
let border = TextBorder.border(with: UIColor.gray, cornerRadius: 3)

let highlight = TextHighlight()
highlight.color = .white
highlight.backgroundBorder = highlightBorder
highlight.tapAction = { (containerView, text, range, rect) in
    print("tap text range:...")
    // 你也可以把事件回撥放到 BSLabel 和 BSTextView 來處理。
}

// 2. 把"高亮"屬性設定到某個文字範圍
let attributedText = NSMutableAttributedString(string: " ")
attributedText.bs_set(textHighlight: highlight, range: highlightRange)
    
// 3. 把屬性文字設定到 BSLabel 或 BSTextView
let label = BSLabel()
label.attributedText = attributedText

let textView = BSTextView()
textView.delegate = self
textView.attributedText = ...
    
// 4. 接受事件回撥
label.highlightTapAction = { (containerView, text, range, rect) in
    print("tap text range:...")
};
label.highlightLongPressAction = { (containerView, text, range, rect) in
    print("tap text range:...")
};

// MARK: - TextViewDelegate
func textView(_ textView: BSTextView, didTap highlight: TextHighlight, in characterRange: NSRange, rect: CGRect) {
    print("tap text range:...")
}
func textView(_ textView: BSTextView, didLongPress highlight: TextHighlight, in characterRange: NSRange, rect: CGRect) {
    print("tap text range:...")
}
複製程式碼

圖文混排

let text = NSMutableAttributedString()
let font = UIFont.systemFont(ofSize: 16)
	
// 嵌入 UIImage
let image = UIImage.init(named: "dribbble64_imageio")
guard let attachment = NSMutableAttributedString.bs_attachmentString(with: image, contentMode: .center, attachmentSize: image?.size ?? .zero, alignTo: font, alignment: .center) else {
    return
}
text.append(attachment)
	
// 嵌入 UIView
let switcher = UISwitch()
switcher.sizeToFit()
guard let attachment1 = NSMutableAttributedString.bs_attachmentString(with: switcher, contentMode: .center, attachmentSize: switcher.frame.size, alignTo: font, alignment: .center) else {
    return
}
text.append(attachment1)
	
// 嵌入 CALayer
let layer = CAShapeLayer()
layer.path = ...
guard let attachment2 = NSMutableAttributedString.bs_attachmentString(with: layer, contentMode: .center, attachmentSize: layer.frame.size, alignTo: font, alignment: .center) else {
    return
}
text.append(attachment2)
複製程式碼

文字佈局計算

let text = NSAttributedString()
let size = CGSize(width: 100, height: CGFloat.greatestFiniteMagnitude)
let container = TextContainer()
container.size = size
guard let layout = TextLayout(container: container, text: text) else {
    return
}
	
// 獲取文字顯示位置和大小
layout.textBoundingRect // get bounding rect
layout.textBoundingSize // get bounding size
	
 // 查詢文字排版結果
layout.lineIndex(for: CGPoint(x: 10, y: 10))
layout.closestLineIndex(for: CGPoint(x: 10, y: 10))
layout.closestPosition(to: CGPoint(x: 10, y: 10))
layout.textRange(at: CGPoint(x: 10, y: 10))
layout.rect(for: TextRange(range: NSRange(location: 10, length: 2)))
layout.selectionRects(for: TextRange(range: NSRange(location: 10, length: 2)))
	
// 顯示文字排版結果
let label = BSLabel()
label.frame = CGRect(x: 0, y: 0, width: layout.textBoundingSize.width, height: layout.textBoundingSize.height)
label.textLayout = layout;
複製程式碼

文字行位置調整

// 由於中文、英文、Emoji 等字型高度不一致,或者富文字中出現了不同字號的字型,
// 可能會造成每行文字的高度不一致。這裡可以新增一個修改器來實現固定行高,或者自定義文字行位置。
  
// 簡單的方法:
// 1. 建立一個文字行位置修改類,實現 `TextLinePositionModifier` 協議。
// 2. 設定到 Label 或 TextView。

let modifier = TextLinePositionSimpleModifier()
modifier.fixedLineHeight = 24
  
let label = BSLabel()
label.linePositionModifier = modifier

// 完全控制:
let modifier = TextLinePositionSimpleModifier()
modifier.fixedLineHeight = 24
  
let container = TextContainer()
container.size = CGSize(width: 100, height: CGFloat.greatestFiniteMagnitude)
container.linePositionModifier = modifier
  
guard let layout = TextLayout(container: container, text: text) else {
    return
}
let label = BSLabel()
label.size = layout.textBoundingSize
label.textLayout = layout
複製程式碼

非同步排版和渲染

// 如果你在顯示字串時有效能問題,可以這樣開啟非同步模式:
let label = BSLabel()
label.displaysAsynchronously = true
    
// 如果需要獲得最高的效能,你可以在後臺執行緒用 `TextLayout` 進行預排版: 
let label = BSLabel()
label.displaysAsynchronously = true // 開啟非同步繪製
label.ignoreCommonProperties = true // 忽略除了 textLayout 之外的其他屬性

DispatchQueue.global().async {
    // 建立屬性字串
    let text = NSMutableAttributedString(string: "Some Text")
    text.bs_font = UIFont.systemFont(ofSize: 16)
    text.bs_color = UIColor.gray
    text.bs_set(color: .red, range: NSRange(location: 0, length: 4))

    // 建立文字容器
    let container = TextContainer()
    container.size = CGSize(width: 100, height: CGFloat.greatestFiniteMagnitude);
    container.maximumNumberOfRows = 0;

    // 生成排版結果
    let layout = TextLayout(container: container, text: text)

    DispatchQueue.main.async {
        label.frame = CGRect(x: 0, y: 0, width: layout.textBoundingSize.width, height: layout.textBoundingSize.height)
        label.textLayout = layout;
    }
}
複製程式碼

文字容器控制

let label = BSLabel()
label.textContainerPath = UIBezierPath(...)
label.exclusionPaths = [UIBezierPath(), ...]
label.textContainerInset = UIEdgeInsets(...)
label.verticalForm = true/false
    
let textView = BSTextView()
textView.exclusionPaths = [UIBezierPath(), ...]
textView.textContainerInset = UIEdgeInsets(...)
textView.verticalForm = true/false
複製程式碼

文字解析

// 1. 建立一個解析器
	
// 內建簡單的表情解析
let simpleEmoticonParser = TextSimpleEmoticonParser()
var mapper = [String: UIImage]()
mapper[":smile:"] = UIImage.init(named: "smile.png")
mapper[":cool:"] = UIImage.init(named: "cool.png")
mapper[":cry:"] = UIImage.init(named: "cry.png")
mapper[":wink:"] = UIImage.init(named: "wink.png")
simpleEmoticonParser.emoticonMapper = mapper;
	
// 內建簡單的 markdown 解析
let markdownParser = TextSimpleMarkdownParser()
markdownParser.setColorWithDarkTheme()
    
// 實現 `TextParser` 協議的自定義解析器
let parser = MyCustomParser()
    
// 2. 把解析器新增到 BSLabel 或 BSTextView
let label = BSLabel()
label.textParser = parser

let textView = BSTextView()
textView.textParser = parser
複製程式碼

Debug

// 設定一個全域性的 debug option 來顯示排版結果。
let debugOption = TextDebugOption()
debugOption.baselineColor = .red
debugOption.ctFrameBorderColor = .red
debugOption.ctLineFillColor = UIColor(red: 0, green: 0.463, blue: 1, alpha: 0.18)
debugOption.cgGlyphBorderColor = UIColor(red: 1, green: 0.524, blue: 0, alpha: 0.2)
TextDebugOption.setSharedDebugOption(debugOption)
複製程式碼

更多示例

檢視演示工程 Demo/BSTextDemo.xcodeproj:

BSText - YY大神的富文字框架 YYText 的 Swift 版本
BSText - YY大神的富文字框架 YYText 的 Swift 版本
BSText - YY大神的富文字框架 YYText 的 Swift 版本
BSText - YY大神的富文字框架 YYText 的 Swift 版本

安裝

CocoaPods

  1. 在 Podfile 中新增 pod 'BSText'

    source 'https://github.com/CocoaPods/Specs.git'
    platform :ios, '8.0'
    use_frameworks!
    
    target 'MyApp' do
      # your other pod
      # ...
      pod 'BSText', '~> 1.0'
    end
    複製程式碼
  2. 執行 pod installpod update

  3. 匯入模組 import BSText,OC 專案中使用 @import BSText;

Carthage

  1. 在 Cartfile 中新增 github "a1049145827/BSText"
  2. 執行 carthage update --platform ios 並將生成的 framework 新增到你的工程。
  3. 匯入模組 import BSText,OC 專案中使用 @import BSText;

手動安裝

  1. 下載 BSText 資料夾內的所有內容。
  2. 將 BSText 內的原始檔新增(拖放)到你的工程。
  3. 連結以下 frameworks:
    • UIKit
    • CoreFoundation
    • CoreText
    • QuartzCore
    • Accelerate
    • MobileCoreServices
  4. 匯入模組 import BSText,OC 專案中使用 @import BSText;

注意

你可以新增 YYImageYYWebImage 到你的工程,以支援動畫格式(GIF/APNG/WebP)的圖片。

文件

本專案目前還沒有生成線上文件,你可以在 CocoaDocs 檢視 YYText 的線上 API 文件,也可以用 appledoc 本地生成文件。

系統要求

該專案最低支援 iOS 8.0Xcode 10.0

已知問題

  • 與 YYText 一樣,BSText 並不能支援所有 CoreText/TextKit 的屬性,比如 NSBackgroundColor、NSStrikethrough、NSUnderline、NSAttachment、NSLink 等,但 BSText 中基本都有對應屬性作為替代。詳情見上方表格。
  • BSTextView 未實現區域性重新整理,所以在輸入和編輯大量的文字(比如超過大概五千個漢字、或大概一萬個英文字元)時會出現較明顯的卡頓現象。
  • 豎排版時,新增 exclusionPaths 在少數情況下可能會導致文字顯示空白。
  • 當新增了非矩形的 textContainerPath,並且有嵌入大於文字排版方向寬度的 RunDelegate 時,RunDelegate 之後的文字會無法顯示。這是 CoreText 的 Bug(或者說是 Feature)。

相關文章