1

I am trying to share a subview as an image. This question combines code from these two questions conversion of a subview to an image and showing a UIActivityController in swift.

The view and minimal working example are below. I am trying to share SubView, here, the rectangle with the text as an image. The problem is that self.uiimage evaluates to nil whenever I press the button and causes the app to crash.

I know that an image can be created because I can add this line of code UIImageWriteToSavedPhotosAlbum(self.uiimage!, nil, nil, nil) in the action of the button and the image gets saved to the photos album.

My best guess is that the ActivityViewController is created when the entire UI is first rendered, at which point self.uiimage would be nil. I suppose I could make uiimage a binding property to ActivityViewController, but I would rather not do that since it would complicate creating the activityItems in the parent controller. Any ideas on what I can do to fix this?

import SwiftUI

struct SubView: View {
    var body: some View {
        VStack{
            Text("Hello, world!")
                .padding()
            Rectangle()
                .foregroundColor(.blue)
        }
        
    }
}

struct ContentView: View {
    @State private var isSharePresented: Bool = false
    @State private var rect: CGRect = .zero
    @State private var uiimage: UIImage? = nil
    
    var body: some View {
        SubView()
            .frame(width: 150, height: 150)
            .background(RectGetter(rect: $rect))
        
        Button(action: {
            self.uiimage = UIApplication.shared.windows[0].rootViewController?.view.asImage(rect: self.rect)
            self.isSharePresented = true
        }) {
            Label("Share", systemImage: "square.and.arrow.up")
        }
        .sheet(isPresented: $isSharePresented, onDismiss: {
            print("Dismiss")
        }, content: {
            ActivityViewController(activityItems: [self.uiimage!])
        })
    }
}

struct ActivityViewController: UIViewControllerRepresentable {

    var activityItems: [Any]
    var applicationActivities: [UIActivity]? = nil
    @Environment(\.presentationMode) var presentationMode

    func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
        let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
        controller.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
            self.presentationMode.wrappedValue.dismiss()
        }
        return controller
    }

    func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
}

struct RectGetter: View {
    @Binding var rect: CGRect

    var body: some View {
        GeometryReader { proxy in
            self.createView(proxy: proxy)
        }
    }

    func createView(proxy: GeometryProxy) -> some View {
        DispatchQueue.main.async {
            self.rect = proxy.frame(in: .global)
        }

        return Rectangle().fill(Color.clear)
    }
}

extension UIView {
    func asImage(rect: CGRect) -> UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: rect)
        return renderer.image { rendererContext in
            layer.render(in: rendererContext.cgContext)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
K. Shores
  • 875
  • 1
  • 18
  • 46

1 Answers1

3

My best guess is that the ActivityViewController is created when the entire UI is first rendered

That's pretty much exactly the problem — SwiftUI sheets sometimes get pre-rendered, before your uiimage is set. Especially when you're working with optionals, sheet(item:onDismiss:content:) is the more reliable modifier.

The only requirement is to make the image conform to Identifiable. The easiest way to do this is probably to make a wrapper struct.

/// Conform to Identifiable.
struct ImageWrapper: Identifiable {
    let id = UUID()
    var image: UIImage
}

struct ContentView: View {
    @State private var rect: CGRect = .zero
    @State private var imageWrapper: ImageWrapper?
    
    var body: some View {
        SubView()
            .frame(width: 150, height: 150)
            .background(RectGetter(rect: $rect))
        
        Button(action: {
            if let image = UIApplication.shared.windows[0].rootViewController?.view.asImage(rect: self.rect) {
                self.imageWrapper = ImageWrapper(image: image)
            }
        }) {
            Label("Share", systemImage: "square.and.arrow.up")
        }
        
        /// When `imageWrapper` is not nil, the sheet will be presented.
        .sheet(item: $imageWrapper) { imageWrapper in
            ActivityViewController(activityItems: [imageWrapper.image])
        }
    }
}

Result:

Image is not nil and the activity view controller is presented

aheze
  • 24,434
  • 8
  • 68
  • 125