Attempting to Screenshot Chart in UIImage, LineChart's lines and points disappear?
gesabo opened this issue · 4 comments
Using the code below I'm attempting to build a share feature to share the chart (the demo uses the LineChartDemoView from the example app) however when you try and capture the view (tap on the share button), the lines and points are missing on the SwiftUIChart in the UIImage
. Is there a way around this issue? I thought it might be related to the animation but setting the animation
to nil
didn't change the behavior.
import SwiftUI
//construct enum to decide which sheet to present:
enum ActiveSheet: String, Identifiable { // <--- note that it's now Identifiable
case photoLibrary, shareSheet
var id: String {
return self.rawValue
}
}
struct ShareHomeView: View {
@State private var shareCardAsImage: UIImage? = nil
@State var activeSheet: ActiveSheet? = nil // <--- now an optional property
var shareCard: some View {
LineChartDemoView()
.frame(height: 350)
}
var body: some View {
NavigationView {
VStack {
shareCard
Button(action: {
shareCardAsImage = shareCard.asImage()
self.activeSheet = .shareSheet
}) {
HStack {
Image(systemName: "square.and.arrow.up")
.font(.system(size: 20))
Text("Share")
.font(.headline)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 50, maxHeight: 50)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(20)
}
.padding(.horizontal)
} //End of Master VStack
//sheet choosing view to display based on selected enum value:
.sheet(item: $activeSheet) { [shareCardAsImage] sheet in // <--- sheet is of type ActiveSheet and lets you present the appropriate sheet based on which is active
switch sheet {
case .photoLibrary:
Text("TODO")
case .shareSheet:
if let unwrappedImage = shareCardAsImage {
ShareSheet(photo: unwrappedImage)
}
}
}
//Needed to Wrap in a Navigation View and hide title so that dark mode would work, otherwise this sheet was always in the iPhone's light or dark mode
.navigationBarHidden(true)
.navigationTitle("")
}
}
}
struct RecoveryShareHomeView_Previews: PreviewProvider {
static var previews: some View {
ShareHomeView().preferredColorScheme(.dark)
ShareHomeView().preferredColorScheme(.light)
}
}
extension View {
func asImage() -> UIImage {
let controller = UIHostingController(rootView: self)
// locate far out of screen
controller.view.frame = CGRect(x: 0, y: CGFloat(Int.max), width: 1, height: 1)
UIApplication.shared.keyWindow!.rootViewController?.view.addSubview(controller.view)
let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
controller.view.bounds = CGRect(origin: .zero, size: size)
controller.view.sizeToFit()
let image = controller.view.asImage()
controller.view.removeFromSuperview()
return image
}
}
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
// [!!] Uncomment to clip resulting image
// rendererContext.cgContext.addPath(
// UIBezierPath(roundedRect: bounds, cornerRadius: 20).cgPath)
// rendererContext.cgContext.clip()
// As commented by @MaxIsom below in some cases might be needed
// to make this asynchronously, so uncomment below DispatchQueue
// if you'd same met crash
// DispatchQueue.main.async {
layer.render(in: rendererContext.cgContext)
// }
}
}
}
extension UIApplication {
var keyWindow: UIWindow? {
// Get connected scenes
return UIApplication.shared.connectedScenes
// Keep only active scenes, onscreen and visible to the user
.filter { $0.activationState == .foregroundActive }
// Keep only the first `UIWindowScene`
.first(where: { $0 is UIWindowScene })
// Get its associated windows
.flatMap({ $0 as? UIWindowScene })?.windows
// Finally, keep only the key window
.first(where: \.isKeyWindow)
}
}
import LinkPresentation
//This code is from https://gist.github.com/tsuzukihashi/d08fce005a8d892741f4cf965533bd56
struct ShareSheet: UIViewControllerRepresentable {
let photo: UIImage
func makeUIViewController(context: Context) -> UIActivityViewController {
//let text = ""
//let itemSource = ShareActivityItemSource(shareText: text, shareImage: photo)
let activityItems: [Any] = [photo]
let controller = UIActivityViewController(
activityItems: activityItems,
applicationActivities: nil)
return controller
}
func updateUIViewController(_ vc: UIActivityViewController, context: Context) {
}
}
@zcjhnsn no I never did. Ended up using another charts lib for the share function, would love to figure it out though!
@gesabo I got it to work but it feels hacky. Had to "disable" the chart animation using the .transaction
modifier on the superview. Then used a DispatchQueue.main.asyncAfter
block to wait to capture the screenshot until the animation finished. It'll be much cleaner once they officially add support to disable chart animation.
Hey @gesabo and @zcjhnsn, on branch https://github.com/willdale/SwiftUICharts/tree/189-screenshot-chart-in-uiimage-linecharts, is a proposed fix for it. Currently, it is only tested on Line Chart.
Feel free to have a go and offer feedback, I'll look to add in the other chart types and to make it work on iOS 14.
Below is the client side code based off of @gesabo.
Add .disableAnimation(chartData: data)
to the view that will be rendered. - Probably add it just after the chart so it is at the top.
Bear in mind that the view to render doesn't have to be the view that is being displayed - you can tweak the view to be rendered as needed.
#if canImport(UIKit)
enum ActiveSheet: String, Identifiable {
case photoLibrary, shareSheet
var id: String {
return self.rawValue
}
}
struct ShareHomeView: View {
var chartImageController = ChartImageController()
@State var showImage = false
@State var image = UIImage()
@State var bag = Set<AnyCancellable>()
@State private var shareCardAsImage: UIImage? = nil
@State var activeSheet: ActiveSheet? = nil
var shareCard: some View {
LineChartDemoView()
.frame(width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.height)
}
var body: some View {
Button {
let controller = ChartImageHostingController(rootView: shareCard)
controller.finalImage
.sink { completion in
self.chartImageController.controller = nil
} receiveValue: { image in
self.shareCardAsImage = image
self.activeSheet = .shareSheet
}
.store(in: &bag)
controller.start()
chartImageController.controller = controller
} label: {
HStack {
Image(systemName: "square.and.arrow.up")
.font(.system(size: 20))
Text("Share")
.font(.headline)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 50, maxHeight: 50)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(20)
}
.padding(.horizontal)
.sheet(item: $activeSheet) { [shareCardAsImage] sheet in
switch sheet {
case .photoLibrary:
Text("TODO")
case .shareSheet:
if let unwrappedImage = shareCardAsImage {
ShareSheet(photo: unwrappedImage)
}
}
}
}
}
struct RecoveryShareHomeView_Previews: PreviewProvider {
static var previews: some View {
ShareHomeView().preferredColorScheme(.dark)
ShareHomeView().preferredColorScheme(.light)
}
}
#endif
import LinkPresentation
//This code is from https://gist.github.com/tsuzukihashi/d08fce005a8d892741f4cf965533bd56
struct ShareSheet: UIViewControllerRepresentable {
let photo: UIImage
func makeUIViewController(context: Context) -> UIActivityViewController {
let activityItems: [Any] = [photo]
return UIActivityViewController(
activityItems: activityItems,
applicationActivities: nil
)
}
func updateUIViewController(_ vc: UIActivityViewController, context: Context) {
}
}