MarkdownView provides robust image handling with asynchronous loading, memory caching, URL caching, and support for both local and remote images.
Overview
Images in markdown () are handled by the ImageLoader singleton, which:
- Loads images asynchronously from URLs
- Caches images in memory for fast access
- Supports URL session disk cache
- Handles local file URLs
- Posts notifications when images finish loading
- Renders images inline with text using
LTXAttachment
ImageLoader Architecture
The ImageLoader class is a singleton that manages all image loading operations:
public final class ImageLoader {
public static let shared = ImageLoader()
private let cache = NSCache<NSString, PlatformImage>()
private let session: URLSession
private var inFlightTasks: [URL: URLSessionDataTask] = [:]
private let lock = NSLock()
}
Memory Cache
Images are cached in memory using NSCache with a limit of 128 images (ImageLoader.swift:31):
NSCache automatically evicts images under memory pressure, so your app won’t crash if it loads many large images.
URL Cache
The URLSession is configured with both memory and disk caching for persistent storage across app launches:
let config = URLSessionConfiguration.default
config.urlCache = URLCache(
memoryCapacity: 20 * 1024 * 1024, // 20 MB
diskCapacity: 100 * 1024 * 1024 // 100 MB
)
session = URLSession(configuration: config)
Async Loading
Images are loaded asynchronously to prevent blocking the main thread. The loading flow:
- Check memory cache - Instant return if cached
- Check URL cache - URLSession automatically checks disk cache
- Download - Fetch from network if not cached
- Cache and notify - Store in memory cache and post notification
From ImageLoader.swift:42-54:
public func loadImage(
from urlString: String,
completion: @escaping (PlatformImage?) -> Void
) {
// Check memory cache
if let cached = cache.object(forKey: cacheKey) {
completion(cached)
return
}
// ... async download
}
The completion handler is always called on the main thread, making it safe to update UI directly.
Local File Support
Local file URLs are supported and loaded synchronously since they don’t require network access:
if url.isFileURL {
if let image = PlatformImage(contentsOfFile: url.path) {
cache.setObject(image, forKey: cacheKey)
completion(image)
} else {
completion(nil)
}
return
}
This allows you to reference bundled images or documents directory images in your markdown:

In-Flight Task Management
To prevent duplicate downloads, the loader tracks in-flight requests:
lock.lock()
if inFlightTasks[url] != nil {
lock.unlock()
return // Already fetching
}
lock.unlock()
If multiple markdown views request the same image URL before it loads, only one network request is made.
Notification System
When an image finishes loading, a notification is posted on the main thread:
public static let imageDidLoadNotification =
Notification.Name("ImageLoader.imageDidLoad")
The notification’s object is the URL string. MarkdownView observes this notification and automatically re-renders affected views (MarkdownTextView+Private.swift:246-263):
@objc func handleImageDidLoad(_ notification: Notification) {
guard !document.blocks.isEmpty else { return }
if let imageSource = notification.object as? String,
!document.imageSources.contains(imageSource) {
return
}
use(document) // Re-render to show the loaded image
}
Views only re-render if the loaded image URL is present in their document, avoiding unnecessary work.
Image Rendering
Images are rendered as inline attachments using LTXAttachment. The rendering process:
Initial State: Placeholder
Before an image loads, a text placeholder is shown as a link:
let placeholderText = altText.isEmpty ? source : altText
return NSAttributedString(
string: placeholderText,
attributes: [
.link: source,
.foregroundColor: theme.colors.highlight,
]
)
After Loading: Image Attachment
Once cached, the image is rendered using a custom drawing callback (InlineNode+Render.swift:213-279):
let drawingCallback = LTXLineDrawingAction { context, line, lineOrigin in
let rect = CGRect(
x: lineOrigin.x + runOffsetX,
y: lineOrigin.y,
width: imageSize.width,
height: imageSize.height
)
// Platform-specific drawing
image.draw(in: rect)
}
Image Sizing
Images are automatically scaled to fit within a maximum width while preserving aspect ratio:
var imageSize = image.size
let maxWidth: CGFloat = 600
if imageSize.width > maxWidth {
let scale = maxWidth / imageSize.width
imageSize = CGSize(width: maxWidth, height: imageSize.height * scale)
}
The 600pt maximum width ensures images don’t overflow on most devices while maintaining readability.
Image Tap Handler
You can handle taps on images by setting the imageTapHandler property on MarkdownTextView:
markdownView.imageTapHandler = { imageURL, tapLocation in
print("Tapped image: \(imageURL) at \(tapLocation)")
// Open full-screen viewer, share, etc.
}
The handler receives:
imageURL: The source URL string from the markdown
tapLocation: CGPoint of the tap in the view’s coordinate space
From MarkdownTextView+LTXDelegate.swift:
if let source = highlightRegion.attributes[.imageSource] as? String {
imageTapHandler?(source, location)
return
}
Image Metadata
Images carry metadata in their attributed string attributes:
.imageSource: Original URL string
.link: URL for tap detection
.contextIdentifier: Unique identifier for drawing callbacks
LTXAttachmentAttributeName: The attachment object
This metadata enables features like tap handling, accessibility, and proper text selection.
Accessibility
Images use their alt text for VoiceOver. The LTXAttachment stores the alt text or URL as its string representation:
let representedText = altText.isEmpty ? source : altText
let attachment = LTXAttachment.hold(attrString: .init(string: representedText))
When VoiceOver reads the content, it speaks the alt text instead of the replacement character.
Error Handling
If an image fails to load:
- The completion handler receives
nil
- The placeholder text remains visible as a link
- The user can tap the link to open the URL in a browser
- An error is logged in debug builds:
#if DEBUG
Self.log.warning("failed: \(urlString) error=\(error?.localizedDescription ?? \"bad data\")")
#endif
Invalid URLs or network errors result in the alt text/URL remaining as a clickable link. Always provide meaningful alt text for better user experience.
Performance Best Practices
- Use appropriate image sizes: Don’t embed 4K images in markdown - they’ll be scaled down anyway
- Leverage caching: Reference the same image multiple times without penalty
- Provide alt text: Makes placeholders more readable while loading
- Use local images for bundled assets: Faster loading with no network dependency
Synchronous Cache Check
You can synchronously check if an image is cached without triggering a load:
if let cachedImage = ImageLoader.shared.cachedImage(for: "https://example.com/image.png") {
// Image is already cached
}
This is useful for preflighting or conditional UI updates.