Swift UI exporting content of canvas

Viewed 201

I have a canvas where the user can draw stuff on it. I want to export whatever the user draw on my canvas and I'm using the following extension to get images out of Views

extension View {
    func snapshot() -> 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)

        return renderer.image { _ in
            view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
        }
    }
}

But unfortunatelly this extension works fine for buttons on any simple view. Whenever I use it for my canvas, it gives me a blank image, it appears it doesn't get the content of my canvas.

Is it possible to export my canvas content?

Is it because I'm drawing my canvas with a stroke?

This is how I'm using my canvas:

    @State private var currentLine = Line(color: .red)
    @State private var drawedLines = [Line]()
    @State private var selectedColor: Color = .red
    @State private var selectedSize = 1.0
    private var colors:[Color] = [.red, .green, .blue, .black, .orange, .yellow, .brown, .pink, .purple, .indigo, .cyan, .mint]

    var canvasPallete: some View {
        return Canvas { context, size in
            for line in drawedLines {
                var path = Path()
                path.addLines(line.points)
                context.stroke(path, with: .color(line.color), lineWidth: line.size)
            }

        }.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
            .onChanged({ value in
                let point = value.location
                currentLine.points.append(point)
                currentLine.color = selectedColor
                currentLine.size = selectedSize
                drawedLines.append(currentLine)
            })
            .onEnded({ value in
                currentLine = Line(color: selectedColor)
             })
        )
    }

    var body: some View {
        VStack {
            canvasPallete
                .frame(width: 300, height: 300)
            Divider()
            HStack(alignment: .bottom ) {
                VStack{
                    Button {
                        print("Will save draw image")
                        let image = canvasPallete.snapshot()
                        
                        UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
                    } label: {
                        Text("Save")
                    }
                    .padding([.bottom], 25)

                    Button {
                        drawedLines.removeAll()
                    } label: {
                        Image(systemName: "trash.circle.fill")
                            .font(.system(size: 40))
                            .foregroundColor(.red)
                    }.frame(width: 70, height: 50, alignment: .center)
                }
                VStack(alignment: .leading) {
                    Text("Colors:")
                    ScrollView(.horizontal, showsIndicators: false) {
                        HStack(spacing: 4) {
                            ForEach(colors, id:\.self){color in
                                ColoredCircleButton(color: color, selected: color == selectedColor) {
                                    selectedColor = color
                                }
                            }

                        }
                        .padding(.all, 5)

                    }
                    Text("Size:")
                    Slider(value: $selectedSize, in: 0.2...8)
                        .padding([.leading,.trailing], 20)
                        .tint(selectedColor)
                }
                .padding([.bottom], 10)

            }


        }.frame(minWidth: 400, minHeight: 400)
    }
}


    struct ColoredCircleButton: View {
        let color: Color
        var selected = false
        let onTap: (() -> Void)
    
        var body: some View {
            Circle()
                .frame(width: 25, height: 25)
                .foregroundColor(color)
                .overlay(content: {
                    if selected {
                        Circle().stroke(.gray, lineWidth: 3)
                    }
                })
                .onTapGesture {
                    onTap()
                }
        }
    }
1 Answers

This works fine!

The issue you are encountering is that in your sample code, canvasPallete didn't get a frame modifier when snapshot() was called on it. So it got rendered at a size that's not what you want, probably (0,0).

If you change the snapshot line it will work:

let image = canvasPallete
    .frame(width: 300, height: 300)
    .snapshot()

One more thing! In iOS 16, there's new API for rendering a view to an Image. So for iOS 16 and up, you can skip the View extension and write:

let renderer = ImageRenderer(content: canvasPallete)
if let image = renderer.uiImage {
    UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
Related