From c1efda979fae556d24e09ff08b30c8f4496c2967 Mon Sep 17 00:00:00 2001 From: NuPlay <73557895+NuPlay@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:39:29 +0900 Subject: [PATCH 1/2] docs: split the README and document the 3.1.0 changes The README had grown to just over 1000 lines, which buries the two things a visitor actually needs first - what this is and how to install it - under a full API reference, two release-note sections, a migration guide and a troubleshooting appendix. Split by audience: | file | contents | |---|---| | README.md | pitch, features, quick start, install, links | | docs/API.md | initializers, modifiers, utilities | | docs/ADVANCED.md | custom fonts, theming, media, performance, examples | | docs/TROUBLESHOOTING.md | common issues, platform notes | | docs/MIGRATION.md | upgrading from v2.x | | CHANGELOG.md | release notes, newest first | | CONTRIBUTING.md | moved to the path GitHub surfaces in its UI | README goes from 1079 lines to 161. Nothing was dropped: the only prose removed is the second Quick Start example, folded into the first, and the table of contents, replaced by the documentation table. Also documents 3.1.0 itself - the fixes, the errors that now actually fire, the interpolated CSS builders, and a table of the nine new deprecations with their replacements. Version pins in the install instructions move to 3.1.0, and the sample app README lists the new behaviour worth testing. --- CHANGELOG.md | 141 +++++++ CONTRIBUTING.md | 33 ++ README.md | 880 ++-------------------------------------- TestApp/README.md | 10 + docs/ADVANCED.md | 269 ++++++++++++ docs/API.md | 219 ++++++++++ docs/MIGRATION.md | 87 ++++ docs/TROUBLESHOOTING.md | 122 ++++++ 8 files changed, 917 insertions(+), 844 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 docs/ADVANCED.md create mode 100644 docs/API.md create mode 100644 docs/MIGRATION.md create mode 100644 docs/TROUBLESHOOTING.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..79f2d12 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,141 @@ +# Changelog + +All notable changes to RichText are documented here. + +--- + +## 3.1.0 + +A correctness release. Every change below is source-compatible - nothing public was removed. + +### ๐Ÿ› **Fixes** + +**Images were never constrained to the view width.** The image stylesheet is rendered with `String(format:)`, which eats a bare `%`, so `max-width: 100%` was emitted as `max-width: 100` - an invalid length the browser discards. Large images overflowed. ([#76](https://github.com/NuPlay/RichText/pull/76)) + +**Content was clipped and `
` never expanded.** The rendered height was measured exactly once, in `window.onload`. Anything that changed the layout afterwards - a `
` toggled open, late images, web fonts, rotation, Dynamic Type - was never reported back to SwiftUI. The container is now watched with a `ResizeObserver`. ([#18](https://github.com/NuPlay/RichText/issues/18), [#59](https://github.com/NuPlay/RichText/issues/59)) + +**The document was reloaded on every SwiftUI update.** `updateUIView` called `loadHTMLString` unconditionally, including for the height updates the view produces itself, so the document was re-parsed several times per appearance and all in-page state was thrown away. It now reloads only when the generated HTML or base URL actually changes. ([#60](https://github.com/NuPlay/RichText/issues/60)) + +**Dynamic Type silently discarded `fontType`.** `font: -apple-system-body` is a shorthand, and a shorthand resets `font-family`, so `supportsDynamicType: true` made `.monospaced`, `.italic` and `.customName` fall back to the default family. ([#63](https://github.com/NuPlay/RichText/issues/63)) + +**`SafariServices` was imported unconditionally**, which broke the build on platforms that do not ship it. ([#77](https://github.com/NuPlay/RichText/issues/77)) + +### ๐Ÿšจ **Errors that now actually fire** + +`RichTextError.webViewConfigurationFailed` and `.cssGenerationFailed` were documented but never constructed. They now report real failures: + +```swift +RichText(html: html) + .onError { error in + switch error { + case .cssGenerationFailed: + // fontColor or linkColor holds an invalid hex value, so the browser + // is dropping the declaration and the colour falls back to default + case .webViewConfigurationFailed: + // macOS: the web view background could not be made transparent + default: + break + } + } +``` + +`ColorSet.isValid` backs the first of those, and now accepts every hex form CSS accepts - `#RGB`, `#RGBA`, `#RRGGBB` and `#RRGGBBAA`. It previously rejected the two shorthand forms. + +### ๐Ÿ—๏ธ **CSS is now built by interpolation** + +The stylesheet and document templates were printf format strings. That is what caused the image bug above, and it also meant literal percent signs had to be escaped as `%%`. They are replaced by builders: + +```swift +RichTextConstants.imageCSS(radius: 8) +RichTextConstants.textCSS(alignment:lineHeight:fontFamily:color:backgroundColor:) +RichTextConstants.iframeCSS(height: 250) +RichTextConstants.linkCSS(color: "#007AFF") +RichTextConstants.styleDocument(css:customCSS:) +RichTextConstants.styleDocument(lightCSS:darkCSS:customCSS:) +RichTextConstants.htmlDocument(css:body:) +``` + +Output is unchanged - each builder was diffed against the format string it replaces and all seven render byte-identical results. + +### โš ๏ธ **Deprecations** + +Nothing is removed; all of these still compile and behave as before. + +| Deprecated | Use instead | +|---|---| +| `RichTextConstants.imageCSS` | `RichTextConstants.imageCSS(radius:)` | +| `RichTextConstants.textCSS` | `RichTextConstants.textCSS(alignment:lineHeight:fontFamily:color:backgroundColor:)` | +| `RichTextConstants.iframeCSS` | `RichTextConstants.iframeCSS(height:)` | +| `RichTextConstants.linkCSS` | `RichTextConstants.linkCSS(color:)` | +| `RichTextConstants.cssTemplate` | `RichTextConstants.styleDocument(css:customCSS:)` | +| `RichTextConstants.mediaCSSTemplate` | `RichTextConstants.styleDocument(lightCSS:darkCSS:customCSS:)` | +| `RichTextConstants.htmlTemplate` | `RichTextConstants.htmlDocument(css:body:)` | +| `RichTextConstants.bodyCSS` | nothing - it was never applied | +| `Configuration.isColorsImportant` | `.colorPreference(forceColor:)`, or `ColorSet(light:dark:isImportant:)` | + +`Configuration.isColorsImportant` deserves a note: it was recorded but never read during CSS generation, so `Configuration(isColorsImportant: .all)` silently did nothing. `!important` comes from the colour sets alone. + +```swift +// โŒ had no effect +Configuration(fontColor: ColorSet(light: "000000", dark: "FFFFFF"), isColorsImportant: .all) + +// โœ… either of these works +RichText(html: html).colorPreference(forceColor: .all) +Configuration(fontColor: ColorSet(light: "000000", dark: "FFFFFF", isImportant: true)) +``` + +### ๐Ÿงน **Also** + +- `loading: lazy` removed from the image CSS. `loading` is an HTML attribute, not a CSS property, so no browser ever applied it. +- Percentage `min-height`/`max-height` removed from the image CSS. Both resolved against an `auto`-height containing block and did nothing. +- macOS transparency no longer risks an uncatchable `NSUnknownKeyException`. + +--- + +## 3.0.0 + +### ๐Ÿš€ **Core Modernization** + +- **โšก 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 + +### ๐ŸŽจ **Enhanced User Experience** + +- **๐ŸŽจ 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 + +### ๐Ÿ› ๏ธ **Developer Experience** + +- **๐Ÿงช 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 `
`, `
`, ``, `
`, and semantic elements + +### ๐Ÿ”„ **Migration & Compatibility** + +- **โœ… 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 + +### ๐Ÿ”„ **Backward Compatibility Promise** + +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 + +### ๐ŸŽฏ **Recommended Migration Path** + +1. **Update to v3.0.0**: Immediate performance and reliability improvements +2. **Add Error Handling**: Use `.onError()` for better debugging and user experience +3. **Modernize APIs**: Replace deprecated methods with type-safe alternatives +4. **Enhance Interactivity**: Add `.onMediaClick()` for rich media experiences +5. **Improve Loading UX**: Implement `.placeholder {}` and modern transitions diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a094835 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# ๐Ÿค Contributing + +We welcome contributions! Here's how you can help: + +### Reporting Issues + +- Use [GitHub Issues](https://github.com/NuPlay/RichText/issues) for bug reports +- Include reproduction steps and sample code +- Specify iOS/macOS version and RichText version + +### Suggesting Features + +- Create a [Discussion](https://github.com/NuPlay/RichText/discussions) for feature requests +- Explain the use case and expected behavior +- Consider backward compatibility implications + +### Code Contributions + +1. **Fork** the repository +2. **Create** a feature branch (`git checkout -b feature/amazing-feature`) +3. **Write** tests for your changes +4. **Commit** your changes (`git commit -m 'Add amazing feature'`) +5. **Push** to the branch (`git push origin feature/amazing-feature`) +6. **Open** a Pull Request + +### Development Guidelines + +- 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 diff --git a/README.md b/README.md index 732534b..12332c4 100644 --- a/README.md +++ b/README.md @@ -34,120 +34,53 @@ A modern, powerful, and type-safe SwiftUI component for rendering HTML content w --- -## Table of Contents - -- [โœจ Features](#-features) -- [๐ŸŒŸ Used By](#-used-by) -- [๐Ÿš€ Quick Start](#-quick-start) -- [๐Ÿ“ฆ Installation](#-installation) -- [๐Ÿ“š Complete API Reference](#-complete-api-reference) -- [๐Ÿ†• What's New in v3.0.0](#-whats-new-in-v300) -- [๐Ÿ”ง Advanced Usage](#-advanced-usage) -- [๐Ÿ’ก Examples](#-examples) -- [๐Ÿ› Troubleshooting](#-troubleshooting) -- [๐Ÿ“– Migration Guide](#-migration-guide) -- [๐Ÿค Contributing](#-contributing) - --- ## โœจ Features -### ๐Ÿš€ **v3.0.0 - Modern Architecture** -- โšก **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 - -### ๐Ÿ“ฑ **Platform Support** -- ๐Ÿ“ฑ **Cross-platform**: iOS 15.0+ and macOS 12.0+ with Swift 5.9+ (tvOS and watchOS are not supported, as `WKWebView` is unavailable there) -- ๐ŸŽจ **Theme Support**: Automatic light/dark mode with custom color schemes -- ๐Ÿ”ค **Typography**: System fonts, custom fonts, monospace, italic, and Dynamic Type support - -### ๐ŸŽ›๏ธ **Rich Features** -- ๐Ÿ–ผ๏ธ **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 - ---- - -## ๐ŸŒŸ Used By - -RichText is used in public Swift projects such as: - -- [IBM/mac-ibm-notifications](https://github.com/IBM/mac-ibm-notifications) - macOS notification and alert agent -- [AudioBooth/AudioBooth](https://github.com/AudioBooth/AudioBooth) - iOS companion app for Audiobookshelf +- **Cross-platform** โ€” iOS 15.0+ and macOS 12.0+, Swift 5.9+ +- **Automatic sizing** โ€” the view resizes itself as the content lays out, including late images, web fonts and `
` toggles +- **Theming** โ€” automatic light/dark mode, custom colour sets, transparent or custom backgrounds +- **Typography** โ€” system, monospaced, italic, custom and bundled fonts, with Dynamic Type support +- **Interactive media** โ€” click callbacks for images and videos +- **Link handling** โ€” Safari, `SFSafariViewController`, or your own handler +- **HTML5** โ€” `
`, `
`, ``, `
` and the semantic elements +- **Error reporting** โ€” typed errors for load, configuration and CSS failures -No affiliation or endorsement implied. Using RichText in your project? Feel free to open a PR to add it here. +> tvOS and watchOS are not supported: Apple does not ship `WKWebView` on those platforms. --- ## ๐Ÿš€ Quick Start -### Basic Usage - -The simplest way to get started with RichText: - ```swift import SwiftUI import RichText struct ContentView: View { - let htmlContent = """ + let html = """

Welcome to RichText

A powerful HTML renderer for SwiftUI.

""" - - var body: some View { - ScrollView { - RichText(html: htmlContent) - } - } -} -``` - -### Enhanced Example - -Add styling, media handling, and error handling: -```swift -struct ContentView: View { - let htmlContent = """ -

Welcome to RichText

-

A powerful HTML renderer with extensive customization.

- Sample Image -

Visit our GitHub

- """ - 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)") + RichText(html: html) + .colorScheme(.auto) + .lineHeight(170) + .imageRadius(12) + .placeholder { ProgressView() } + .onMediaClick { media in + if case .image(let src) = media { print(src) } } + .onError { error in print(error) } } } } ``` +See [the API reference](docs/API.md) for every modifier, and [advanced usage](docs/ADVANCED.md) for custom fonts, themes and complete examples. + --- ## ๐Ÿ“ฆ Installation @@ -159,7 +92,7 @@ struct ContentView: View { ``` https://github.com/NuPlay/RichText.git ``` -3. Select version rule: **"Up to Next Major Version"** from **"3.0.0"** +3. Select version rule: **"Up to Next Major Version"** from **"3.1.0"** 4. Click **Add Package** ### Manual Package.swift @@ -168,7 +101,7 @@ Add RichText to your `Package.swift`: ```swift dependencies: [ - .package(url: "https://github.com/NuPlay/RichText.git", .upToNextMajor(from: "3.0.0")) + .package(url: "https://github.com/NuPlay/RichText.git", .upToNextMajor(from: "3.1.0")) ], targets: [ .target( @@ -180,768 +113,27 @@ targets: [ --- -## ๐Ÿ“š Complete API Reference - -### Core Components - -#### RichText Initializers - -```swift -// Basic initializer -RichText(html: String) - -// With configuration -RichText(html: String, configuration: Configuration) - -// With placeholder -RichText(html: String, placeholder: AnyView?) +## ๐Ÿ“– Documentation -// Full initializer -RichText(html: String, configuration: Configuration, placeholder: AnyView?) -``` - -### Styling Modifiers - -#### Background Colors - -```swift -// 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 -``` - -#### Typography & Colors - -```swift -// 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 -``` - -#### Layout & Spacing - -```swift -.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 -``` - -#### Link Behavior - -```swift -.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 -``` - -### Advanced Features - -#### Loading States - -```swift -// 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 -``` - -#### Event Handling - -```swift -// 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): - // The web view failed to load the generated document - print("Failed to load HTML: \(html)") - case .webViewConfigurationFailed: - // The web view could not be configured as requested (macOS transparency) - print("WebView configuration failed") - case .cssGenerationFailed: - // A colour was given as an invalid hex value, so the browser drops the declaration - print("CSS generation failed") - case .mediaHandlingFailed(let media): - print("Media handling failed: \(media)") - } -} -``` - -#### Custom Styling - -```swift -// 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) -``` - -### Configuration-Based Initialization - -For complex configurations, create a `Configuration` object: - -```swift -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 */ }, - transition: .easeInOut(duration: 0.3) -) - -RichText(html: htmlContent, configuration: config) -``` - -### Utility Methods - -```swift -// 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) -``` - ---- - -## ๐Ÿ†• What's New in v3.0.0 - -### ๐Ÿš€ **Core Modernization** - -- **โšก 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 - -### ๐ŸŽจ **Enhanced User Experience** - -- **๐ŸŽจ 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 - -### ๐Ÿ› ๏ธ **Developer Experience** - -- **๐Ÿงช 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 `
`, `
`, ``, `
`, and semantic elements - -### ๐Ÿ”„ **Migration & Compatibility** - -- **โœ… 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 - -### ๐Ÿ”„ **Backward Compatibility Promise** - -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 - -### ๐ŸŽฏ **Recommended Migration Path** - -1. **Update to v3.0.0**: Immediate performance and reliability improvements -2. **Add Error Handling**: Use `.onError()` for better debugging and user experience -3. **Modernize APIs**: Replace deprecated methods with type-safe alternatives -4. **Enhance Interactivity**: Add `.onMediaClick()` for rich media experiences -5. **Improve Loading UX**: Implement `.placeholder {}` and modern transitions +| | | +|---|---| +| [API Reference](docs/API.md) | Every initializer, modifier and utility | +| [Advanced Usage](docs/ADVANCED.md) | Custom fonts, theming, media handling, performance, full examples | +| [Troubleshooting](docs/TROUBLESHOOTING.md) | Common issues and platform-specific notes | +| [Migration Guide](docs/MIGRATION.md) | Upgrading from v2.x | +| [Changelog](CHANGELOG.md) | Release notes | +| [Contributing](CONTRIBUTING.md) | How to report issues and send patches | --- -## ๐Ÿ”ง Advanced Usage - -### Custom Fonts - -#### Using System-Installed Fonts - -```swift -RichText(html: html) - .fontType(.customName("SF Mono")) // System monospace font - .fontType(.customName("Helvetica")) // System Helvetica -``` - -#### Using Bundled Fonts - -```swift -RichText(html: html) - .fontType(.customName("CustomFont-Regular")) - .customCSS(""" - @font-face { - font-family: 'CustomFont-Regular'; - src: url("CustomFont-Regular.ttf") format('truetype'); - } - """) -``` - -#### Dynamic Type Support - -```swift -let config = Configuration( - supportsDynamicType: true // Automatically use iOS Dynamic Type -) - -RichText(html: html, configuration: config) -``` - -### Complex Color Schemes - -#### Gradient Backgrounds - -```swift -RichText(html: html) - .backgroundColor(.transparent) - .customCSS(""" - body { - background: linear-gradient(45deg, #ff6b6b, #4ecdc4); - padding: 20px; - border-radius: 12px; - } - """) -``` - -#### Theme-Aware Colors - -```swift -RichText(html: html) - .foregroundColor(light: .primary, dark: .primary) - .linkColor(light: .blue, dark: .cyan) - .backgroundColor(.system) - .colorPreference(forceColor: .all) // Override HTML colors -``` - -### Interactive Media Handling - -```swift -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( - get: { selectedImage }, - set: { selectedImage = $0 } - )) { imageURL in - ImageViewer(url: imageURL) - } - } -} -``` - -### Error Handling and Debugging - -```swift -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 - ]) - } - } - } -} -``` - -### Performance Optimization - -#### For Large Content - -```swift -RichText(html: largeHtmlContent) - .imageRadius(0) // Disable image styling for performance - .customCSS(""" - img { - max-width: 100%; - height: auto; - } - """) - .loadingTransition(.none) // Disable transitions for faster rendering -``` - -#### Memory Management - -```swift -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 - } - } -} -``` - ---- - -## ๐Ÿ’ก Examples - -### Blog Post Renderer - -```swift -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 - } -} -``` - -### Email Content Viewer - -```swift -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 - } -} -``` - -### Documentation Viewer - -```swift -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) - } - } -} -``` - ---- - -## ๐Ÿ› Troubleshooting - -### Common Issues - -#### Content Not Displaying - -**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 - -```swift -RichText(html: html) - .onError { error in - print("Debug error: \(error)") - } -``` - -#### Images Not Loading - -**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 - -```swift -RichText(html: html) - .baseURL(Bundle.main.bundleURL) // For bundled resources -``` - -#### Performance Issues - -**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 - -```swift -RichText(html: largeContent) - .loadingTransition(.none) - .imageRadius(0) -``` - -#### Dark Mode Issues - -**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 - -```swift -RichText(html: html) - .colorScheme(.auto) - .forceColorSchemeBackground(true) - .foregroundColor(light: .black, dark: .white) -``` - -### Platform-Specific Issues - -#### macOS Specific - -**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 - -#### iOS Specific - -**Issue**: SFSafariViewController not presenting -- **Solution**: Ensure you have a presented view controller -- **Alternative**: Use `.linkOpenType(.Safari)` as fallback - -#### tvOS / watchOS - -**Issue**: The package does not build for tvOS or watchOS -- **Cause**: `RichText` renders HTML through `WKWebView`, and Apple does not ship `WKWebView` on tvOS or watchOS. There is no supported way to display a web view on those platforms. -- **Solution**: Render the content natively on those platforms, for example with `AttributedString` and `Text`. - -### Memory Management - -If you experience memory issues with large content: - -```swift -// Implement proper cleanup -struct ContentView: View { - @State private var html = "" - - var body: some View { - RichText(html: html) - .onDisappear { - html = "" // Clear content when not visible - } - } -} -``` - -### Getting Help - -1. **Check the Issues**: Search [GitHub Issues](https://github.com/NuPlay/RichText/issues) for similar problems -2. **Provide Details**: When reporting issues, include: - - iOS/macOS version - - RichText version - - Sample HTML content - - Error messages or console output -3. **Create Minimal Example**: Provide a minimal reproducible example - ---- - -## ๐Ÿ“– Migration Guide - -### From v2.x to v3.0.0 - -#### Background Colors - -```swift -// โœ… 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 -``` - -#### Enhanced Features (Optional Upgrades) - -```swift -// ๐Ÿš€ 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) -``` - -#### Font & Color API Modernization - -```swift -// โœ… 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 -``` - -#### Enhanced Font Rendering - -No changes needed - font rendering is automatically improved: - -```swift -// โœ… Automatically better in v3.0.0 with async/await -RichText(html: html) - .fontType(.monospaced) // Enhanced rendering - .fontType(.italic) // Improved CSS generation -``` - -### Recommended Migration Steps - -1. **Update to v3.0.0**: Your existing code continues to work -2. **Add Error Handling**: Use `.onError()` for better debugging -3. **Update Background Colors**: Replace string-based with type-safe methods -4. **Add Media Handling**: Use `.onMediaClick()` for interactive content -5. **Improve Loading UX**: Add `.placeholder {}` with custom views and transitions - ---- - -## ๐Ÿค Contributing - -We welcome contributions! Here's how you can help: - -### Reporting Issues - -- Use [GitHub Issues](https://github.com/NuPlay/RichText/issues) for bug reports -- Include reproduction steps and sample code -- Specify iOS/macOS version and RichText version - -### Suggesting Features - -- Create a [Discussion](https://github.com/NuPlay/RichText/discussions) for feature requests -- Explain the use case and expected behavior -- Consider backward compatibility implications - -### Code Contributions +## ๐ŸŒŸ Used By -1. **Fork** the repository -2. **Create** a feature branch (`git checkout -b feature/amazing-feature`) -3. **Write** tests for your changes -4. **Commit** your changes (`git commit -m 'Add amazing feature'`) -5. **Push** to the branch (`git push origin feature/amazing-feature`) -6. **Open** a Pull Request +RichText is used in public Swift projects such as: -### Development Guidelines +- [IBM/mac-ibm-notifications](https://github.com/IBM/mac-ibm-notifications) - macOS notification and alert agent +- [AudioBooth/AudioBooth](https://github.com/AudioBooth/AudioBooth) - iOS companion app for Audiobookshelf -- 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 +No affiliation or endorsement implied. Using RichText in your project? Feel free to open a PR to add it here. --- diff --git a/TestApp/README.md b/TestApp/README.md index b358d9a..5a4e273 100644 --- a/TestApp/README.md +++ b/TestApp/README.md @@ -52,9 +52,19 @@ The test app covers all major features: 7. **Link Behavior**: Test different link opening methods 8. **Performance**: Load large content and measure render time 9. **Backward Compatibility**: Ensure deprecated methods still work +10. **Wide Images**: Load an image wider than the view and confirm it is scaled down, not clipped (v3.1.0) +11. **Collapsible Content**: Toggle a `
` open and confirm the view grows to fit it (v3.1.0) +12. **Dynamic Type + fontType**: Enable `supportsDynamicType` together with `.monospaced` and confirm both apply (v3.1.0) ## Expected Behavior +### New in v3.1.0 +- **Images Fit the View**: `max-width: 100%` now applies, so wide images scale down instead of overflowing +- **Height Tracks the Content**: Opening a `
`, late-loading images and web fonts all resize the view instead of being clipped +- **No Redundant Reloads**: The document is reloaded only when the HTML or base URL changes, so in-page state survives a SwiftUI update +- **Dynamic Type Keeps `fontType`**: `supportsDynamicType` no longer discards `.monospaced`, `.italic` or `.customName` +- **More Errors Reported**: Invalid hex colours raise `.cssGenerationFailed`; macOS transparency failures raise `.webViewConfigurationFailed` + ### New in v3.0.0 - **Better Font Rendering**: Monospaced and italic fonts should display correctly - **Media Interaction**: Clicking images/videos should trigger callbacks diff --git a/docs/ADVANCED.md b/docs/ADVANCED.md new file mode 100644 index 0000000..a29f289 --- /dev/null +++ b/docs/ADVANCED.md @@ -0,0 +1,269 @@ +# ๐Ÿ”ง Advanced Usage + +### Custom Fonts + +#### Using System-Installed Fonts + +```swift +RichText(html: html) + .fontType(.customName("SF Mono")) // System monospace font + .fontType(.customName("Helvetica")) // System Helvetica +``` + +#### Using Bundled Fonts + +```swift +RichText(html: html) + .fontType(.customName("CustomFont-Regular")) + .customCSS(""" + @font-face { + font-family: 'CustomFont-Regular'; + src: url("CustomFont-Regular.ttf") format('truetype'); + } + """) +``` + +#### Dynamic Type Support + +```swift +let config = Configuration( + supportsDynamicType: true // Automatically use iOS Dynamic Type +) + +RichText(html: html, configuration: config) +``` + +### Complex Color Schemes + +#### Gradient Backgrounds + +```swift +RichText(html: html) + .backgroundColor(.transparent) + .customCSS(""" + body { + background: linear-gradient(45deg, #ff6b6b, #4ecdc4); + padding: 20px; + border-radius: 12px; + } + """) +``` + +#### Theme-Aware Colors + +```swift +RichText(html: html) + .foregroundColor(light: .primary, dark: .primary) + .linkColor(light: .blue, dark: .cyan) + .backgroundColor(.system) + .colorPreference(forceColor: .all) // Override HTML colors +``` + +### Interactive Media Handling + +```swift +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( + get: { selectedImage }, + set: { selectedImage = $0 } + )) { imageURL in + ImageViewer(url: imageURL) + } + } +} +``` + +### Error Handling and Debugging + +```swift +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 + ]) + } + } + } +} +``` + +### Performance Optimization + +#### For Large Content + +```swift +RichText(html: largeHtmlContent) + .imageRadius(0) // Disable image styling for performance + .customCSS(""" + img { + max-width: 100%; + height: auto; + } + """) + .loadingTransition(.none) // Disable transitions for faster rendering +``` + +#### Memory Management + +```swift +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 + } + } +} +``` + +--- + +## ๐Ÿ’ก Examples + +### Blog Post Renderer + +```swift +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 + } +} +``` + +### Email Content Viewer + +```swift +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 + } +} +``` + +### Documentation Viewer + +```swift +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) + } + } +} +``` + +--- + +[โ† Back to README](../README.md) diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..bd5ac01 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,219 @@ +# ๐Ÿ“š Complete API Reference + +### Core Components + +#### RichText Initializers + +```swift +// 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?) +``` + +### Styling Modifiers + +#### Background Colors + +```swift +// 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 +``` + +#### Typography & Colors + +```swift +// 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 +``` + +#### Layout & Spacing + +```swift +.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 +``` + +#### Link Behavior + +```swift +.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 +``` + +### Advanced Features + +#### Loading States + +```swift +// 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 +``` + +#### Event Handling + +```swift +// 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): + // The web view failed to load the generated document + print("Failed to load HTML: \(html)") + case .webViewConfigurationFailed: + // The web view could not be configured as requested (macOS transparency) + print("WebView configuration failed") + case .cssGenerationFailed: + // A colour was given as an invalid hex value, so the browser drops the declaration + print("CSS generation failed") + case .mediaHandlingFailed(let media): + print("Media handling failed: \(media)") + } +} +``` + +#### Custom Styling + +```swift +// 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) +``` + +### Configuration-Based Initialization + +For complex configurations, create a `Configuration` object: + +```swift +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 */ }, + transition: .easeInOut(duration: 0.3) +) + +RichText(html: htmlContent, configuration: config) +``` + +### Utility Methods + +```swift +// 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) +``` + +Individual rules can be built directly (v3.1.0+). These replace the deprecated +`String(format:)` constants, so percent signs no longer need escaping: + +```swift +RichTextConstants.imageCSS(radius: 8) +// img{height:auto; max-width: 100%; width:auto;margin-bottom:5px; border-radius: 8.0px;} + +RichTextConstants.iframeCSS(height: 250) +// iframe{width:100%; height:250px; border: none;} + +RichTextConstants.linkCSS(color: "#007AFF") +// a:link {color: #007AFF; transition: color 0.2s ease;} +``` + +--- + +[โ† Back to README](../README.md) diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md new file mode 100644 index 0000000..a7603e4 --- /dev/null +++ b/docs/MIGRATION.md @@ -0,0 +1,87 @@ +# ๐Ÿ“– Migration Guide + +### From v2.x to v3.0.0 + +#### Background Colors + +```swift +// โœ… 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 +``` + +#### Enhanced Features (Optional Upgrades) + +```swift +// ๐Ÿš€ 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) +``` + +#### Font & Color API Modernization + +```swift +// โœ… 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 +``` + +#### Enhanced Font Rendering + +No changes needed - font rendering is automatically improved: + +```swift +// โœ… Automatically better in v3.0.0 with async/await +RichText(html: html) + .fontType(.monospaced) // Enhanced rendering + .fontType(.italic) // Improved CSS generation +``` + +### Recommended Migration Steps + +1. **Update to v3.0.0**: Your existing code continues to work +2. **Add Error Handling**: Use `.onError()` for better debugging +3. **Update Background Colors**: Replace string-based with type-safe methods +4. **Add Media Handling**: Use `.onMediaClick()` for interactive content +5. **Improve Loading UX**: Add `.placeholder {}` with custom views and transitions + +--- + +[โ† Back to README](../README.md) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md new file mode 100644 index 0000000..1f19cba --- /dev/null +++ b/docs/TROUBLESHOOTING.md @@ -0,0 +1,122 @@ +# ๐Ÿ› Troubleshooting + +### Common Issues + +#### Content Not Displaying + +**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 + +```swift +RichText(html: html) + .onError { error in + print("Debug error: \(error)") + } +``` + +#### Images Not Loading + +**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 + +```swift +RichText(html: html) + .baseURL(Bundle.main.bundleURL) // For bundled resources +``` + +#### Performance Issues + +**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 + +```swift +RichText(html: largeContent) + .loadingTransition(.none) + .imageRadius(0) +``` + +#### Dark Mode Issues + +**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 + +```swift +RichText(html: html) + .colorScheme(.auto) + .forceColorSchemeBackground(true) + .foregroundColor(light: .black, dark: .white) +``` + +### Platform-Specific Issues + +#### macOS Specific + +**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 + +#### iOS Specific + +**Issue**: SFSafariViewController not presenting +- **Solution**: Ensure you have a presented view controller +- **Alternative**: Use `.linkOpenType(.Safari)` as fallback + +#### tvOS / watchOS + +**Issue**: The package does not build for tvOS or watchOS +- **Cause**: `RichText` renders HTML through `WKWebView`, and Apple does not ship `WKWebView` on tvOS or watchOS. There is no supported way to display a web view on those platforms. +- **Solution**: Render the content natively on those platforms, for example with `AttributedString` and `Text`. + +### Memory Management + +If you experience memory issues with large content: + +```swift +// Implement proper cleanup +struct ContentView: View { + @State private var html = "" + + var body: some View { + RichText(html: html) + .onDisappear { + html = "" // Clear content when not visible + } + } +} +``` + +### Getting Help + +1. **Check the Issues**: Search [GitHub Issues](https://github.com/NuPlay/RichText/issues) for similar problems +2. **Provide Details**: When reporting issues, include: + - iOS/macOS version + - RichText version + - Sample HTML content + - Error messages or console output +3. **Create Minimal Example**: Provide a minimal reproducible example + +--- + +[โ† Back to README](../README.md) From e90d88ed1d980fba85298c2c3882284f61af1f0e Mon Sep 17 00:00:00 2001 From: NuPlay <73557895+NuPlay@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:43:22 +0900 Subject: [PATCH 2/2] docs: tighten the Used By section Re-verified both entries by reading the dependency manifest in each repository rather than trusting a search index: - IBM/mac-ibm-notifications - Notification Agent.xcodeproj/project.pbxproj - AudioBooth/AudioBooth - AudioBooth.xcodeproj/project.pbxproj Both are unarchived and were pushed to within the last two months. Reformatted as a table and made the disclaimer state the actual evidence - each project's public manifest - and that no endorsement is implied in either direction. Star counts are deliberately left out, since they would go stale in the file. Note for anyone looking to extend this list: GitHub's dependency graph reports zero dependents for this repository because it does not index Swift package manifests, and code search misses both entries above, so neither is a reliable source on its own. --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 12332c4..21c4a51 100644 --- a/README.md +++ b/README.md @@ -128,12 +128,14 @@ targets: [ ## ๐ŸŒŸ Used By -RichText is used in public Swift projects such as: +Open-source apps that render their content with RichText: -- [IBM/mac-ibm-notifications](https://github.com/IBM/mac-ibm-notifications) - macOS notification and alert agent -- [AudioBooth/AudioBooth](https://github.com/AudioBooth/AudioBooth) - iOS companion app for Audiobookshelf +| Project | What it is | +|---|---| +| [IBM/mac-ibm-notifications](https://github.com/IBM/mac-ibm-notifications) | macOS agent for displaying notifications and alerts to managed devices | +| [AudioBooth](https://github.com/AudioBooth/AudioBooth) | iOS companion app for Audiobookshelf | -No affiliation or endorsement implied. Using RichText in your project? Feel free to open a PR to add it here. +Each of these declares RichText in its public dependency manifest. No affiliation or endorsement is implied in either direction. Using RichText in your project? Open a PR to add it. ---