Search code examples
iosswiftuicore-imagexcode12

In SwiftUI, how do I continuously update a view while a gesture is being preformed?


This is part of an ongoing attempt at teaching myself how to create a basic painting app in iOS, like MSPaint. I'm using SwiftUI and CoreImage to do this.

While I have my mind wrapped around the pixel manipulation in CoreImage (I've been looking at this), I'm not sure how to add a drag gesture to SwiftUI so that I can "paint".

With the drag gesture, I'd like to do this:

onBegin and onChanged:

  • send the current x,y position of my finger to the function handling the CoreImage manipulation;
  • receive and display the updated image;
  • repeat until gesture ends.

So in other words, continuously update the image as my finger moves.

UPDATE: I've taken a look at what Asperi below responded with, and added .gesture below .onAppear. However, this results in a warning "Modifying state during view update, this will cause undefined behavior."

struct ContentView: View {
    @State private var image: Image?
    @GestureState var location = CGPoint(x: 0, y: 0)

    var body: some View {
        VStack {
            image?
                .resizable()
                .scaledToFit()
        }
        .onAppear(perform: newPainting)
        .gesture(
            DragGesture()
                .updating($location) { (value, gestureState, transaction) in
                    gestureState = value.location
                    paint(location: location)
                }
        )
    }

    func newPainting() {
        guard let newPainting = createBlankCanvas(size: CGSize(width: 128, height: 128)) else {
            print("failed to create a blank canvas")
            return
        }
        
        image = Image(uiImage: newPainting)
    }

    func createBlankCanvas(size: CGSize, filledWithColor color: UIColor = UIColor.clear, scale: CGFloat = 0.0, opaque: Bool = false) -> UIImage? {

        let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)

        UIGraphicsBeginImageContextWithOptions(size, opaque, scale)
        color.set()
        UIRectFill(rect)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        
        return image
    }

    func paint(location: CGPoint) {

    // do the CoreImage manipulation here

    // now, take the output of the CI manipulation and
    // attempt to get a CGImage from our CIImage

            if let cgimg = context.createCGImage(outputImage, from: outputImage.extent) {
                // convert that to a UIImage
                let uiImage = UIImage(cgImage: cgimg)

                // and convert that to a SwiftUI image
                image = Image(uiImage: uiImage) // <- Modifying state during view update, this will cause undefined behavior.
            }
    } 

}

Where do I add the gesture and have it repeatedly call the paint() func? How do I get view to update continuously as long as the gesture continues?

Thank you!


Solution

  • You shouldn't store SwiftUI views(like Image) inside @State variables. Instead you should store UIImage:

    struct ContentView: View {
        @State private var uiImage: UIImage?
        @GestureState var location = CGPoint(x: 0, y: 0)
    
        var body: some View {
            VStack {
                uiImage.map { uiImage in
                    Image(uiImage: uiImage)
                        .resizable()
                        .scaledToFit()
                }
            }
            .onAppear(perform: newPainting)
            .gesture(
                DragGesture()
                    .updating($location) { (value, gestureState, transaction) in
                        gestureState = value.location
                        paint(location: location)
                    }
            )
        }
    
        func newPainting() {
            guard let newPainting = createBlankCanvas(size: CGSize(width: 128, height: 128)) else {
                print("failed to create a blank canvas")
                return
            }
            
            uiImage = newPainting
        }
    
        func createBlankCanvas(size: CGSize, filledWithColor color: UIColor = UIColor.clear, scale: CGFloat = 0.0, opaque: Bool = false) -> UIImage? {
    
            let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
    
            UIGraphicsBeginImageContextWithOptions(size, opaque, scale)
            color.set()
            UIRectFill(rect)
            let image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            
            return image
        }
    
        func paint(location: CGPoint) {
    
        // do the CoreImage manipulation here
    
        // now, take the output of the CI manipulation and
        // attempt to get a CGImage from our CIImage
    
                if let cgimg = context.createCGImage(outputImage, from: outputImage.extent) {
                    // convert that to a UIImage
                    uiImage = UIImage(cgImage: cgimg)
                }
        }
    
    }