Skip to content

Content Modeling in Apple

Learn how to handle different content-type and nestable blocks, render rich text, and use story references to manage content globally.

In the existing space, create the following blocks:

  • An article content type block with the following fields:
    • title: Text
    • content: Rich text
  • A featured-articles nestable block with the following field:
    • articles: References

Next, create an Articles folder, open it, and create a few stories that use the article content type.

Finally, add the featured-articles block to the body field of the Home story, and select the created articles to feature.

Update the BlockLibrary.swift file to add cases for the new blocks.

Example/BlockLibrary.swift
import StoryblokClient
@BlockLibrary
enum Block: Decodable, Hashable {
case feature(name: String)
case grid(columns: [Block])
case page(body: [Block])
case teaser(headline: String)
case article(Article)
case featuredArticles(articles: [Story<Article>])
case unknown
enum CodingKeys: String, CodingKey {
case featuredArticles = "featured-articles"
}
nonisolated struct Article: Decodable, Hashable {
let title: String
let content: RichText<Block>
}
}

The fields of the article and featured-articles blocks are defined as follows:

  • Article.content is defined as RichText<Block> which can represent any rich text node (a document in this case).
  • featuredArticles.articles is defined as [Story<Article>] which will cause the referenced stories to be resolved and returned. To receive only the UUIDs of the linked stories change the type of the field to [UUID].
  • The special nested enumeration CodingKeys is declared to map the block’s technical name, featured-articles, which is not a valid Swift identifier, to the featuredArticles case.
  • Instead of labeled associated values for the individual fields of the block, the added article case declares the nested Article struct as a single unnamed associated value. This allows the sharing of the Article type with the featuredArticles case.

Next, add views for these blocks to the View conformance in BlockView.swift.

Example/BlockView.swift
import SwiftUI
import StoryblokClient
import RichTextView
extension Block: View {
var body: some View {
switch self {
case let .page(body):
LazyVStack(alignment: .leading, spacing: 0) {
ForEach(body, id: \.self) { $0 }
}
case let .featuredArticles(articles):
VStack(alignment: .leading, spacing: 8) {
Text("Featured Articles")
.font(.title2)
ForEach(articles, id: \.self) { article in
NavigationLink(article.content.title, value: article)
}
}
.padding()
case let .article(article):
article // Article conforms to View (below)
case .unknown: Text("Unknown block type")
}
}
}
extension Block.Article: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text(title)
.font(.largeTitle)
content
}
.padding()
}
}

Views for featuredArticles and article are added alongside the existing blocks:

  • The featuredArticles view renders links to the featured articles, each a NavigationLink carrying the article Story instance for immediate display of the selected article.
  • The article case returns the Article struct itself, whose View conformance is added in the extension below which places its rich text content directly in the view hierarchy, where it is rendered by the RichTextView library.

Finally, update ExampleApp.swift to add a navigation destination for article stories, and the onStoryLink modifier to handle any story links present in the rich text content.

Example/ExampleApp.swift
import SwiftUI
import StoryblokClient
import RichTextView
internal import URLSessionExtension
.navigationDestination(for: Story<Block>.self) { story in
StoryView(client.story(story.uuid), story: story)
}
.navigationDestination(for: Story<Block.Article>.self) { story in
StoryView(client.story(story.uuid), story: story)
}
}
.onStoryLink { uuid, _ in path.append(uuid) }
}

Run the app and see the home story with the links to the featured articles rendered. Tapping any link navigates to the article.

Was this page helpful?

What went wrong?

This site uses reCAPTCHA and Google's Privacy Policy (opens in a new window).Terms of Service (opens in a new window) apply.