Skip to content

Attributed String Reference

Use when choosing between AttributedString and NSAttributedString, defining custom attributes, converting between them, or deciding which model should own rich text in a feature. Reach for this when the main task is the attributed-string model decision, not low-level formatting catalog lookup.

Decision Skills

Use when choosing between AttributedString and NSAttributedString, defining custom attributes, converting between them, or deciding which model should own rich text in a feature. Reach for this when the main task is the attributed-string model decision, not low-level formatting catalog lookup.

Family: Rich Text And Formatting

Use this skill when the main question is which attributed-text model to use and how to convert safely between them.

  • You are choosing between AttributedString and NSAttributedString.
  • You need custom attributes or scopes.
  • You need conversion rules between SwiftUI and UIKit/AppKit text APIs.
  • SwiftUI-first, Codable, or Markdown-heavy pipeline -> AttributedString
  • UIKit/AppKit or TextKit API boundary -> NSAttributedString
  • Exact formatting attribute catalog needed -> /skill apple-text-formatting-ref

Keep this file for the model choice, mutation rules, and conversion boundaries. For custom attribute scopes, paragraph-style recipes, and the full attribute quick reference, use advanced-patterns.md. For Apple-authored Xcode-backed guidance on the newest Foundation changes, use /skill apple-text-apple-docs.

AspectAttributedString (Swift)NSAttributedString (ObjC)
TypeValue type (struct)Reference type (class)
AttributesType-safe key pathsUntyped [Key: Any] dictionary
CodableYesNo (use NSCoding)
MarkdownBuilt-in parsingNo
Thread safetyCopy-on-write (safe)Immutable safe, mutable not
Mutable variantSame type (var)NSMutableAttributedString
Required bySwiftUI TextUIKit/AppKit text properties, TextKit
AvailableiOS 15+ / macOS 12+All versions
Need Codable/serialization? → AttributedString
Need Markdown parsing? → AttributedString
SwiftUI Text view? → AttributedString
UIKit label/textView attributedText? → NSAttributedString
TextKit 1 (NSTextStorage)? → NSAttributedString
TextKit 2 (delegate methods)? → NSAttributedString
Core Text? → NSAttributedString (CFAttributedString)
Cross-platform code? → AttributedString (convert at boundaries)

Best practice: Use AttributedString as your internal representation. Convert to NSAttributedString at UIKit/AppKit API boundaries.

var str = AttributedString("Hello World")
str.font = .body
str.foregroundColor = .red
// Range-based attributes
if let range = str.range(of: "World") {
str[range].link = URL(string: "https://example.com")
str[range].font = .body.bold()
}
var greeting = AttributedString("Hello ")
greeting.font = .body
var name = AttributedString("World")
name.font = .body.bold()
name.foregroundColor = .blue
let combined = greeting + name
// Basic Markdown
let str = try AttributedString(markdown: "**Bold** and *italic*")
// Custom interpretation
let str = try AttributedString(
markdown: "Visit [Apple](https://apple.com)",
including: \.foundation,
options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
)
let str = AttributedString("Hello 👋🏽")
str.characters // CharacterView — Swift Characters
str.unicodeScalars // UnicodeScalarView
str.utf8 // UTF8View
str.utf16 // UTF16View
// Iterate characters with attributes
for run in str.runs {
let substring = str[run.range]
let font = run.font
let color = run.foregroundColor
}

A “run” is a contiguous range with identical attributes:

for run in str.runs {
print("Range: \(run.range)")
print("Font: \(run.font ?? .body)")
// Access the attributed substring
let sub = str[run.range]
}
// Filter runs by attribute
for (color, range) in str.runs[\.foregroundColor] {
print("Color: \(String(describing: color)) at \(range)")
}
var str = AttributedString("Hello World")
// Replace text
if let range = str.range(of: "World") {
str.replaceSubrange(range, with: AttributedString("Swift"))
}
// Set attribute on entire string
str.foregroundColor = .red
// Merge attributes
var container = AttributeContainer()
container.font = .body.bold()
container.foregroundColor = .blue
str.mergeAttributes(container)
// Remove attribute
str.foregroundColor = nil

Critical: Mutating an AttributedString invalidates ALL existing indices and ranges.

