apple-text-attributed-string
Use when choosing between AttributedString and NSAttributedString, defining custom attributes, or converting between them
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
AttributedStringandNSAttributedString. - 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. 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
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
var greeting = AttributedString("Hello ")
greeting.font = .body
var name = AttributedString("World")
name.font = .body.bold()
name.foregroundColor = .blue
let combined = greeting + name
Markdown
// 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)
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:
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
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.
// ❌ 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
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
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
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
// Using all default scopes
let nsAS = NSAttributedString(attrString)
// With specific scope (preserves custom attributes)
let nsAS = try NSAttributedString(attrString, including: \.myApp)
NSAttributedString → AttributedString
// 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
- Forgetting
including:in conversion — Custom attributes silently dropped duringAttributedString↔NSAttributedStringconversion. - Index invalidation — Mutating
AttributedStringinvalidates all existing indices. Re-find ranges after mutation. - NSParagraphStyle is immutable — Always create
NSMutableParagraphStyle, then assign. Cannot modify after setting on attributed string. - Mixing AttributedString and NSAttributedString — UIKit APIs require
NSAttributedString. SwiftUI requiresAttributedString. Convert at boundaries. - Scope must include standard scopes — Custom
AttributeScopeshould includeFoundationAttributesandUIKitAttributes/SwiftUIAttributesfor round-trip conversion. - 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.
- Use
/skill apple-text-formatting-reffor the full formatting-key catalog. - Use
/skill apple-text-swiftui-bridgingwhen the real issue is what SwiftUI renders or drops. - Use
/skill apple-text-markdownwhen Markdown parsing is driving the attributed-text shape.