When integrated into an iOS project, this library renders Contentful RichText to NSAttributedString
with native UIView
embedded in the text. This is made possible because this library is built on top of TextKit, and provides a powerful plugin system to render Contentful entries and assets that are embedded in RichText
.
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.
platform :ios, '11.0'
use_frameworks!
pod 'ContentfulRichTextRenderer'
You can also use Carthage for integration by adding the following to your Cartfile
:
github "contentful/rich-text-renderer.swift"
And then run the following command in the terminal:
carthage update --platform iOS --use-xcframeworks
Add all the XCFrameworks from Cartfile/Build
directory into your project manually.
The main entry point for the library is the RichTextViewController
. You can either use it standalone, or subclass it. The view
instance for RichTextViewController
has a UITextView
subview that uses a custom NSLayoutManager
and NSTextContainer
to lay out text, enabling text to wrap around nested views for embedded assets and entries, and also enabling blockquote styling analogous to that seen on websites.
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):
break
}
}
}
}
You can configure how the renderer renders content by modifying DefaultRendererConfiguration
instance or by creating new one conforming to RendererConfiguration
protocol.
Initializing instance of DefaultRendererConfiguration
provides a configuration with sane defaults. Simply create an instance, then mutate it to further customize the rendering.
ResourceLinkBlock
node.You can render custom views for ResourceLinkBlock
nodes by passing a view provider to the configuration object.
var configuration = DefaultRendererConfiguration()
configuration.resourceLinkBlockViewProvider = ExampleBlockViewProvider()
Below is the example implementation on the view provider that is cappable of rendering Car
model and the Asset
.
struct ExampleViewProvider: 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] = [:]
public 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) {}
}
ResourceLinkInline
node.Inline elements in the RichTextDocument
can be rendered as NSMutableAttributedString
object.
var configuration = DefaultRendererConfiguration()
configuration.resourceLinkInlineStringProvider = ExampleInlineStringProvider()
Below is the implementation of the inline string provider that renders Cat
type.
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 CatInlineProvider.string(for: cat)
}
default:
break
}
return NSMutableAttributedString(string: "")
}
}
private final class CatInlineProvider {
static func string(for cat: Cat) -> NSMutableAttributedString {
return NSMutableAttributedString(
string: "🐈 \(cat.name) ❤️",
attributes: [
.foregroundColor: UIColor.rtrLabel
]
)
}
}
The library has been implemented the way that you can provide custom renderers for specific node types.
Simply create a class that inherits from one of the default renderers and write your own. Then attach to a DefaultRenderersProvider
and that's it.
final class ExampleParagraphRenderer: ParagraphRenderer {
typealias NodeType = Paragraph
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)
The best way to get acquainted with using this library in an iOS app is to check out the example that is a part of this repository. In particular, pay attention to the view provider and inline provider in order to learn how to render entries and assets that are embedded in the rich text.
This repository is published under the MIT license.
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.