BSText - YY大神的富文本框架 YYText 的 Swift 版本
原文鏈接
前言
??度過春節期間的安逸期,需要從慵懶的狀態盡快恢復過來,節前有幾個月時間,趁著公司業務線不怎么繁忙,抱著學習的態度,嘗試將 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 原生支持的屬性
| TextAttachment | TextAttachment | |
| TextHighlight | TextHighlight | |
| TextBinding | TextBinding | |
| TextShadow TextInnerShadow | TextShadow | |
| TextBorder | TextBorder | |
| TextBackgroundBorder | TextBorder | |
| TextBlockBorder | TextBorder | |
| TextGlyphTransform | NSValue(CGAffineTransform) | |
| TextUnderline | TextDecoration | |
| TextStrickthrough | TextDecoration | |
| TextBackedString | TextBackedString |
BSText 支持的 CoreText 屬性
| Font | UIFont(CTFontRef) | |
| Kern | NSNumber | |
| StrokeWidth | NSNumber | |
| StrokeColor | CGColorRef | |
| Shadow | NSShadow | |
| Ligature | NSNumber | |
| VerticalGlyphForm | NSNumber(BOOL) | |
| WritingDirection | NSArray(NSNumber) | |
| RunDelegate | CTRunDelegateRef | |
| TextAlignment | NSParagraphStyle (NSTextAlignment) | |
| LineBreakMode | NSParagraphStyle (NSLineBreakMode) | |
| LineSpacing | NSParagraphStyle (CGFloat) | |
| ParagraphSpacing ParagraphSpacingBefore | NSParagraphStyle (CGFloat) | |
| FirstLineHeadIndent | NSParagraphStyle (CGFloat) | |
| HeadIndent | NSParagraphStyle (CGFloat) | |
| TailIndent | NSParagraphStyle (CGFloat) | |
| MinimumLineHeight | NSParagraphStyle (CGFloat) | |
| MaximumLineHeight | NSParagraphStyle (CGFloat) | |
| LineHeightMultiple | NSParagraphStyle (CGFloat) | |
| BaseWritingDirection | NSParagraphStyle (NSWritingDirection) | |
| 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 = textlet 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) inprint("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) inprint("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 = attributedTextlet textView = BSTextView() textView.delegate = self textView.attributedText = ...// 4. 接受事件回調 label.highlightTapAction = { (containerView, text, range, rect) inprint("tap text range:...") }; label.highlightLongPressAction = { (containerView, text, range, rect) inprint("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 = 24let label = BSLabel() label.linePositionModifier = modifier// 完全控制: let modifier = TextLinePositionSimpleModifier() modifier.fixedLineHeight = 24let container = TextContainer() container.size = CGSize(width: 100, height: CGFloat.greatestFiniteMagnitude) container.linePositionModifier = modifierguard 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.graytext.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/falselet 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 = parserlet 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:
安裝
CocoaPods
在 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 復制代碼執行 pod install 或 pod update。
導入模塊 import BSText,OC 項目中使用 @import BSText;。
Carthage
手動安裝
- UIKit
- CoreFoundation
- CoreText
- QuartzCore
- Accelerate
- MobileCoreServices
注意
你可以添加 YYImage 或 YYWebImage 到你的工程,以支持動畫格式(GIF/APNG/WebP)的圖片。
文檔
本項目目前還沒有生成在線文檔,你可以在 CocoaDocs 查看 YYText 的在線 API 文檔,也可以用 appledoc 本地生成文檔。
系統要求
該項目最低支持 iOS 8.0 和 Xcode 10.0。
已知問題
- 與 YYText 一樣,BSText 并不能支持所有 CoreText/TextKit 的屬性,比如 NSBackgroundColor、NSStrikethrough、NSUnderline、NSAttachment、NSLink 等,但 BSText 中基本都有對應屬性作為替代。詳情見上方表格。
- BSTextView 未實現局部刷新,所以在輸入和編輯大量的文本(比如超過大概五千個漢字、或大概一萬個英文字符)時會出現較明顯的卡頓現象。
- 豎排版時,添加 exclusionPaths 在少數情況下可能會導致文本顯示空白。
- 當添加了非矩形的 textContainerPath,并且有嵌入大于文本排版方向寬度的 RunDelegate 時,RunDelegate 之后的文字會無法顯示。這是 CoreText 的 Bug(或者說是 Feature)。
轉載于:https://juejin.im/post/5c7c8a91f265da2dd53fe444
總結
以上是生活随笔為你收集整理的BSText - YY大神的富文本框架 YYText 的 Swift 版本的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 再度吐槽,PHP在centos7的安装方
- 下一篇: k倍区间