AttributedString に UTF-8 / UTF-16 ビューを追加する
AttributedString UTF-8 and UTF-16 Views
このダイジェストは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 との相互運用です。NSAttributedString や NSString は 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 を経由します。
AttributedSubstring と AttributedStringProtocol
AttributedSubstring と AttributedStringProtocol にも 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 として振る舞い、BidirectionalCollection や RangeReplaceableCollection の最適化された実装も内部に持っているため、距離計算やイテレーションは内部の連続表現を活かして効率的に動作します。