A modern, powerful, and type-safe SwiftUI component for rendering HTML content with extensive styling options, async/await support, media interaction, and comprehensive error handling. Built for Swift 5.9+ and optimized for iOS 15.0+ and macOS 12.0+.
![]() |
![]() |
---|---|
Light Mode | Dark Mode |
- β¨ Features
- π Quick Start
- π¦ Installation
- π Complete API Reference
- π What's New in v3.0.0
- π§ Advanced Usage
- π‘ Examples
- π Troubleshooting
- π Migration Guide
- π€ Contributing
- β‘ Async/Await Support: Modern Swift concurrency for better performance
- π‘οΈ Type Safety: Comprehensive Swift type safety with robust error handling
- π§ͺ Swift Testing: Modern testing framework with extensive test coverage
- π§ Backward Compatible: 100% compatibility with v2.x while providing modern APIs
- π± Cross-platform: iOS 15.0+ and macOS 12.0+ with Swift 5.9+
- π¨ Theme Support: Automatic light/dark mode with custom color schemes
- π€ Typography: System fonts, custom fonts, monospace, italic, and Dynamic Type support
- πΌοΈ Interactive Media: Click events for images/videos with custom handling
- π Smart Link Management: Safari, SFSafariView, and custom link handlers
- π¨ Advanced Styling: Type-safe background colors, CSS customization
- π Responsive Layout: Dynamic height calculation with smooth transitions
- π Loading States: Configurable placeholders with animation support
- π HTML5 Complete: Full support for modern semantic elements
- π¨ Error Handling: Comprehensive error types with custom callbacks
The simplest way to get started with RichText:
import SwiftUI
import RichText
struct ContentView: View {
let htmlContent = """
<h1>Welcome to RichText</h1>
<p>A powerful HTML renderer for SwiftUI.</p>
"""
var body: some View {
ScrollView {
RichText(html: htmlContent)
}
}
}
Add styling, media handling, and error handling:
struct ContentView: View {
let htmlContent = """
<h1>Welcome to RichText</h1>
<p>A powerful HTML renderer with <strong>extensive customization</strong>.</p>
<img src="https://via.placeholder.com/300x200" alt="Sample Image">
<p><a href="https://github.com/NuPlay/RichText">Visit our GitHub</a></p>
"""
var body: some View {
ScrollView {
RichText(html: htmlContent)
.colorScheme(.auto) // Auto light/dark mode
.lineHeight(170) // Line height percentage
.imageRadius(12) // Rounded image corners
.transparentBackground() // Transparent background
.placeholder { // Loading Placeholder
Text("Loading email...")
}
.onMediaClick { media in // Handle media clicks
switch media {
case .image(let src):
print("Image clicked: \(src)")
case .video(let src):
print("Video clicked: \(src)")
}
}
.onError { error in // Handle errors
print("RichText error: \(error)")
}
}
}
}
- In Xcode, select File β Add Package Dependencies...
- Enter the repository URL:
https://github.com/NuPlay/RichText.git
- Select version rule: "Up to Next Major Version" from "3.0.0"
- Click Add Package
Add RichText to your Package.swift
:
dependencies: [
.package(url: "https://github.com/NuPlay/RichText.git", .upToNextMajor(from: "3.0.0"))
],
targets: [
.target(
name: "YourTarget",
dependencies: ["RichText"]
)
]
// Basic initializer
RichText(html: String)
// With configuration
RichText(html: String, configuration: Configuration)
// With placeholder
RichText(html: String, placeholder: AnyView?)
// Full initializer
RichText(html: String, configuration: Configuration, placeholder: AnyView?)
// Recommended approaches (v3.0.0+)
.transparentBackground() // Transparent (default)
.backgroundColor(.system) // System default (white/black)
.backgroundColorHex("FF0000") // Hex color
.backgroundColorSwiftUI(.blue) // SwiftUI Color
.backgroundColor(.color(.green)) // Using BackgroundColor enum
// Legacy approach (still works, but deprecated)
.backgroundColor("transparent") // Deprecated but backward compatible
// Font configuration
.fontType(.system) // System font (default)
.fontType(.monospaced) // Monospaced font
.fontType(.italic) // Italic font
.fontType(.customName("Helvetica")) // Custom font by name
.fontType(.custom(UIFont.systemFont(ofSize: 16))) // Custom UIFont (iOS only)
// Text colors - Modern API (v3.0.0+)
.textColor(light: .primary, dark: .primary) // Modern semantic naming
// Legacy text colors (deprecated but supported)
.foregroundColor(light: .primary, dark: .primary) // SwiftUI Colors (deprecated)
.foregroundColor(light: UIColor.black, dark: UIColor.white) // UIColors (deprecated)
.foregroundColor(light: NSColor.black, dark: NSColor.white) // NSColors (deprecated)
// Link colors
.linkColor(light: .blue, dark: .cyan) // SwiftUI Colors
.linkColor(light: UIColor.blue, dark: UIColor.cyan) // UIColors
// Color enforcement
.colorPreference(forceColor: .onlyLinks) // Force only link colors (default)
.colorPreference(forceColor: .all) // Force all colors
.colorPreference(forceColor: .none) // Don't force any colors
.lineHeight(170) // Line height percentage (default: 170)
.imageRadius(12) // Image border radius in points (default: 0)
.colorScheme(.auto) // .auto (default), .light, .dark
.forceColorSchemeBackground(true) // Force background color override
.linkOpenType(.Safari) // Open in Safari (default)
.linkOpenType(.SFSafariView()) // Open in SFSafariViewController (iOS)
.linkOpenType(.SFSafariView( // Advanced SFSafariView config
configuration: config,
isReaderActivated: true,
isAnimated: true
))
.linkOpenType(.custom { url in // Custom link handler
// Handle URL yourself
})
.linkOpenType(.none) // Don't handle link taps
// Loading placeholders (Modern approach - recommended)
.placeholder { // Custom placeholder view
HStack(spacing: 8) {
ProgressView()
.scaleEffect(0.8)
Text("Loading content...")
.foregroundColor(.secondary)
}
.frame(minHeight: 60)
}
// Deprecated methods (still supported for backward compatibility)
.loadingPlaceholder("Loading...") // Deprecated - use placeholder {}
.loadingText("Please wait...") // Deprecated - use placeholder {}
// Loading transitions
.loadingTransition(.fade) // Fade transition
.loadingTransition(.slide) // Slide transition
.loadingTransition(.scale) // Scale transition
.loadingTransition(.custom(.easeInOut)) // Custom animation
.transition(.easeOut) // Legacy transition method
// Media click events (v3.0.0+)
.onMediaClick { media in
switch media {
case .image(let src):
// Handle image clicks
presentImageViewer(src)
case .video(let src):
// Handle video clicks
presentVideoPlayer(src)
}
}
// Error handling (v3.0.0+)
.onError { error in
switch error {
case .htmlLoadingFailed(let html):
print("Failed to load HTML: \(html)")
case .webViewConfigurationFailed:
print("WebView configuration failed")
case .cssGenerationFailed:
print("CSS generation failed")
case .mediaHandlingFailed(let media):
print("Media handling failed: \(media)")
}
}
// Custom CSS
.customCSS("""
p { margin: 10px 0; }
h1 { color: #ff6b6b; }
img { box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
""")
// Base URL for relative resources
.baseURL(Bundle.main.bundleURL)
For complex configurations, create a Configuration
object:
let config = Configuration(
customCSS: "body { padding: 20px; }",
supportsDynamicType: true, // Enable Dynamic Type
fontType: .system,
fontColor: ColorSet(light: "333333", dark: "CCCCCC"),
lineHeight: 180,
colorScheme: .auto,
forceColorSchemeBackground: false,
backgroundColor: .transparent,
imageRadius: 8,
linkOpenType: .Safari,
linkColor: ColorSet(light: "007AFF", dark: "0A84FF", isImportant: true),
baseURL: Bundle.main.bundleURL,
mediaClickHandler: { media in /* handle clicks */ },
errorHandler: { error in /* handle errors */ },
isColorsImportant: .onlyLinks,
transition: .easeInOut(duration: 0.3)
)
RichText(html: htmlContent, configuration: config)
// Generate CSS programmatically (v3.0.0+)
let richText = RichText(html: html)
let css = richText.generateCSS(colorScheme: .light, alignment: .center)
// Generate CSS from configuration
let config = Configuration(lineHeight: 150)
let css = config.generateCompleteCSS(colorScheme: .dark)
- β‘ Async/Await Architecture: Complete rewrite using modern Swift concurrency for better performance and reliability
- π‘οΈ Enhanced Type Safety: Robust ColorSet equality comparison and validation with RGBA-based color handling
- βοΈ Performance Optimizations: Frame update debouncing, improved WebView management, and reduced main thread blocking
- π Comprehensive Logging: Built-in performance monitoring with os.log integration
- π¨ Type-Safe Background Colors: Complete background color system with
.transparent
,.system
,.hex()
, and.color()
support - π± Interactive Media Handling: Full media click event system for images and videos with custom action support
- π§ Improved Font System: Better monospace and italic rendering with enhanced CSS generation
- π Modern Loading States: Type-safe loading transitions with
.fade
,.scale
,.slide
, and custom animations
- π§ͺ Swift Testing Migration: Complete migration from XCTest to modern Swift Testing framework
- π Semantic API Naming: Modern APIs like
.textColor()
replacing.foregroundColor()
for better clarity - π¨ Comprehensive Error Handling: Detailed error types with custom callbacks and debugging support
- π οΈ Public CSS Access: Programmatic CSS generation and access for advanced customization scenarios
- π Enhanced HTML5 Support: Complete support for
<figure>
,<details>
,<summary>
,<figcaption>
, and semantic elements
- β 100% Backward Compatible: All v2.x code works without changes
β οΈ Thoughtful Deprecations: Deprecated methods include clear migration guidance- π Migration Tooling: Built-in TestApp with Modern API demo and migration examples
Version 3.0.0 maintains 100% backward compatibility for v2.x users while providing a clear path to modern APIs:
- β Zero Breaking Changes: All existing v2.x code works unchanged
- β Automatic Performance: Better async/await performance and font rendering without code changes
- β Guided Migration: Helpful deprecation warnings with clear modern API alternatives
- β Additive Enhancement: New features are optional and don't affect existing functionality
- β Future-Proof: Modern architecture ready for Swift 6+ and future iOS/macOS versions
- Update to v3.0.0: Immediate performance and reliability improvements
- Add Error Handling: Use
.onError()
for better debugging and user experience - Modernize APIs: Replace deprecated methods with type-safe alternatives
- Enhance Interactivity: Add
.onMediaClick()
for rich media experiences - Improve Loading UX: Implement
.placeholder {}
and modern transitions
RichText(html: html)
.fontType(.customName("SF Mono")) // System monospace font
.fontType(.customName("Helvetica")) // System Helvetica
RichText(html: html)
.fontType(.customName("CustomFont-Regular"))
.customCSS("""
@font-face {
font-family: 'CustomFont-Regular';
src: url("CustomFont-Regular.ttf") format('truetype');
}
""")
let config = Configuration(
supportsDynamicType: true // Automatically use iOS Dynamic Type
)
RichText(html: html, configuration: config)
RichText(html: html)
.backgroundColor(.transparent)
.customCSS("""
body {
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
padding: 20px;
border-radius: 12px;
}
""")
RichText(html: html)
.foregroundColor(light: .primary, dark: .primary)
.linkColor(light: .blue, dark: .cyan)
.backgroundColor(.system)
.colorPreference(forceColor: .all) // Override HTML colors
struct ContentView: View {
@State private var selectedImage: String?
var body: some View {
RichText(html: htmlWithImages)
.onMediaClick { media in
switch media {
case .image(let src):
selectedImage = src
case .video(let src):
openVideoPlayer(url: src)
}
}
.fullScreenCover(item: Binding<String?>(
get: { selectedImage },
set: { selectedImage = $0 }
)) { imageURL in
ImageViewer(url: imageURL)
}
}
}
struct ContentView: View {
@State private var lastError: RichTextError?
var body: some View {
VStack {
if let error = lastError {
ErrorBanner(error: error)
}
RichText(html: html)
.onError { error in
lastError = error
// Log to analytics
Analytics.log("RichText Error", parameters: [
"error_type": String(describing: error),
"html_length": html.count
])
}
}
}
}
RichText(html: largeHtmlContent)
.imageRadius(0) // Disable image styling for performance
.customCSS("""
img {
max-width: 100%;
height: auto;
loading: lazy; /* Native lazy loading */
}
""")
.loadingTransition(.none) // Disable transitions for faster rendering
struct ContentView: View {
@StateObject private var htmlManager = HTMLContentManager()
var body: some View {
RichText(html: htmlManager.currentHTML)
.onError { error in
htmlManager.handleError(error)
}
.onDisappear {
htmlManager.cleanup() // Custom cleanup logic
}
}
}
struct BlogPostView: View {
let post: BlogPost
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text(post.title)
.font(.largeTitle)
.fontWeight(.bold)
RichText(html: post.content)
.lineHeight(175)
.imageRadius(8)
.backgroundColor(.system)
.linkOpenType(.SFSafariView())
.onMediaClick { media in
handleMediaClick(media)
}
.customCSS("""
blockquote {
border-left: 4px solid #007AFF;
padding-left: 16px;
margin: 16px 0;
font-style: italic;
}
code {
background-color: #f5f5f5;
padding: 2px 4px;
border-radius: 3px;
}
""")
}
.padding()
}
}
private func handleMediaClick(_ media: MediaClickType) {
// Custom media handling
}
}
struct EmailView: View {
let emailHTML: String
@State private var isLoading = true
var body: some View {
RichText(html: emailHTML)
.backgroundColor(.system)
.lineHeight(160)
.fontType(.system)
.linkOpenType(.custom { url in
// Custom link handling for email safety
if url.host?.contains("trusted-domain.com") == true {
UIApplication.shared.open(url)
} else {
showLinkConfirmation(url)
}
})
.placeholder {
Text("Loading email...")
}
.loadingTransition(.fade)
.onError { error in
print("Email loading error: \(error)")
}
}
private func showLinkConfirmation(_ url: URL) {
// Show confirmation dialog
}
}
struct DocumentationView: View {
let markdownHTML: String
var body: some View {
NavigationView {
RichText(html: markdownHTML)
.fontType(.system)
.lineHeight(170)
.backgroundColor(.transparent)
.customCSS("""
h1, h2, h3 {
color: #1d4ed8;
margin-top: 24px;
margin-bottom: 12px;
}
pre {
background-color: #f8f9fa;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
}
code {
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
}
""")
.navigationTitle("Documentation")
.navigationBarTitleDisplayMode(.large)
}
}
}
Problem: RichText shows blank or doesn't render content
Solutions:
- Ensure HTML is valid and well-formed
- Check that images have proper URLs
- Verify network permissions for external resources
- Add error handling to debug loading issues
RichText(html: html)
.onError { error in
print("Debug error: \(error)")
}
Problem: Images don't appear in the rendered content
Solutions:
- Verify image URLs are accessible
- For macOS: Enable "Outgoing Connections (Client)" in App Sandbox
- Use base URL for relative image paths
RichText(html: html)
.baseURL(Bundle.main.bundleURL) // For bundled resources
Problem: Slow rendering with large HTML content
Solutions:
- Simplify CSS and reduce inline styles
- Use image compression for better loading
- Consider pagination for very large content
- Disable animations for better performance
RichText(html: largeContent)
.loadingTransition(.none)
.imageRadius(0)
Problem: Colors don't adapt properly to dark mode
Solutions:
- Use
.colorScheme(.auto)
for automatic adaptation - Set proper light/dark colors for text and links
- Force color scheme background if needed
RichText(html: html)
.colorScheme(.auto)
.forceColorSchemeBackground(true)
.foregroundColor(light: .black, dark: .white)
Issue: External resources don't load
- Solution: Enable "Outgoing Connections (Client)" in App Sandbox settings
- Alternative: Use bundled resources or file URLs
Issue: Scrolling behavior differs from iOS
- Solution: This is expected due to platform differences
- Workaround: Embed in a ScrollView for consistent behavior
Issue: SFSafariViewController not presenting
- Solution: Ensure you have a presented view controller
- Alternative: Use
.linkOpenType(.Safari)
as fallback
If you experience memory issues with large content:
// Implement proper cleanup
struct ContentView: View {
@State private var html = ""
var body: some View {
RichText(html: html)
.onDisappear {
html = "" // Clear content when not visible
}
}
}
- Check the Issues: Search GitHub Issues for similar problems
- Provide Details: When reporting issues, include:
- iOS/macOS version
- RichText version
- Sample HTML content
- Error messages or console output
- Create Minimal Example: Provide a minimal reproducible example
// β
v2.7.0 - Still works, but deprecated
RichText(html: html)
.backgroundColor("transparent") // Deprecated but functional
// π v3.0.0 - Recommended approaches
RichText(html: html)
.transparentBackground() // Easiest for transparent
.backgroundColorHex("#FF0000") // For hex colors
.backgroundColorSwiftUI(.blue) // For SwiftUI colors
.backgroundColor(.system) // For system colors
// π Add error handling
RichText(html: html)
.onError { error in
print("Error: \(error)")
}
// π Add interactive media handling
RichText(html: html)
.onMediaClick { media in
switch media {
case .image(let src):
presentImageViewer(src)
case .video(let src):
presentVideoPlayer(src)
}
}
// π Better loading experience with custom view
RichText(html: html)
.placeholder {
HStack(spacing: 8) {
ProgressView()
.scaleEffect(0.8)
Text("Loading...")
.foregroundColor(.secondary)
}
.frame(minHeight: 60)
}
.loadingTransition(.fade)
// β
v2.x - Still works, but deprecated
RichText(html: html)
.foregroundColor(light: .black, dark: .white) // Deprecated
// π v3.0.0 - Modern semantic naming
RichText(html: html)
.textColor(light: .black, dark: .white) // Modern & clear
No changes needed - font rendering is automatically improved:
// β
Automatically better in v3.0.0 with async/await
RichText(html: html)
.fontType(.monospaced) // Enhanced rendering
.fontType(.italic) // Improved CSS generation
- Update to v3.0.0: Your existing code continues to work
- Add Error Handling: Use
.onError()
for better debugging - Update Background Colors: Replace string-based with type-safe methods
- Add Media Handling: Use
.onMediaClick()
for interactive content - Improve Loading UX: Add
.placeholder {}
with custom views and transitions
We welcome contributions! Here's how you can help:
- Use GitHub Issues for bug reports
- Include reproduction steps and sample code
- Specify iOS/macOS version and RichText version
- Create a Discussion for feature requests
- Explain the use case and expected behavior
- Consider backward compatibility implications
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature
) - Write tests for your changes
- Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
- Follow Swift naming conventions and modern async/await patterns
- Add comprehensive documentation for public APIs with usage examples
- Ensure backward compatibility and provide clear migration paths
- Use Swift Testing for all new test coverage
- Update README.md and TestApp for new features
- Consider performance implications and use os.log for debugging
RichText is available under the MIT license. See the LICENSE file for more info.
- Built with WebKit for reliable HTML rendering
- Inspired by the SwiftUI community's need for rich text solutions
- Thanks to all contributors and users
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Made with β€οΈ for the SwiftUI community