1

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!

RRR
  • 71
  • 7
  • Does this answer your question https://stackoverflow.com/a/60305818/12299030? – Asperi Sep 17 '21 at 20:44
  • @Asperi Thank you! that got me a step closer. However, I'm still getting something wrong. When I run it, I get the warning "Modifying state during view update, this will cause undefined behavior." I've updated the code above to show what changes I've made. – RRR Sep 17 '21 at 22:56

1 Answers1

1

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)
            }
    }

}
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220