rich-text-renderer.swift - Native Rich Text Rendering for Contentful
August 26, 2026 · View on GitHub
rich-text-renderer.swift - Native Rich Text Rendering for Contentful
Renders Contentful Rich Text fields to
NSAttributedString, with nativeUIViews embedded directly in the text. Built on top of TextKit and the official contentful.swift library, it provides a powerful plugin system for rendering the Contentful entries and assets embedded in your rich text.
What is Contentful?
Contentful provides content infrastructure for digital teams to power websites, apps, and devices. Unlike a CMS, Contentful was built to integrate with the modern software stack. It offers a central hub for structured content, powerful management and delivery APIs, and a customizable web app that enable developers and content creators to ship their products faster.
Table of contents
Core Features
- Renders
Contentful.RichTextDocumenttoNSAttributedString, displayed via a ready-to-useRichTextViewControllerbuilt onTextKit. - Native
UIViews wrap text for embedded block-level entries and assets (ResourceLinkBlock), with word-wrap-aware layout via a customNSLayoutManager/NSTextContainer. - Pluggable rendering for embedded content: implement
ResourceLinkBlockViewProvidingfor block-level entries/assets andResourceLinkInlineStringProvidingfor inline entries, so your ownEntryDecodabletypes render as first-class views or styled text runs. - Override any node renderer (paragraphs, headings, lists, block quotes, hyperlinks, tables, and more) by subclassing the default renderer and attaching it to a
DefaultRenderersProvider. - Built-in table rendering with a
SimpleTableViewimplementation, or supply your own. - Blockquote styling with a configurable rule/rectangle, drawn via a custom layout manager decoration — matching the look of blockquotes on the web.
- Fully configurable typography via
StyleProviding(regular/bold/italic/bold-italic/monospace fonts and colors, heading styles, hyperlink color), plusTextConfiguration,BlockQuoteConfiguration, andTextListConfiguration. - Automatic dark-mode-aware default colors (
UIColor.rtrLabel,UIColor.rtrSystemBackground). - Callbacks for intercepting taps on URL hyperlinks (
onHyperlinkPressed) and links to Contentful entries/assets (onResourceHyperlinkPressed). - Ships with a privacy manifest for App Store submissions.
Getting started
Requirements
| Requirement | Version |
|---|---|
| Swift | 5.2 or later |
| iOS | 13.0+ |
The SDK depends on contentful.swift and AlamofireImage (used for loading and caching images for ResourceLinkBlockImageView).
Installation
Swift Package Manager
Swift Package Manager is the recommended way to integrate the SDK. In Xcode, choose File > Add Package Dependencies… and enter https://github.com/contentful/rich-text-renderer.swift, or add the dependency to your Package.swift manifest:
.package(url: "https://github.com/contentful/rich-text-renderer.swift", from: "0.4.10")
Then add the product to the targets that need it:
.target(
name: "MyApp",
dependencies: [
.product(name: "ContentfulRichTextRenderer", package: "rich-text-renderer.swift")
]
)
The library's module is named RichTextRenderer when installed via Swift Package Manager, so import it as:
import RichTextRenderer
CocoaPods
platform :ios, '13.0'
use_frameworks!
pod 'ContentfulRichTextRenderer', '~> 0.4.10'
When installed via CocoaPods, the module name matches the pod name, so import it as:
import ContentfulRichTextRenderer
Carthage
Add the following to your Cartfile:
github "contentful/rich-text-renderer.swift" ~> 0.4.10
Then build the XCFrameworks:
carthage update --platform iOS --use-xcframeworks
Add all the XCFrameworks from the Carthage/Build directory to your project manually.
Your first render
The main entry point for the library is RichTextViewController. Use it standalone, or subclass it as shown below. Its view hosts a UITextView subview backed by a custom NSLayoutManager/NSTextContainer, so text wraps around embedded views and blockquotes render with the familiar rule-and-indent styling:
import Contentful
import RichTextRenderer
import UIKit
class ViewController: RichTextViewController {
private let client = ContentfulService() // Your service fetching data from Contentful.
init() {
// Default configuration of the renderer.
let configuration = DefaultRendererConfiguration()
let renderer = RichTextDocumentRenderer(configuration: configuration)
super.init(renderer: renderer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
fetchContent()
}
private func fetchContent() {
client.fetchArticle { [weak self] result in
switch result {
case .success(let article):
self?.richTextDocument = article.content
case .failure(let error):
print(error)
}
}
}
}
Setting richTextDocument triggers rendering automatically — it's safe to set from any thread, since rendering is always dispatched to the main queue.
Using the SDK
Renderer configuration
DefaultRendererConfiguration provides sane defaults; create an instance and mutate its properties to customize rendering, or provide your own type conforming to RendererConfiguration:
var configuration = DefaultRendererConfiguration()
configuration.contentInsets = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
Rendering views for ResourceLinkBlock nodes
Render custom views for block-level embedded entries and assets by passing a view provider to the configuration:
var configuration = DefaultRendererConfiguration()
configuration.resourceLinkBlockViewProvider = ExampleBlockViewProvider()
Below is an example view provider capable of rendering a Car model and an image Asset:
import Contentful
import RichTextRenderer
import UIKit
struct ExampleBlockViewProvider: ResourceLinkBlockViewProviding {
func view(for resource: Link, context: [CodingUserInfoKey: Any]) -> ResourceLinkBlockViewRepresentable? {
switch resource {
case .entryDecodable(let entryDecodable):
if let car = entryDecodable as? Car {
return CarView(car: car)
}
return nil
case .entry:
return nil
case .asset(let asset):
guard asset.file?.details?.imageInfo != nil else { return nil }
let imageView = ResourceLinkBlockImageView(asset: asset)
imageView.backgroundColor = .gray
imageView.setImageToNaturalHeight()
return imageView
default:
return nil
}
}
}
final class CarView: UIView, ResourceLinkBlockViewRepresentable {
private let car: Car
var surroundingTextShouldWrap: Bool = false
var context: [CodingUserInfoKey: Any] = [:]
init(car: Car) {
self.car = car
super.init(frame: .zero)
let title = UILabel(frame: .zero)
title.text = "🚗 " + car.model + " 🚗"
title.translatesAutoresizingMaskIntoConstraints = false
addSubview(title)
title.topAnchor.constraint(equalTo: topAnchor).isActive = true
title.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
title.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
title.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
title.sizeToFit()
frame = title.bounds
backgroundColor = .lightGray
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func layout(with width: CGFloat) {}
}
ResourceLinkBlockImageView is a ready-made ResourceLinkBlockViewRepresentable for image assets — it loads and caches the image with AlamofireImage and lays itself out to the asset's natural aspect ratio via setImageToNaturalHeight().
Rendering ResourceLinkInline nodes
Inline embedded entries can be rendered as an NSMutableAttributedString:
var configuration = DefaultRendererConfiguration()
configuration.resourceLinkInlineStringProvider = ExampleInlineStringProvider()
import Contentful
import RichTextRenderer
import UIKit
final class ExampleInlineStringProvider: ResourceLinkInlineStringProviding {
func string(
for resource: Link,
context: [CodingUserInfoKey: Any]
) -> NSMutableAttributedString {
switch resource {
case .entryDecodable(let entryDecodable):
if let cat = entryDecodable as? Cat {
return NSMutableAttributedString(
string: "🐈 \(cat.name) ❤️",
attributes: [.foregroundColor: UIColor.rtrLabel]
)
}
default:
break
}
return NSMutableAttributedString(string: "")
}
}
Handling hyperlink taps
Intercept taps on plain URL hyperlinks, and on hyperlinks that point to a Contentful entry or asset, via configuration callbacks:
var configuration = DefaultRendererConfiguration()
configuration.onHyperlinkPressed = { urlString in
guard let url = URL(string: urlString) else { return }
UIApplication.shared.open(url)
}
configuration.onResourceHyperlinkPressed = { link in
switch link {
case .entry(let entry):
print("Navigate to entry", entry.sys.id)
case .asset(let asset):
print("Navigate to asset", asset.sys.id)
default:
break
}
}
Custom node renderers
Every node type can be rendered with your own logic. Subclass one of the default renderers and attach it to a DefaultRenderersProvider:
final class ExampleParagraphRenderer: ParagraphRenderer {
override func render(
node: Paragraph,
rootRenderer: RichTextDocumentRendering,
context: [CodingUserInfoKey: Any]
) -> [NSMutableAttributedString] {
// Your code for rendering paragraphs.
}
}
let configuration = DefaultRendererConfiguration()
var renderersProvider = DefaultRenderersProvider()
renderersProvider.paragraph = ExampleParagraphRenderer()
let renderer = RichTextDocumentRenderer(
configuration: configuration,
nodeRenderers: renderersProvider
)
super.init(renderer: renderer)
DefaultRenderersProvider exposes a renderer property for every node type: blockQuote, heading, horizontalRule, hyperlink, listItem, orderedList, paragraph, resourceLinkBlock, resourceLinkInline, text, unorderedList, table, tableRow, tableRowCell, and tableRowHeaderCell.
Tables
Table, TableRow, and table cell nodes render out of the box using SimpleTableView. No configuration is required, but you can override TableRenderer (and its row/cell counterparts) the same way as any other node renderer if you need a custom table layout.
Advanced configuration
Styling text, headings, and lists
DefaultStyleProvider supplies fonts and colors for every text variant. Provide your own to customize the base font, monospace font, or hyperlink color:
let styleProvider = DefaultStyleProvider(
baseFont: .systemFont(ofSize: 16),
baseColor: .label,
monospacedFont: .monospacedSystemFont(ofSize: 15, weight: .regular),
hyperlinkColor: .systemBlue
)
var configuration = DefaultRendererConfiguration()
configuration.styleProvider = styleProvider
For a fully custom typography system, conform to StyleProviding directly. TextConfiguration controls paragraph and line spacing; BlockQuoteConfiguration controls the rule color, width, and text inset of block quotes; TextListConfiguration controls indentation of ordered/unordered lists:
var configuration = DefaultRendererConfiguration()
configuration.textConfiguration = TextConfiguration(paragraphSpacing: 12, lineSpacing: 2)
configuration.blockQuote = BlockQuoteConfiguration(rectangleColor: .systemGray3, rectangleWidth: 4, textInset: 16)
configuration.textList = TextListConfiguration(indentationMultiplier: 18, distanceToListItem: 22)
Dark mode
UIColor.rtrLabel and UIColor.rtrSystemBackground resolve to UIColor.label/UIColor.systemBackground on iOS 13+ (falling back to .black/.white otherwise), and are used as the defaults throughout the library, so rendered content automatically adapts to the user's Light/Dark Mode setting.
Privacy manifest
The SDK ships PrivacyInfo.xcprivacy, declaring that it collects no data, performs no tracking, and uses file-timestamp, user-defaults, and system-boot-time APIs only for the reasons Apple permits. When installed via Swift Package Manager or CocoaPods, the manifest is bundled automatically and folds into your app's privacy report.
Documentation & References
For more information about Rich Text itself, see the Rich Text concept documentation. This library is a companion to contentful.swift; consult its README for details on Client, EntryDecodable, and fetching content.
Example applications
The best way to get acquainted with this library is to check out the example apps in this repository:
Example-iOS— a UIKit app demonstratingRichTextViewController, block/inline view providers, and custom models.Example-iOS-SwiftUI— the same content rendered from SwiftUI viaUIViewControllerRepresentable.
Pay particular attention to the view provider and inline provider implementations in each example to learn how to render your own entries and assets embedded in rich text.
Reach out to us
Have questions about how to use this library?
You found a bug or want to propose a feature?
- File an issue here on GitHub:
. Make sure to remove any credential from your code before sharing it.
You need to share confidential information or have other questions?
Get involved
We appreciate any help on our repositories. For more details about how to contribute, see the contributing guide for our Swift SDKs.
License
This repository is published under the MIT license.
Code of Conduct
We want to provide a safe, inclusive, welcoming, and harassment-free space and experience for all participants, regardless of gender identity and expression, sexual orientation, disability, physical appearance, socioeconomic status, body size, ethnicity, nationality, level of experience, age, religion (or lack thereof), or other identity markers.