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..21c4a51 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 +- **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 -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 - -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,29 @@ 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?) - -// Full initializer -RichText(html: String, configuration: Configuration, placeholder: AnyView?) -``` - -### Styling Modifiers +## ๐Ÿ“– Documentation -#### 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) -``` +| | | +|---|---| +| [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 | --- -## ๐Ÿ†• 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 - ---- - -## ๐Ÿ”ง 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 +Open-source apps that render their content with RichText: -### Development Guidelines +| 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 | -- 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 +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. --- 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)