Swift Digest

AttributedString に UTF-8 / UTF-16 ビューを追加する

AttributedString UTF-8 and UTF-16 Views

Proposal
SF-0012
Authors
Jeremy Schonfeld
Review Manager
Tina Liu
Status
Accepted

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

AttributedString は属性付きテキストを表す Foundation の型で、テキストの内容そのものは複数の「ビュー」を通して操作する設計になっています。AttributedString 自身は Collection ではなく、用途に応じた次の 3 つのビューを提供してきました。

  • .characters : 表示単位(grapheme cluster)を要素とする Character のコレクション
  • .unicodeScalars : Unicodeスカラを要素とする Unicode.Scalar のコレクション
  • .runs : 同じ属性が連続する区間(attribute run)のコレクション

これら 3 つのビューはテキスト本体と属性 run を扱う基本 API として十分機能します。しかし、より低レベルなエンコーディング単位、すなわち UTF-8 や UTF-16 のコードユニット単位でテキストを覗きたい場面では足りません。

特に問題になるのは UTF-16 単位でインデックスや要素を扱う API との相互運用です。NSAttributedStringNSString は UTF-16 オフセットをインデックスとして使いますし、テキストの選択範囲やキャレット位置を UTF-16 オフセットで保持している既存コードは数多く存在します。String には .utf8 / .utf16 ビューがあり、UTF-8 / UTF-16 単位でのイテレーションやオフセット計算が直接できますが、AttributedString には対応するビューがなく、これらの操作を素直に書けない状態でした。

02 どのように解決されるのか

String.utf8 / .utf16 と同じ感覚で使える、イミュータブルな UTF-8 ビューと UTF-16 ビューが AttributedString に追加されます。FoundationPreview 6.2 以降で利用できます。

@available(FoundationPreview 6.2, *)
extension AttributedString {
    public struct UTF8View: BidirectionalCollection, CustomStringConvertible, Sendable {
        public typealias Element = UTF8.CodeUnit
        public typealias Index = AttributedString.Index
        public typealias SubSequence = AttributedString.UTF8View
    }

    public struct UTF16View: BidirectionalCollection, CustomStringConvertible, Sendable {
        public typealias Element = UTF16.CodeUnit
        public typealias Index = AttributedString.Index
        public typealias SubSequence = AttributedString.UTF16View
    }

    public var utf8: UTF8View { get }
    public var utf16: UTF16View { get }
}

要素はそれぞれ UTF-8 / UTF-16 のコードユニット(UTF8.CodeUnit / UTF16.CodeUnit)で、インデックスは既存のビューと同じ AttributedString.Index を使います。これにより、.characters.unicodeScalars で取り回しているインデックスと相互に行き来できます。SubSequence は同じビュー型なので、スライスを取っても扱いが変わりません。

var attrStr: AttributedString = ...

// UTF-8 コードユニットを順に処理
for codeUnit in attrStr.utf8 {
    print(codeUnit)
}

// 任意の AttributedString.Index が、UTF-8 換算で先頭から何バイト目かを求める
let offset = attrStr.utf8.distance(from: attrStr.startIndex, to: someOtherIndex)

これらのビューは読み取り専用です。属性 run の構造を保ったままテキストを書き換えるのは、UTF-8 / UTF-16 単位の操作とは噛み合いが悪いため、変更系の API は提供されません。テキストの編集はこれまで通り .characters.unicodeScalars を経由します。

AttributedSubstringAttributedStringProtocol

AttributedSubstringAttributedStringProtocol にも utf8 / utf16 プロパティが追加されるため、AttributedString 本体だけでなくスライスやプロトコルベースのジェネリックなコードからも同じように UTF-8 / UTF-16 ビューを取り出せます。

@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
protocol AttributedStringProtocol {
    // ...

    @available(FoundationPreview 6.2, *)
    var utf8: AttributedString.UTF8View { get }
    @available(FoundationPreview 6.2, *)
    var utf16: AttributedString.UTF16View { get }
}

@available(FoundationPreview 6.2, *)
extension AttributedStringProtocol {
    public var utf8: AttributedString.UTF8View { get }
    public var utf16: AttributedString.UTF16View { get }
}

@available(FoundationPreview 6.2, *)
extension AttributedSubstring {
    public var utf8: AttributedString.UTF8View { get }
    public var utf16: AttributedString.UTF16View { get }
}

AttributedStringProtocol の新しい要件にはデフォルト実装が用意されているため、既存の適合型を壊すことはありません。

主な使いどころ

NSAttributedString / NSString をはじめとする UTF-16 ベースの API との橋渡しが代表的な用途です。たとえば外部から「先頭から N 個の UTF-16 コードユニット目」という形で位置情報が渡される場合、utf16 ビュー上で index(_:offsetBy:) を使って AttributedString.Index に変換できます。

// UTF-16 オフセットから AttributedString.Index を得る
let utf16 = attrStr.utf16
let index = utf16.index(utf16.startIndex, offsetBy: utf16Offset)

// 逆方向: 既存のインデックスを UTF-16 オフセットに直す
let offset = utf16.distance(from: utf16.startIndex, to: index)

UTF-8 についても同様で、ファイルやネットワーク越しに UTF-8 バイト列としてテキストを扱うコードと、AttributedString のインデックスとの対応付けに使えます。なお、ビューはあくまで BidirectionalCollection として振る舞い、BidirectionalCollectionRangeReplaceableCollection の最適化された実装も内部に持っているため、距離計算やイテレーションは内部の連続表現を活かして効率的に動作します。