Reputation: 11
In SwiftUI, I would like to take a screenshot of a subview called ExtractedView. I'm using UIGraphicsImageRenderer to render the view as an image. However, when ExtractedView is too long, I encounter an error message. How can I fix this?
In SwiftUI, I want to take a screenshot of the child view ExtractedView. I use UIGraphicsImageRenderer to render the view as an Image. When the ExtractedView is too long, it outputs a blank Image.(But I need a long picture), and I get the following error message. How can I fix it
Run log:
Render server returned error for view (0x102804310, _TtGC7SwiftUI14_UIHostingViewV5Kylin13ExtractedView_).
imageSize : (462.0, 2803.3333333333335)
Body
var body: some View {
VStack {
Button(action: {
renderToImage()
}, label: {
Text("Snapshot")
})
ExtractedView()
}
}
ExtractedView
struct ExtractedView: View {
var body: some View {
ScrollView {
let contentStr = "About 2000 word long piece of text"
Text(contentStr)
.frame(maxWidth: UIScreen.main.bounds.size.width, maxHeight: .infinity)
.padding()
}
}
}
renderToImage:
@MainActor func renderToImage() -> UIImage? {
return ExtractedView()
.saveToImage()
}
saveToImage
extension View {
func saveToImage() -> UIImage {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
let image = renderer.image { render in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
return image
}
}
I tried to use the ImageRenderer for screen capture, but it only supports a limited set of components and does not support ScrollView or TextView, so I gave up on it
Upvotes: 1
Views: 564
Reputation: 1396
Using ImageRenderer
, I achieved a pretty neat image.
As you already mentioned, ImageRenderer
is limited in many ways (ScrollView, WebView, etc), my approach is to create a computed property of the content you want to screenshot without the ScrollView
, and then pass the content to ImageRenderer
.
ExtractedView
struct ExtractedView: View {
let contentStr = twokwords // I created some at https://ctxt.io/2/AABIE5oBFg
// 1. create a computed property for the content without the scroll view (text in this case)
private var myContentView: some View {
Text(contentStr)
}
var body: some View {
ScrollView {
// 2. Here, you can use the content as you previously did
myContentView
.frame(maxWidth: UIScreen.main.bounds.size.width, maxHeight: .infinity)
.padding()
}
}
// 3. This is just a helper to retrieve the content to screenshot
// You can remove `private` modifier on `myContentView` property and access it directly
func getViewToScreenshot() -> some View {
myContentView
}
}
struct ContentView: View {
@State private var screenshot: UIImage?
@Environment(\.displayScale) private var displayScale
var body: some View {
VStack {
if let screenshot {
Image(uiImage: screenshot)
.resizable()
.scaledToFit()
.frame(width: UIScreen.main.bounds.width)
.frame(maxHeight: .infinity)
.onTapGesture {
self.screenshot = nil
}
} else {
Button(action: {
if let img = renderToImage() {
self.screenshot = img
}
}, label: {
Text("Snapshot")
})
extractedView
}
}
}
private var extractedView: ExtractedView {
ExtractedView()
}
@MainActor func renderToImage() -> UIImage? {
// 1. Get the view you want and add any modifier you want (padding in my case)
let screenshot = extractedView.getViewToScreenshot().padding()
let renderer = ImageRenderer(content: screenshot)
// 2. make sure to use the correct display scale for the device
// Learn more at https://www.hackingwithswift.com/quick-start/swiftui/how-to-convert-a-swiftui-view-to-an-image
renderer.scale = displayScale
// 3. Set the proposedViewSize you want
var size = ProposedViewSize.infinity
size.width = 400 // 4. set the width you want for better result
renderer.proposedSize = size
if let uiImage = renderer.uiImage {
return uiImage
}
return nil
}
}
After some Trial and error, I was able to make the UIGraphicsImageRenderer
work as well, basically the approach is to use fixedSize
modifier to ensure the view does not compress or truncate vertically
extension View {
func saveToImage() -> UIImage {
// Apply the fixedSize modifier to ensure this view is at its ideal size (does not compress or truncate vertically in this case)
let controller = UIHostingController(rootView: self.fixedSize(horizontal: false, vertical: true))
guard let view = controller.view else { return .init() }
// Calculate the target size based on the intrinsic content size of the view
let targetSize = controller.sizeThatFits(in: .init(width: view.intrinsicContentSize.width, height: .greatestFiniteMagnitude))
view.bounds = CGRect(origin: .zero, size: targetSize)
view.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
let image = renderer.image { render in
view.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
return image
}
}
Let me know how it works out for you!
Upvotes: 1