SwiftUI-first rich text rendering with UIKit-backed fast paths for AI response streaming in Remodex.
RemodexTextKit is a Remodex-focused rework of Textual, built for the way an AI coding app actually renders text: large assistant messages, token-by-token updates, selectable/copyable output, code blocks, Markdown, and long sessions where UI freezes are not acceptable. The public API stays SwiftUI-first, while the expensive live-rendering paths use UIKit internally on iOS-family platforms.
The package keeps Textual's rich SwiftUI text model where it works well, then adds a lighter UIKit-backed path for the parts that are expensive during real streaming: appending text, measuring growing content, rendering settled fragments, and keeping native selection/copy responsive. It is intended to be used in Remodex as the text rendering layer for AI responses.
RemodexTextKit is also still the spiritual successor to MarkdownUI, reimagined from the ground up to address the lessons learned from community feedback. While MarkdownUI focuses on Markdown rendering, RemodexTextKit is designed as a SwiftUI text rendering engine that happens to support Markdown. This shift in perspective influenced every design decision.
RemodexTextKit preserves SwiftUI's Text rendering pipeline so you can get performance, composability, and automatic
platform adaptations. The rendering flow transforms markup into attributed content, resolves attachments asynchronously,
applies styling through environment values, and uses SwiftUI's layout system to position everything.
- Remodex-ready AI response streaming through
StreamingText - Specialized views with
InlineTextfor inline-formatted text andStructuredTextfor block-based documents - Native text selection with proper copy-paste support
- Markdown support via Foundation's
AttributedStringbuilt-in parser - UIKit-backed streaming text for high-frequency AI response updates on iOS-family platforms
- Delta append support with
appendedMarkdown, avoiding repeated full-string prefix scans during token bursts - Coalesced UIKit updates to reduce SwiftUI invalidation pressure while a model response is still arriving
- Optimized settled fragments for large AI answers, code blocks, and selectable rich text
- Custom markup parser support through the
MarkupParserprotocol - Inline attachments that flow with the text, such as images and custom emoji
- Math expressions rendered as inline or block attachments
- Animated image support (GIF, APNG, WebP)
- Syntax highlighting with customizable themes
- Comprehensive styling for headings, code blocks, tables, links, lists, and more
- Font-relative layout measurements that scale with text size and accessibility settings
For inline content with formatting, images, and links, use InlineText:
InlineText(
markdown: """
This is a *lighthearted* but **perfectly serious** paragraph where `inline code` lives \
happily alongside ~~a terrible idea~~ a better one, a [useful link](https://example.com), \
and a bit of _extra emphasis_ just for style. To keep things interesting without overdoing \
it, here’s a completely random image that adapts to the container width:

"""
)This creates a view that renders formatted text and flows naturally within its container. InlineText is a drop-in
replacement for SwiftUI's Text with attachment support and comprehensive styling.
You can customize InlineText with standard SwiftUI modifiers (like .font() and .foregroundStyle()) or use
RemodexTextKit's inline styling system:
InlineText(
markdown: "Use `git status` to check _uncommitted changes_"
)
.font(.custom("Avenir Next", size: 18))
.remodex.inlineStyle(
InlineStyle()
.code(
.monospaced,
.fontScale(0.85),
.backgroundColor(.purple),
.foregroundColor(.white)
)
.emphasis(.italic, .underlineStyle(.single))
)For structured content with headings, paragraphs, lists, code blocks, and tables, use StructuredText:
StructuredText(
markdown: """
## The Problem
> After merging PR #347, users reported that tapping "Back" from the detail view would sometimes
> navigate to a completely random screen. One user ended up in Settings while trying to return to
> their inbox. Another saw the onboarding flow. Creative, but not ideal.
Here's what we knew going in:
- The issue only appeared **after** the state restoration changes
- It happened _inconsistently_—maybe 1 in 5 back navigations
- The stack trace was... let's call it "unhelpful"
"""
)This renders a heading, a blockquote, a paragraph, and a bulleted list with appropriate spacing and styling. Each block can be customized independently.
For token-by-token AI output, use StreamingText while the response is still arriving, then flip
isStreaming to false when the final message is available:
StreamingText(
markdown: responseText,
appendedMarkdown: latestDelta,
isStreaming: responseIsRunning
)On UIKit platforms this uses a non-editable UITextView, coalesces token bursts, and appends the
optional appendedMarkdown delta without scanning the full response. When isStreaming becomes
false, rendering switches to StructuredText with optimized UIKit text fragments for plain rich
text and code blocks. Native selection and copy stay available without running RemodexTextKit's full
SwiftUI text-fragment layout on every token.
This is the path Remodex should use for live assistant messages. It keeps the hot loop small:
- while streaming, UIKit mutates text storage directly instead of rebuilding a deep SwiftUI view tree per token
appendedMarkdownlets the caller pass only the latest delta when available- updates are coalesced so bursts of model tokens do not create a render pass for every tiny chunk
- intrinsic-size measurements are cached so layout does not repeatedly recompute the same growing text
- selection and copy use native text-view behavior, so users can copy output without freezing the response UI
If you already own the streaming state outside RemodexTextKit, you can also opt a final StructuredText
render into the same UIKit fragment path:
StructuredText(markdown: responseText)
.remodex.optimizedTextFragments(isSelectable: true)RemodexTextKit ships with Markdown support built on top of Foundation's AttributedString markdown parser, but you can
plug in any format that can produce strings with PresentationIntent
attributes by conforming your parser to the MarkupParser protocol.
The built-in Markdown parser supports syntax extensions, like custom emoji. You can define emoji with shortcodes that will be substituted after parsing:
let emoji: Set<Emoji> = [
Emoji(
shortcode: "rocket",
url: URL(string: "https://example.com/rocket.png")!
),
Emoji(
shortcode: "sparkles",
url: URL(string: "https://example.com/sparkles.gif")!
),
]
InlineText(
markdown: "Shipped the new feature :rocket: and it's working :sparkles:",
syntaxExtensions: [.emoji(emoji)]
)Math expressions are also supported when you include .math in syntaxExtensions:
StructuredText(
markdown: "The area is $A = \\pi r^2$.",
syntaxExtensions: [.math]
)You can control whether users can select text within InlineText or StructuredText views with the
textual.textSelection(_:) modifier:
StructuredText(
markdown: """
## The Problem
...
"""
)
.remodex.textSelection(.enabled)Scrollable regions like code blocks handle their own selection contexts. When you select text in a scrollable area, any document-level selection clears automatically, and vice versa.
Each StructuredText coordinates selection only within itself. When you render many of them in one
container — a chat timeline, for example — apply textual.textSelectionScope() to the container so
starting a selection in one view clears the selection in the others, keeping at most one active
selection across the whole subtree:
ScrollView {
VStack {
ForEach(messages) { message in
StructuredText(markdown: message.text)
.remodex.textSelection(.enabled)
}
}
.remodex.textSelectionScope()
}RemodexTextKit provides a flexible styling system that lets you customize every aspect of structured text rendering. At the highest level, you can apply a complete style preset with a single modifier. For finer control, you can override individual block types or create fully custom styles.
RemodexTextKit includes a complete .default style preset. Apply it using the
textual.structuredTextStyle(_:) modifier:
StructuredText(
markdown: """
## The Problem
...
"""
)
.remodex.structuredTextStyle(.default)This single modifier configures the entire rendering stack: inline styles (code, emphasis, strong, links), block styles (headings, paragraphs, blockquotes, code blocks, tables), and list markers.
You can override specific aspects of a style without rebuilding everything. Each block type has its own modifier:
StructuredText(markdown: content)
.remodex.structuredTextStyle(.default)
.remodex.headingStyle(
CustomHeadingStyle()
)
.remodex.codeBlockStyle(
CustomCodeBlockStyle()
)Here's a practical example, a custom heading style that adds a subtle underline to H1:
struct CustomHeadingStyle: StructuredText.HeadingStyle {
private static let fontScales: [CGFloat] = [2, 1.5, 1.25, 1, 0.875, 0.85]
func makeBody(configuration: Configuration) -> some View {
let headingLevel = min(configuration.headingLevel, 6)
let fontScale = Self.fontScales[headingLevel - 1]
VStack(alignment: .leading, spacing: 0) {
configuration.label
.remodex.fontScale(fontScale)
.fontWeight(.semibold)
if headingLevel == 1 {
Divider()
.remodex.padding(.top, .fontScaled(0.25))
}
}
.remodex.blockSpacing(.fontScaled(top: 1.5, bottom: 0.5))
}
}The configuration provides the rendered label and context like heading level and indentation, which you can use to build custom layouts and apply additional styling.
Notice the .fontScaled() values in the example above. RemodexTextKit's font-relative measurement system ensures your layouts
scale harmoniously with text size:
.remodex.padding(.fontScaled(1.0))
.remodex.blockSpacing(.fontScaled(top: 0.8, bottom: 1.2))These measurements adapt automatically to the current font size, dynamic type settings, and accessibility preferences.
A padding of .fontScaled(0.5) creates padding that is half of the current font size. As users adjust text size, your
spacing scales proportionally.
You may have noticed the .remodex prefix on modifiers throughout these examples. RemodexTextKit organizes its view modifiers
under this namespace, making them easy to discover through autocomplete while avoiding potential naming conflicts with
SwiftUI or other libraries. When you type .remodex, you see only RemodexTextKit-specific capabilities.
Many modifiers in the .remodex namespace accept font-relative measurements through .fontScaled() values. Beyond
padding and spacing, you can use these measurements for frame sizes, insets, and any numeric value where scaling with
text size makes sense.
For full control, implement the StructuredText.Style protocol. This lets you define every aspect of rendering in one
cohesive theme:
struct CompactStyle: StructuredText.Style {
var inlineStyle: InlineStyle {
InlineStyle()
.code(.monospaced, .fontScale(0.9))
.strong(.fontWeight(.semibold))
}
var headingStyle: some StructuredText.HeadingStyle {
CompactHeadingStyle()
}
var paragraphStyle: some StructuredText.ParagraphStyle {
CompactParagraphStyle()
}
// ... other block styles
var unorderedListMarker: StructuredText.UnorderedListMarker {
.hierarchical(.disc, .circle, .square)
}
var orderedListMarker: StructuredText.OrderedListMarker {
.decimal
}
}
// Then apply it:
StructuredText(markdown: content)
.remodex.structuredTextStyle(CompactStyle())The protocol requires implementations for all block types, list markers, and inline styles. This ensures visual consistency across your entire document.
This repository includes a demo app that showcases all of RemodexTextKit's features, from inline formatting and custom emoji to advanced styling and syntax highlighting. Each feature is demonstrated in focused, isolated examples that are easy to explore and reference.
The demo lives in Examples/RemodexTextKitDemo and is included in RemodexTextKit.xcworkspace at the
repository root. Open the workspace to browse the library source and run the demo side-by-side.
The latest documentation for RemodexTextKit is available here.
You can add RemodexTextKit to an Xcode project by adding it to your project as a package.
If you want to use RemodexTextKit in a SwiftPM project, it's as
simple as adding it to your Package.swift:
dependencies: [
.package(url: "https://github.com/Emanuele-web04/RemodexTextKit", from: "0.1.0")
]And then adding the product to any target that needs access to the library:
.product(name: "RemodexTextKit", package: "RemodexTextKit"),This library is released under the MIT license. See LICENSE for details.