Is there a way to make sure an image (screenshot from my app) is rendered before I launch a sheet trying to share that image?
I have a share sheet that I'm launching that takes a current "Card" in a view that shows a stack of cards, and then takes a screenshot of the card that is stored in a variable "questionScreenShot". This card is supposed to be shared along with the link - and I'm using a variation of Asperi's solution from this question to generate the image: How do I render a SwiftUI View that is not at the root hierarchy as a UIImage?
The card doesn't share the first time I click the "Share Question" button (I only get the link). However, if I keep running the app, and then click Share Question again, I get an image at that point.
I have an if statement that is supposed to launch the share sheet only if the image has a value. I have breakpoints before the ShareSheet launches, which should be going in a different direction since the image appears to still be null based on the value of "questionScreenShot" in the image below.
However, when the app actually launches the sheet, it says there is some value for the image in my printed output:
sharesheet has some value
sharesheet equals Optional(<UIImage:0x282fdb330 anonymous {300, 380}>)
Here's my file with the extensions of the view that generate the image:
import SwiftUI
import UIKit
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
self.layer.render(in: rendererContext.cgContext)
}
}
}
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.windows.first!.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()
controller.view.backgroundColor = .clear
let image = controller.view.asImage()
controller.view.removeFromSuperview()
return image
}
}
And here is my page structure, with the share sheet button, and the if statement that is only supposed to launch the share sheet if the image var has a value:
struct TopicPage: View {
//inherit current topic from the PackTopics page, and create a state variable that gets set to a random question.
var currentTopic: Topic
@State private var currentQuestions: [String]
@State private var showShareSheet = false
@State private var rect: CGRect = .zero
let appURL: String = "https://apps.apple.com/us/app/im-curious/id1518506383"
@State var questionScreenShot: UIImage? = nil
@State var shareQuestionVisible: Bool = true
var body: some View {
GeometryReader { geometry in
Blah Blah Blah
Spacer()
.sheet(isPresented: $showShareSheet, onDismiss: {
questionScreenShot = nil
print("Question Screenshot reset to \(questionScreenShot ?? nil)")
}) {
ShareSheet(activityItems: [questionScreenShot ?? nil, appURL])
}
}
//Set the background image.
.navigationBarItems(trailing:
Button(action: {
self.questionScreenShot = render()
if self.questionScreenShot != nil {
print("sharesheet has some value")
print("sharesheet equals \(questionScreenShot)")
self.showShareSheet = true
} else {
print("Did not set screenshot")
}
}) {
Text("Share Question").bold()
}.disabled(!shareQuestionVisible)
)
}
// Geometry Reader ignores all safe edges
.edgesIgnoringSafeArea(.all)
}
func removeQuestion(at question: Int) {
currentQuestions.remove(at: question)
if currentQuestions.count == 0 {
self.shareQuestionVisible = false
}
}
private func render() -> UIImage {
Card(color: currentTopic.color, text: self.currentQuestions[currentQuestions.count-1]).asImage()
}
}
Edit: Just tried Asperi's solution of wrapping the launch share sheet call in DispatchQueue.main.async, and the behavior seems to be about the same:
I tried changing the call to ShareSheet to match Asperi's suggestion, and the behavior is about the same. It doesn't launch the screenshot the first time, but then launches the screenshot the second time.
Spacer()
.sheet(isPresented: $showShareSheet, onDismiss: {
questionScreenShot = nil
print("Question Screenshot reset to \(questionScreenShot ?? nil)")
}) {
ShareSheet(activityItems: [questionScreenShot ?? nil, appURL])
}
}
//Set the background image.
.navigationBarItems(trailing:
Button(action: {
self.questionScreenShot = render()
print("Question Screenshot now set to \(questionScreenShot ?? nil)")
DispatchQueue.main.async {
self.showShareSheet = true
}
}) {
And here is the output I am getting, so it appears it's properly setting the screenshot both the first and second time, and then resetting it to nil when I dismiss the share sheet. I do get the CFMessagePort error, I'm not sure if that has anything to do with it.
Question Screenshot now set to Optional(<UIImage:0x162e24c80 anonymous {300, 380}>)
2020-11-02 07:20:02.917212-0600 Travel Conversation Starters[6492:694831] [PPT] Error creating the CFMessagePort needed to communicate with PPT.
Question Screenshot reset to nil
2020-11-02 07:20:12.731769-0600 Travel Conversation Starters[6492:695053] [ShareSheet] connection invalidated
Question Screenshot now set to Optional(<UIImage:0x162e1b970 anonymous {300, 380}>)
2020-11-02 07:20:20.276303-0600 Travel Conversation Starters[6492:694831] [PPT] Error creating the CFMessagePort needed to communicate with PPT.
Note: I did change the variable in my struct to point to an actual image in my Images.xcassets folder, and it loaded that image the first time instead. So it seems there is some issue with the image loading too slowly the first time.
@State var questionScreenShot: UIImage? = UIImage(named: "Test")