Trust these instructions first. Only search the repository if information here is incomplete or wrong.
SwiftStreamingMarkdown is a Swift Package that renders Markdown in SwiftUI. It is consumed by Apple-platform apps that need to display Markdown content, either bespoke or incrementally produced by an LLM or other streaming source. The package is iOS-first, distributed via Swift Package Manager only, and ships with a sample app under Examples/.
Key technologies:
| Topic | Value |
|---|---|
| Language | Swift |
| UI | SwiftUI (some UIKit interop under Sources/MarkdownText/UI/UIKit/) |
| swift-tools-version | 5.9 |
| Minimum Xcode | 16.0 (the package contains @available(iOS 18.0, *) annotations that require the iOS 18 SDK) |
| Minimum iOS deployment | iOS 16 |
| Build system | Swift Package Manager (no Bazel, no CocoaPods) |
| Linter | SwiftLint (config: .swiftlint.yml, run via swiftlint --strict) |
SwiftStreamingMarkdown/
├── Makefile # Common local development commands
├── Package.swift # SPM manifest — single library target
├── Sources/
│ └── MarkdownText/ # The library target
│ ├── Block/ # Block-level Markdown rendering
│ ├── Inline/ # Inline-level Markdown rendering
│ ├── Citation/ # Inline citation handling
│ ├── Style/ # Colors, fonts, typography
│ ├── TextTransition/ # iOS 18+ FadeInTextTransition
│ ├── UI/ # SwiftUI views (CodeBlockView, TableView, etc.)
│ │ └── UIKit/ # UIKit interop (ParagraphUIView, etc.)
│ ├── Utilities/ # Bundle, URL, String helpers
│ └── Resources/ # Assets.xcassets, Media.xcassets (Bundle.module)
├── Tests/
│ └── MarkdownTextTests/ # XCTest + swift-snapshot-testing
├── Examples/
│ └── SwiftStreamingMarkdownSample/ # Sample iOS app + XcodeGen project.yml
├── .agents/skills/ # Repo-scoped Copilot skills (pr-writer, snapshot-tests)
├── scripts/
│ └── dev-setup.sh # One-time local tooling check
├── .github/workflows/ci.yml # SwiftLint + SPM unit tests + sample-app build
├── .swiftlint.yml # Lint rules
├── .xcode-version # Minimum Xcode version, read by dev-setup.sh
└── CONTRIBUTING.md # Contributor guide
The major components of this library are parsing and rendering. They are highly isolated to make sure code is executed on the right threads and is easy to contribute to.
Perform text-level processing before sending it to the markdown parser, if needed. The library uses it to recognize math syntax and convert it to a format the markdown parser understands. This works in most cases but is not ideal — it's on the roadmap to include math parsing as part of the markdown parsing itself.
This is based on Apple's open-source parser from swift-markdown, which is backed by cmark-gfm.
Perform markdown AST-level manipulation before passing the result to the UI layer. The library uses this to speculatively close half-typed emphasis — for example, a streaming chunk Yeah, this is *cool gets rewritten to render as if it were Yeah, this is **cool**, so the text doesn't jitter back and forth as the rest of the token streams in.
This step converts the markdown AST (Document) into a RenderableDocument for rendering. It translates markdown styles into Apple's language, mainly around NSAttributedString, NSTextAttachment, etc.
The RenderableDocument is then passed to the SwiftUI/UIKit layer to render on iOS devices. Most of the UI components are written in SwiftUI except for paragraphs. We chose UIKit's UITextView to render paragraphs to ensure the library can support streamed markdown with fine-grained animation control and high performance.
Behavioral guidelines for agents (and humans) working in this repo. These bias toward caution over speed; for trivial tasks, use judgment.
Don't assume. Don't hide confusion. Surface tradeoffs.
- State assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them — don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop, name what's confusing, and ask.
Minimum code that solves the problem. Nothing speculative.
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
Touch only what you must. Clean up only your own mess.
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it — don't delete it.
- Remove imports/variables/functions that your changes made unused; don't remove pre-existing dead code unless asked.
The test: every changed line should trace directly to the user's request.
Define success criteria. Loop until verified.
- "Add validation" → "Write tests for invalid inputs, then make them pass."
- "Fix the bug" → "Write a test that reproduces it, then make it pass."
- "Refactor X" → "Ensure tests pass before and after."
For multi-step tasks, state a brief plan with a verification step per item.
Names should be grammatical, concise, and accurate. Avoid abbreviations and Boolean-naming mistakes (isFooEnabled, not isEnableFoo).
// ❌ Not preferred
var data: String
func process() -> Void
var usrNm: String
var isEnableFoo: Bool
// ✅ Preferred
var userName: String
var profileImageURL: URL
func processLoginCredentials() -> Void
var isFooEnabled: Bool- Prefer
async/awaitandTask. AvoidDispatchQueue/OperationQueueexcept when bridging legacy callbacks. - Use
actorfor shared mutable state owned by the library. - Do not sweep
@MainActorisolation across non-UI types. Only views and types that mutate observable view state need to be main-actor isolated. - This package's streaming pipeline runs on a background task and pushes rendered output to the main actor at the boundary; do not move that boundary inward without measuring.
- Code must not block the main thread with sleeps, semaphore waits, busy polling etc.
- Do not perform heavy operations inside SwiftUI view bodies — precompute upstream of the view.
Streaming input is modeled as AsyncSequence. Prefer Swift's standard concurrency primitives and AsyncStream/AsyncSequence operators from the standard library over bridging to Combine.
For new async subscriptions inside view-scoped code, bind the Task lifecycle to the owning type rather than creating an orphan Task. If a background loop must outlive a single render, factor it into a type with deterministic teardown.
- One primary type per source file. Nested helper types (
enum State,struct Configuration) belong inside the parent type when they are only meaningful in that context. - Group related extensions in
Type+Feature.swiftfiles (e.g.Colors+Theme.swift,String+.swift).
// ✅ Preferred — State is meaningful only inside the view model
final class FooViewModel: ObservableObject {
enum State { case loading, ready, error(Error) }
@Published var state: State = .loading
}- The unit-test target is
SwiftStreamingMarkdownTestsatTests/MarkdownTextTests, built on XCTest. - Snapshot tests use
pointfreeco/swift-snapshot-testing. See the.agents/skills/snapshot-tests/SKILL.mdskill for how to record, validate, and visually diff snapshots (diff-imagefrom ImageMagick).
make dev-setupThis verifies Homebrew, Xcode ≥ .xcode-version, SwiftLint, and XcodeGen are present. For optional snapshot diff helpers it can install ImageMagick via Homebrew and offers to download diff-image into ~/.local/bin.
# Show all Make targets
make help
# Resolve Swift package dependencies and open the package in Xcode
make project
# Generate the sample app project
make generate-sample-project
# Generate and open the sample app project in Xcode
make sample-project
# Count code using cloc's Git file discovery
make cloc# Lint
make lint
# Run the package unit tests
make test
# Build the sample app
make build-sample