Swapster
Swapster

Reputation: 11

Swiftui UIViewRepresentable - How to update PKCanvasView image when imageData source is changed on user action

I am creating a SwiftUI app with PencilKit & UIViewRepresentable. I am adding a image coming from server on top of the PKCanvasView. Code works fine when I click on a image and open a canvas view then close it and reopen it. But I am not able load a next image by having a next button on same screen to load a next image in array as a foreground of a CanvasView (like a image carousel). Since makeUIView in UIViewRepresentable loads only once, unless I close the view and reopen, canvas view does not update with next image when I am clicking on next button.

I tried sending @Binding Data or by accessing a @Published data from Environment Object to a UIViewRepresentable struct.

I tried adding edit mode on and off button and updating a view through updateUIView function and it works fine. But I am not able to update the next image in array. I am sure I must be doing something stupid

I am fairly new in SwiftUI, please call out any stupid code here.

struct HomeView: View {
    
    // MARK: -  Properties
    
    @EnvironmentObject var viewModel: ViewModels.ImageDataViewModel
    @Environment(\.dismiss) var dismiss
    @Binding var name: String
    @State var editable: Bool = true
    
    var body: some View {

// currentImageData is a @Published property coming from viewmodel

        if let currentSelectedImageData = viewModel.currentImageData {
            VStack {
// Display image number text, pause/UnPause editing and a Next button to go to next image
                HStack {
                    Text("Image Number: \(currentSelectedImageData.number)")
                        .font(.largeTitle)
                        .foregroundColor(.white)
                        .frame(height: 100)
                        .frame(maxWidth: .infinity)
                    
                    Button {
                        editable.toggle()
                    } label: {
                        Image(systemName: "pencil")
                            .resizable()
                            .frame(width: 40, height: 40)
                            .padding()
                    }

// Next button will perform Save currently edited image, remove it from array and set next image // as currentSelectedImage to show next image. 
                    Button {
                        DispatchQueue.main.async {
                            viewModel.saveImage()
                            viewModel.removeEditedImage(imageId: currentSelectedImageData.id)
                        }
                    } label: {
                        Image(systemName: "arrow.right.circle.fill")
                            .resizable()
                            .frame(width: 40, height: 40)
                            .padding()
                    }
                    
                }
                // Canvas to load PKCanvas and image as a foreground
                Canvas(editable: $editable)
                    .environmentObject(viewModel)
                    .frame(maxHeight: .infinity)            }
            
        } else {
// When all the images are finished from stack.
            Text("There are No images left to edit")
        }
    }
}


struct Canvas: View {
    @EnvironmentObject var viewModel: ViewModels.ImageDataViewModel
    @Binding var editable: Bool
    
    var body: some View {
        VStack {
            // Drawing View
            GeometryReader { proxy -> AnyView in
                let size = proxy.frame(in: .global).size
                DispatchQueue.main.async {
                    if viewModel.rect == .zero {
                        viewModel.rect = proxy.frame(in: .global)
                    }
                }
                return AnyView(
                    // Pencil kit drawing view
                    DrawingView(canvas: $viewModel.canvas, imageData: $viewModel.imageData, editable: $editable, toolPicker: $viewModel.toolPicker, rect: size)
                        .environmentObject($viewModel)
                )
            }
            
        }
    }
}
struct DrawingView: UIViewRepresentable {

    @Binding var canvas: PKCanvasView
    @Binding var imageData: Data
    @Binding var editable: Bool
    @Binding var toolPicker: PKToolPicker

    // View size
    var rect: CGSize

    func makeUIView(context: Context) -> PKCanvasView {
        canvas.drawingPolicy = .anyInput
        canvas.backgroundColor = .clear
        canvas.isOpaque = false
        print("Make canvas with image")
        // append image in canvas subview
        if let image = UIImage(data: imageData) {
            let imageView = UIImageView(image: image)
            imageView.frame = CGRect(x: 0, y: 0, width: rect.width, height: rect.height)
            imageView.contentMode = .scaleAspectFit
            imageView.clipsToBounds = true
            
            // Setting image to the back of the canvas
            let subView = canvas.subviews[0]
            subView.addSubview(imageView)
            subView.sendSubviewToBack(imageView)

            // Show tool picker
            // Add tool picker visible and first responder
            toolPicker.setVisible(true, forFirstResponder: canvas)
            toolPicker.addObserver(canvas)
            canvas.becomeFirstResponder()
        }
        return canvas
    }

    func updateUIView(_ uiView: PKCanvasView, context: Context) {
        print("Update canvas")
        uiView.drawingPolicy = editable ? .anyInput : .pencilOnly
        uiView.isUserInteractionEnabled = editable
         
         toolPicker.setVisible(editable, forFirstResponder: uiView)
         toolPicker.addObserver(uiView)
    }
}

Upvotes: 1

Views: 909

Answers (2)

Ioannis
Ioannis

Reputation: 21

I am facing a similar problem.. I use almost the same DrawingView: UIViewRepresentable and my issue is that when the image/photo that is loaded is taken in portrait mode with the camera, the DrawingView will rotate the image to the right. I found out how to rotate the UIIMage but the view will not refresh unless I close and open again... In my understanding the updateUIView understands changes in the canvas but not in the subview..

Update: Ok I solved it. As Swapster mentioned, I replaced the image in the UIIViewupdate method. Just a note for future adventurers, I used the below to remove the existing subView

canvas.subviews[0].subviews[0].removeFromSuperview()

Upvotes: 0

malhal
malhal

Reputation: 30746

There are a few mistakes

  1. we don't use view model objects in SwiftUI, that is what the View struct is for.
  2. There should be no need to use DispatchQueue.main.async.
  3. makeUIView needs to init and return the UIView, it should not come from somewhere else or be stored in a var in the struct.
  4. updateUIView needs to update the UIView only with things that have changed since the last time update was called.

Upvotes: 0

Related Questions