Greetings, traveler!
SwiftUI offers a great tool to render your views into images or PDFs. I am talking about ImageRenderer
.
Image
Let’s take a closer look at some examples then. Frist, let’s render an image. Create an extension for View
to use ImageRenerer
. Here, we should provide a display scale. To this, we can use a SwiftUI environment value displayScale
.
extension View {
@MainActor
func renderImage(scale displayScale: CGFloat) -> UIImage? {
let renderer = ImageRenderer(content: self)
renderer.scale = displayScale
return renderer.uiImage
}
}
Then, let’s create a class to manage image rendering. To save an image to the pasteboard, we can create such a method:
@MainActor
final class ImageSaver {
func saveToPasteboard(view: some View, scale: CGFloat) {
guard let uiImage = view.renderImage(scale: scale) else { return }
UIPasteboard.general.image = uiImage
}
}
To save an image to the Photo Album, we can use the UIImageWriteToSavedPhotosAlbum
function. To use its completion, we can create a separate method:
@MainActor
final class ImageSaver {
private var completion: (() -> Void)?
func saveToPhotoAlbum(view: some View, scale: CGFloat, _ completion: (() -> Void)?) {
guard let uiImage = view.renderImage(scale: scale) else { return }
self.completion = completion
UIImageWriteToSavedPhotosAlbum(uiImage, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil)
}
@objc
private func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) {
if let error {
print(error.localizedDescription)
}
completion?()
}
}
Usage
Let’s try it out!
struct ContentView: View {
@Environment(\.displayScale) private var displayScale
private let imageSaver = ImageSaver()
var body: some View {
Button(action: renderImage) {
Text("Render Image")
}
}
private func renderImage() {
imageSaver.saveToPasteboard(view: body, scale: displayScale)
imageSaver.saveToPhotoAlbum(view: body, scale: displayScale) {
print("finished")
}
}
}
Now, let’s deal with PDFs. First, we should provide the view. The next step is to create a URL
to store a file. Then, we can render an image and use a CGContext
to generate the PDF page. As the last step, we can end the page and close the PDF file.
extension View {
func renderPDF(scale displayScale: CGFloat) -> URL {
let renderer = ImageRenderer(content: self)
renderer.scale = displayScale
let url = URL.documentsDirectory.appending(path: "myPDF.pdf")
renderer.render { size, context in
var container = CGRect(
x: .zero,
y: .zero,
width: size.width,
height: size.height
)
guard let pdf = CGContext(url as CFURL, mediaBox: &container, nil) else { return }
pdf.beginPDFPage(nil)
context(pdf)
pdf.endPDFPage()
pdf.closePDF()
}
return url
}
}
Now, let’s create the PDFSaver
class.
@MainActor
final class PDFSaver {
func saveToDocuments(view: some View, scale: CGFloat) {
let url = view.renderPDF(scale: scale)
print(url)
}
}
Usage
Let’s test it!
struct ContentView: View {
@Environment(\.displayScale) private var displayScale
private let pdfSaver = PDFSaver()
var body: some View {
Button(action: renderPDF) {
Text("Render PDF")
}
}
private func renderPDF() {
pdfSaver.saveToDocuments(view: body, scale: displayScale)
}
Limitations
We can’t use view components made with UIKit, and we cannot use views created with the UIViewRepresentable
protocol.
struct LabelView: UIViewRepresentable {
var text: String
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.text = text
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 24)
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.text = text
}
}
If we do, we will gain a yellow placeholder with the 🚫 mark instead of the desired view.
struct ContentView: View {
@Environment(\.displayScale) private var displayScale
private let imageSaver = ImageSaver()
var body: some View {
Button(action: renderImage) { // Normal rendering ✅
Text("Render Image")
}
LabelView(text: "Some text") // Renders as a 🚫
}
private func renderImage() {
imageSaver.saveToPasteboard(view: body, scale: displayScale)
}
}
If you enjoyed this article, please feel free to follow me on my social media: