Hello,
I am trying to implement an RTF document export feature on a small app. But after days of trying and failing with dozens of approaches I’m beginning to wonder if it is even possible to create an .rtf file with embedded text and images in Swift/SwiftUI.
I’ve tried many variations of (A) building the text and image content with HTML, converting it to AttributedString, then exporting to RTF, and (B) building the text and image content directly with AttributedString attributes and attachments for the image — and in both cases, the images are not saved in the RTF file.
I am able to create a preview of the AttributedString with formatted text and image, and able to create an RTF file with formatted text that opens with TextEdit, Pages and Word without issue; but cannot get the image to appear in the saved RTF file. I’m hoping someone here can shed some light on if this is possible and if yes, how to save the combined text and image data to an RTF file.
Here is the latest variation of the code I’m using — any ideas/suggestions are appreciated 🙏🏽:
import SwiftUI
struct ContentView: View {
@State private var showExportSheet = false
@State private var rtfData: Data?
@State private var isLoading = false
@State private var previewAttributedString: NSAttributedString?
var body: some View {
VStack {
Button("Export RTF with Image") {
isLoading = true
createRTFWithEmbeddedImage()
}
.disabled(isLoading)
if isLoading {
ProgressView()
}
if let previewAttributedString = previewAttributedString {
VStack {
Text("Preview:")
.font(.headline)
TextView(attributedString: previewAttributedString)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity)
.background(Color.gray.opacity(0.1))
}
.padding()
}
}
.sheet(isPresented: $showExportSheet) {
DocumentPicker(rtfData: $rtfData)
}
}
func createRTFWithEmbeddedImage() {
let text = "This is a sample RTF document with an embedded image:"
// Load the image (star.fill as a fallback)
guard let image = UIImage(systemName: "star.fill") else {
print("Failed to load image")
isLoading = false
return
}
// Resize the image to 100x100 pixels
let resizedImage = resizeImage(image: image, targetSize: CGSize(width: 100, height: 100))
// Convert image to NSTextAttachment
let attachment = NSTextAttachment()
attachment.image = resizedImage
// Set bounds for the image
attachment.bounds = CGRect(x: 0, y: 0, width: 100, height: 100)
// Create attributed string with the attachment
let attributedString = NSMutableAttributedString(string: text)
let attachmentString = NSAttributedString(attachment: attachment)
attributedString.append(attachmentString)
// Add red border around the image
attributedString.addAttribute(.strokeColor, value: UIColor.red, range: NSRange(location: attributedString.length - attachmentString.length, length: attachmentString.length))
attributedString.addAttribute(.strokeWidth, value: -2.0, range: NSRange(location: attributedString.length - attachmentString.length, length: attachmentString.length))
// Set previewAttributedString for preview
self.previewAttributedString = attributedString
// Convert attributed string to RTF data
guard let rtfData = try? attributedString.data(from: NSRange(location: 0, length: attributedString.length),
documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf]) else {
print("Failed to create RTF data")
isLoading = false
return
}
self.rtfData = rtfData
isLoading = false
showExportSheet = true
// Debug: Save RTF to a file in the Documents directory
saveRTFToDocuments(rtfData)
}
func resizeImage(image: UIImage, targetSize: CGSize) -> UIImage {
let size = image.size
let widthRatio = targetSize.width / size.width
let heightRatio = targetSize.height / size.height
let newSize: CGSize
if widthRatio > heightRatio {
newSize = CGSize(width: size.width * heightRatio, height: size.height * heightRatio)
} else {
newSize = CGSize(width: size.width * widthRatio, height: size.height * widthRatio)
}
let rect = CGRect(origin: .zero, size: newSize)
UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0)
image.draw(in: rect)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage ?? UIImage()
}
func saveRTFToDocuments(_ data: Data) {
let fileManager = FileManager.default
guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
print("Unable to access Documents directory")
return
}
let fileURL = documentsDirectory.appendingPathComponent("debug_output.rtf")
do {
try data.write(to: fileURL)
print("Debug RTF file saved to: \(fileURL.path)")
} catch {
print("Error saving debug RTF file: \(error)")
}
}
}
struct DocumentPicker: UIViewControllerRepresentable {
@Binding var rtfData: Data?
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("document_with_image.rtf")
do {
try rtfData?.write(to: tempURL)
} catch {
print("Error writing RTF file: \(error)")
}
let picker = UIDocumentPickerViewController(forExporting: [tempURL], asCopy: true)
return picker
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {}
}
struct TextView: UIViewRepresentable {
let attributedString: NSAttributedString
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.isEditable = false
textView.attributedText = attributedString
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.attributedText = attributedString
}
}
Thank in advance!
To save images in a rich text document, you need to save the file as an RTFD file. An RTFD file is an RTF file with support for attachments, such as image files.
You set the document type to RTF in the createRTFWithEmbeddedImage
function.
[.documentType: NSAttributedString.DocumentType.rtf])
If you change the document type to NSAttributedString.DocumentType.rtfd
, the file should save as an RTFD file.
I have not tried saving an attributed string with an image to a file so I can't guarantee there isn't more you have to do to get the image to save. But saving the file as RTFD is a start.