WWDC Quick Look 💓 By SwiftGGTeam
Meet TextKit 2

Meet TextKit 2

元の動画を見る

ハイライト

TextKit 2 は Apple の次世代テキスト エンジンであり、正確性 (グリフ処理の抽象化)、安全性 (値セマンティクスの重視)、パフォーマンス (ビューポート ベースのレイアウトとレンダリング) という 3 つの核となる設計原則に基づいています。

主な内容

TextKit 1 は OpenStep から始まり、20 年以上の歴史があります。すべての Apple プラットフォームでテキスト表示を推進しますが、元の設計による制約があり、最新のテクノロジーとうまく統合するのに苦労しています。Big Sur の TextKit 2 は、システム内の多くのテキスト コンポーネントを密かに強化しています。

TextKit 2 と TextKit 1 は共存しており、どちらも Foundation、Quartz、Core Text 上に構築されています。MVC 設計が維持されています。ビューには新しいバージョンがあり、UIKit/AppKit、モデル層、コントローラー層に追加のコンポーネントが含まれています。

3 つの中心的な設計原則: 正確性 抽象グリフ処理、安全性 は値のセマンティクスを重視、パフォーマンス はビューポート ベースのレイアウトとレンダリングを使用します。

##詳細

レスポンシブ レイアウトの更新

32:22

func textViewportLayoutControllerWillLayout(_ controller: NSTextViewportLayoutController) {
    contentLayer.sublayers = nil
    CATransaction.begin()
}

キーポイント:

  • textViewportLayoutControllerWillLayoutレイアウトが開始される前に呼び出されます
  • サブレイヤをクリアし、新しい CATransaction を開始します。
  • レイアウト更新の準備
func textViewportLayoutController(_ controller: NSTextViewportLayoutController,
                                  configureRenderingSurfaceFor textLayoutFragment: NSTextLayoutFragment) {
    let (layer, layerIsNew) = findOrCreateLayer(textLayoutFragment)
    if !layerIsNew {
        let oldPosition = layer.position
        layer.updateGeometry()
        if oldPosition != layer.position {
            animate(layer, from: oldPosition, to: layer.position)
        }
    }
    contentLayer.addSublayer(layer)
}

キーポイント:

  • configureRenderingSurfaceForレイアウト フラグメントごとにレンダリング サーフェスを構成する
  • 既存のレイヤーを追跡し、位置の変更のみを更新できます
  • アニメーションによる位置変更により、スムーズな視覚体験を提供します
func textViewportLayoutControllerDidLayout(_ controller: NSTextViewportLayoutController) {
    CATransaction.commit()
    updateSelectionHighlights()
    updateContentSizeIfNeeded()
    adjustViewportOffsetIfNeeded()
}

キーポイント:

  • textViewportLayoutControllerDidLayoutレイアウトが完了した後に呼び出されます
  • CATransaction を送信して、選択範囲の強調表示、コンテンツ サイズなどを更新します。 -完全なレイアウトサイクル

テキスト属性の表示を上書きする

33:47

func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? {
    var paragraphWithDisplayAttributes: NSTextParagraph? = nil

    let originalText = textContentStorage.textStorage!.attributedSubstring(from: range)
    if originalText.attribute(.commentDepth, at: 0, effectiveRange: nil) != nil {
        let displayAttributes: [NSAttributedString.Key: AnyObject] = [
            .font: commentFont,
            .foregroundColor: commentColor
        ]
        let textWithDisplayAttributes = NSMutableAttributedString(attributedString: originalText)
        let rangeForDisplayAttributes = NSRange(location: 0, length: textWithDisplayAttributes.length - 2)
        textWithDisplayAttributes.addAttributes(displayAttributes, range: rangeForDisplayAttributes)

        paragraphWithDisplayAttributes = NSTextParagraph(attributedString: textWithDisplayAttributes)
    }
    return paragraphWithDisplayAttributes
}

キーポイント:

  • textParagraphWithこのメソッドにより、元のテキスト ストレージを変更せずに表示プロパティをオーバーライドできます
  • スタイルを一時的に変更する必要があるが、元のデータを汚したくないシナリオに適しています
  • 戻るnil元の段落を使用する場合

特定の要素を非表示にする

34:06

func textContentManager(_ textContentManager: NSTextContentManager,
                        shouldEnumerate textElement: NSTextElement,
                        with options: NSTextElementProviderEnumerationOptions) -> Bool {
    if !showComments {
        if let paragraph = textElement as? NSTextParagraph {
            let commentDepthValue = paragraph.attributedString.attribute(.commentDepth, at: 0, effectiveRange: nil)
            if commentDepthValue != nil {
                return false
            }
        }
    }
    return true
}

キーポイント:

  • shouldEnumerateレイアウトに参加する特定の要素を列挙するかどうかを決定します
  • 戻るfalseこの要素のレイアウトとレンダリングはスキップされます
  • コメントの表示/非表示などの機能が実現可能

カスタム レイアウト フラグメントを生成する

34:28

func textLayoutManager(_ textLayoutManager: NSTextLayoutManager,
                       textLayoutFragmentFor location: NSTextLocation,
                       in textElement: NSTextElement) -> NSTextLayoutFragment {
    let index = textLayoutManager.offset(from: textLayoutManager.documentRange.location, to: location)
    let commentDepthValue = textContentStorage!.textStorage!.attribute(.commentDepth, at: index, effectiveRange: nil) as? NSNumber

    if commentDepthValue != nil {
        let layoutFragment = BubbleLayoutFragment(textElement: textElement, range: textElement.elementRange)
        layoutFragment.commentDepth = commentDepthValue!.uintValue
        return layoutFragment
    } else {
        return NSTextLayoutFragment(textElement: textElement, range: textElement.elementRange)
    }
}