// ❌ WRONG — indices invalidated after mutation
var str = AttributedString("Hello World")
let range = str.range(of: "World")!
str.replaceSubrange(str.range(of: "Hello")!, with: AttributedString("Hi"))
// range is NOW INVALID — using it may crash
// ✅ CORRECT — re-find ranges after mutation
var str = AttributedString("Hello World")
str.replaceSubrange(str.range(of: "Hello")!, with: AttributedString("Hi"))
if let range = str.range(of: "World") {
str[range].font = .body.bold()
}
let attrs: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 16),
.foregroundColor: UIColor.red,
.kern: 1.5
]
let str = NSAttributedString(string: "Hello", attributes: attrs)
let mutable = NSMutableAttributedString(string: "Hello World")
// Add attributes to range
mutable.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: 16),
range: NSRange(location: 6, length: 5))
// Set attributes (replaces all existing in range)
mutable.setAttributes([.foregroundColor: UIColor.blue],
range: NSRange(location: 0, length: 5))
// Remove attribute
mutable.removeAttribute(.font, range: NSRange(location: 0, length: mutable.length))
// Replace characters
mutable.replaceCharacters(in: NSRange(location: 0, length: 5), with: "Hi")
// Insert
mutable.insert(NSAttributedString(string: "Hey "), at: 0)
// Delete
mutable.deleteCharacters(in: NSRange(location: 0, length: 4))
let str: NSAttributedString = ...
// Enumerate all attributes
str.enumerateAttributes(in: NSRange(location: 0, length: str.length)) { attrs, range, stop in
if let font = attrs[.font] as? UIFont {
print("Font: \(font) at \(range)")
}
}
// Enumerate specific attribute
str.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: str.length)) { value, range, stop in
if let color = value as? UIColor {
print("Color: \(color) at \(range)")
}
}
// Using all default scopes
let nsAS = NSAttributedString(attrString)
// With specific scope (preserves custom attributes)
let nsAS = try NSAttributedString(attrString, including: \.myApp)
// Using all default scopes
let swiftAS = try AttributedString(nsAS, including: \.foundation)
// With specific scope
let swiftAS = try AttributedString(nsAS, including: \.myApp)

Pitfall: Conversion without including: your custom scope silently drops custom attributes. Always include the scope containing your custom keys.

  1. Forgetting including: in conversion — Custom attributes silently dropped during AttributedStringNSAttributedString conversion.
  2. Index invalidation — Mutating AttributedString invalidates all existing indices. Re-find ranges after mutation.
  3. NSParagraphStyle is immutable — Always create NSMutableParagraphStyle, then assign. Cannot modify after setting on attributed string.
  4. Mixing AttributedString and NSAttributedString — UIKit APIs require NSAttributedString. SwiftUI requires AttributedString. Convert at boundaries.
  5. Scope must include standard scopes — Custom AttributeScope should include FoundationAttributes and UIKitAttributes/SwiftUIAttributes for round-trip conversion.
  6. NSTextStorage IS an NSMutableAttributedString — Can use all NSAttributedString APIs directly on text storage.

This page documents the apple-text-attributed-string decision skill. Use it when the main task is choosing the right Apple text API, view, or architecture.

  • apple-text-formatting-ref: Use when the user already knows the formatting problem and needs exact text-formatting attributes such as NSAttributedString.Key values, underline styles, shadows, lists, tables, or view-compatibility rules. Reach for this when the job is verifying concrete formatting APIs, not choosing the text model.
  • apple-text-swiftui-bridging: Use when deciding whether a text type or attribute model crosses the SwiftUI and TextKit boundary cleanly, such as AttributedString, NSAttributedString, UITextView, or SwiftUI Text. Reach for this when the main question is interoperability and support boundaries, not wrapper mechanics.
  • apple-text-markdown: Use when the user is working with Markdown in SwiftUI Text or AttributedString and needs to know what renders, what is ignored, how PresentationIntent behaves, or when native Markdown stops being enough. Reach for this when the problem is Markdown semantics, not general attributed-string choice.
  • skills/apple-text-attributed-string/advanced-patterns.md