キーポイント:

  • textLayoutFragmentFor特定の場所のカスタム レイアウト フラグメントを返すことができます
  • テキスト属性に基づいてさまざまなフラグメント タイプを返すことができます
  • カスタム フラグメントにより、特別なレンダリング効果 (バブルなど) を実現できます。

バブルの背景を描画する

34:58

override func draw(at renderingOrigin: CGPoint, in ctx: CGContext) {
    ctx.saveGState()
    let bubblePath = createBubblePath(with: ctx)
    ctx.addPath(bubblePath)
    ctx.setFillColor(bubbleColor.cgColor)
    ctx.fillPath()
    ctx.restoreGState()

    super.draw(at: renderingOrigin, in: ctx)
}

キーポイント:

  • カスタマイズ中NSTextLayoutFragmentサブクラスでのオーバーライドdraw方法
  • 最初にカスタム背景 (泡など) を描画してから、super.drawテキストを描画する
  • 使用CGPath(roundedRect:cornerWidth:cornerHeight:)丸みを帯びた長方形のパスを作成する

NSTextView を TextKit 2 に切り替えます

37:26

var scrollView: NSScrollView!
var containerSize = CGSize.zero
var textContainer = NSTextContainer()
var textContentStorage = NSTextContentStorage()

override func viewDidLoad() {
    super.viewDidLoad()

    scrollView = NSScrollView(frame: view.bounds)
    view.addSubview(scrollView)

    setUpScrollView(scrollsHorizontally: false)
}

func setUpTextContainer(scrollsHorizontally: Bool) {
    let contentSize = scrollView.contentSize
    if scrollsHorizontally {
        containerSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
        textContainer.containerSize = containerSize
        textContainer.widthTracksTextView = false
    } else {
        containerSize = NSSize(width: contentSize.width, height: CGFloat.greatestFiniteMagnitude)
        textContainer.containerSize = containerSize
        textContainer.widthTracksTextView = true
    }
}

func setUpTextView(scrollsHorizontally: Bool) {
    let textLayoutManager = NSTextLayoutManager()
    textLayoutManager.textContainer = textContainer

    textContentStorage.addTextLayoutManager(textLayoutManager)

    let textView = NSTextView(frame: scrollView.contentView.bounds, textContainer: textLayoutManager.textContainer)

    textView.isEditable = true
    textView.isSelectable = true
    textView.textStorage?.append(NSAttributedString(string: "Text content..."))
    scrollView.documentView = textView
}

キーポイント:

  • 作成するNSTextLayoutManagerそしてそれに関連するNSTextContainer
  • レイアウトマネージャーを追加NSTextContentStorage
  • レイアウトマネージャーを使用するtextContainer初期化NSTextView
  • これにより、テキスト ビューがレイアウトとレンダリングに TextKit 2 を使用するようになります。

重要ポイント

1. テキストエディタにバブルコメント機能を追加

アプリにテキスト編集機能 (コード エディター、ドキュメント コラボレーションなど) がある場合は、コード レビューと同様のバブル コメント機能を追加します。

実装アイデア: コメントのカスタマイズを作成するNSTextLayoutFragmentサブカテゴリ (例:BubbleLayoutFragment)、存在するtextLayoutFragmentForコメント属性を検出し、カスタム フラグメントを返します。リライトdrawバブルの背景を描画する方法。

2. 構文強調表示のインクリメンタル レンダリングを実装する

コード エディターの構文の強調表示は頻繁に更新する必要があります。TextKit 2 のビューポートベースのレイアウトは、表示されている領域のみを再描画できます。

実装アイデア: 使用NSTextViewportLayoutController表示領域の変化を監視し、表示範囲内のテキスト断片にのみハイライト スタイルを適用します。協力するconfigureRenderingSurfaceForレンダリング結果のキャッシュ。

3. チャット メッセージの特別なレイアウトを実装する

チャット アプリのメッセージには通常、異なるレイアウトが必要です (自分のメッセージが右側、相手のメッセージが右側、場合によってはバブルやアバターが表示される)。

実装アイデア: メッセージごとに作成NSUserActivity、存在するtextLayoutFragmentFor送信者に基づいてさまざまなカスタム フラグメントを返します。カスタム スニペットは、背景、丸い角、アバターの位置などを制御できます。

4. 大きなドキュメントのスクロール パフォーマンスを最適化する

TextKit 1 は、数十万行を含む大きなドキュメントを表示するとフリーズする場合があります。TextKit 2 のビューポート レイアウトは、表示可能なコンテンツのみを処理します。

実装アイデア: 必ず使用してくださいNSTextLayoutManager古いものの代わりにNSLayoutManager。のためにNSTextContainer合理的な設定をするcontainerSize、無限の幅によって引き起こされるパフォーマンスの問題を回避するため。

5. 折りたたみ可能なテキスト領域を実装する

マークダウン エディターとコード エディターでは、多くの場合、コード ブロックまたは章を折りたたむ必要があります。TextKit 2 は要素を非表示にすることでこれを実現します。

実装アイデア: でshouldEnumerate折り畳み状態を確認し、折り畳まれた要素を返却しますfalse。折りたたまれた状態を更新するときに呼び出されますtextLayoutManager.textViewportLayoutController.layoutViewport再レイアウト。

関連セッション

コメント

GitHub Issues · utterances