Full SKILL.md source
SKILL.md
---
name: apple-text-attributed-string
description: Use when choosing between AttributedString and NSAttributedString, defining custom attributes, converting between them, or deciding which model should own rich text in a feature. Reach for this when the main task is the attributed-string model decision, not low-level formatting catalog lookup.
license: MIT
---
# Attributed String Reference
Use this skill when the main question is which attributed-text model to use and how to convert safely between them.
## When to Use
- You are choosing between `AttributedString` and `NSAttributedString`.
- You need custom attributes or scopes.
- You need conversion rules between SwiftUI and UIKit/AppKit text APIs.
## Quick Decision
- SwiftUI-first, Codable, or Markdown-heavy pipeline -> `AttributedString`
- UIKit/AppKit or TextKit API boundary -> `NSAttributedString`
- Exact formatting attribute catalog needed -> `/skill apple-text-formatting-ref`
## Core Guidance
Keep this file for the model choice, mutation rules, and conversion boundaries. For custom attribute scopes, paragraph-style recipes, and the full attribute quick reference, use [advanced-patterns.md](advanced-patterns.md). For Apple-authored Xcode-backed guidance on the newest Foundation changes, use `/skill apple-text-apple-docs`.
## AttributedString vs NSAttributedString
| Aspect | AttributedString (Swift) | NSAttributedString (ObjC) |
|--------|-------------------------|--------------------------|
| **Type** | Value type (struct) | Reference type (class) |
| **Attributes** | Type-safe key paths | Untyped `[Key: Any]` dictionary |
| **Codable** | Yes | No (use NSCoding) |
| **Markdown** | Built-in parsing | No |
| **Thread safety** | Copy-on-write (safe) | Immutable safe, mutable not |
| **Mutable variant** | Same type (var) | NSMutableAttributedString |
| **Required by** | SwiftUI Text | UIKit/AppKit text properties, TextKit |
| **Available** | iOS 15+ / macOS 12+ | All versions |
### When to Use Which
```
Need Codable/serialization? → AttributedString
Need Markdown parsing? → AttributedString
SwiftUI Text view? → AttributedString
UIKit label/textView attributedText? → NSAttributedString
TextKit 1 (NSTextStorage)? → NSAttributedString
TextKit 2 (delegate methods)? → NSAttributedString
Core Text? → NSAttributedString (CFAttributedString)
Cross-platform code? → AttributedString (convert at boundaries)
```
**Best practice:** Use `AttributedString` as your internal representation. Convert to `NSAttributedString` at UIKit/AppKit API boundaries.
## Swift AttributedString (iOS 15+)
### Basic Usage
```swift
var str = AttributedString("Hello World")
str.font = .body
str.foregroundColor = .red
// Range-based attributes
if let range = str.range(of: "World") {
str[range].link = URL(string: "https://example.com")
str[range].font = .body.bold()
}
```
### Concatenation
```swift
var greeting = AttributedString("Hello ")
greeting.font = .body
var name = AttributedString("World")
name.font = .body.bold()
name.foregroundColor = .blue
let combined = greeting + name
```
### Markdown
```swift
// Basic Markdown
let str = try AttributedString(markdown: "**Bold** and *italic*")
// Custom interpretation
let str = try AttributedString(
markdown: "Visit [Apple](https://apple.com)",
including: \.foundation,
options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
)
```
### Views (Character-Level Access)
```swift
let str = AttributedString("Hello 👋🏽")
str.characters // CharacterView — Swift Characters
str.unicodeScalars // UnicodeScalarView
str.utf8 // UTF8View
str.utf16 // UTF16View
// Iterate characters with attributes
for run in str.runs {
let substring = str[run.range]
let font = run.font
let color = run.foregroundColor
}
```
### Runs
A "run" is a contiguous range with identical attributes:
```swift
for run in str.runs {
print("Range: \(run.range)")
print("Font: \(run.font ?? .body)")
// Access the attributed substring
let sub = str[run.range]
}
// Filter runs by attribute
for (color, range) in str.runs[\.foregroundColor] {
print("Color: \(String(describing: color)) at \(range)")
}
```
### Mutation
```swift
var str = AttributedString("Hello World")
// Replace text
if let range = str.range(of: "World") {
str.replaceSubrange(range, with: AttributedString("Swift"))
}
// Set attribute on entire string
str.foregroundColor = .red
// Merge attributes
var container = AttributeContainer()
container.font = .body.bold()
container.foregroundColor = .blue
str.mergeAttributes(container)
// Remove attribute
str.foregroundColor = nil
```
### Index Invalidation
**Critical:** Mutating an `AttributedString` invalidates ALL existing indices and ranges.
```swift
// ❌ WRONG — indices invalidated after mutation
var str = AttributedString("Hello World")
let range = str.range(of: "World")!
str.replaceSubrange(str.range(of: "Hello")!, with: AttributedString("Hi"))
// range is NOW INVALID — using it may crash
// ✅ CORRECT — re-find ranges after mutation
var str = AttributedString("Hello World")
str.replaceSubrange(str.range(of: "Hello")!, with: AttributedString("Hi"))
if let range = str.range(of: "World") {
str[range].font = .body.bold()
}
```
## NSAttributedString
### Basic Usage
```swift
let attrs: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 16),
.foregroundColor: UIColor.red,
.kern: 1.5
]
let str = NSAttributedString(string: "Hello", attributes: attrs)
```
### Mutable Variant
```swift
let mutable = NSMutableAttributedString(string: "Hello World")
// Add attributes to range
mutable.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: 16),
range: NSRange(location: 6, length: 5))
// Set attributes (replaces all existing in range)
mutable.setAttributes([.foregroundColor: UIColor.blue],
range: NSRange(location: 0, length: 5))
// Remove attribute
mutable.removeAttribute(.font, range: NSRange(location: 0, length: mutable.length))
// Replace characters
mutable.replaceCharacters(in: NSRange(location: 0, length: 5), with: "Hi")
// Insert
mutable.insert(NSAttributedString(string: "Hey "), at: 0)
// Delete
mutable.deleteCharacters(in: NSRange(location: 0, length: 4))
```
### Enumerating Attributes
```swift
let str: NSAttributedString = ...
// Enumerate all attributes
str.enumerateAttributes(in: NSRange(location: 0, length: str.length)) { attrs, range, stop in
if let font = attrs[.font] as? UIFont {
print("Font: \(font) at \(range)")
}
}
// Enumerate specific attribute
str.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: str.length)) { value, range, stop in
if let color = value as? UIColor {
print("Color: \(color) at \(range)")
}
}
```
## Conversion Between Types
### AttributedString → NSAttributedString
```swift
// Using all default scopes
let nsAS = NSAttributedString(attrString)
// With specific scope (preserves custom attributes)
let nsAS = try NSAttributedString(attrString, including: \.myApp)
```
### NSAttributedString → AttributedString
```swift
// Using all default scopes
let swiftAS = try AttributedString(nsAS, including: \.foundation)
// With specific scope
let swiftAS = try AttributedString(nsAS, including: \.myApp)
```
**Pitfall:** Conversion without `including:` your custom scope silently drops custom attributes. Always include the scope containing your custom keys.
## Common Pitfalls
1. **Forgetting `including:` in conversion** — Custom attributes silently dropped during `AttributedString``NSAttributedString` conversion.
2. **Index invalidation** — Mutating `AttributedString` invalidates all existing indices. Re-find ranges after mutation.
3. **NSParagraphStyle is immutable** — Always create `NSMutableParagraphStyle`, then assign. Cannot modify after setting on attributed string.
4. **Mixing AttributedString and NSAttributedString** — UIKit APIs require `NSAttributedString`. SwiftUI requires `AttributedString`. Convert at boundaries.
5. **Scope must include standard scopes** — Custom `AttributeScope` should include `FoundationAttributes` and `UIKitAttributes`/`SwiftUIAttributes` for round-trip conversion.
6. **NSTextStorage IS an NSMutableAttributedString** — Can use all NSAttributedString APIs directly on text storage.
## Related Skills
- For custom scopes, paragraph-style recipes, and the attribute-key table, see [advanced-patterns.md](advanced-patterns.md).
- Use `/skill apple-text-formatting-ref` for the full formatting-key catalog.
- Use `/skill apple-text-swiftui-bridging` when the real issue is what SwiftUI renders or drops.
- Use `/skill apple-text-markdown` when Markdown parsing is driving the attributed-text shape.