Using Swift UI Canvas

Hello,

I am trying to use the SwiftUI canvas and I'm having a couple of issues. One is that I'd like to be able to fit a canvas view in a ScrollView, but when the canvas is inside a ScrolLView, nothing draws. Additionally, I'm trying to draw lines on the canvas on Appear, but I can't seem to get that to work either. Below is a simplified version of the code I have:

struct Line: Codable, Hashable {
    var points: [CGPoint]
    var colorName: String?
    var rgb: [Double]?
    
    private enum CodingKeys: String, CodingKey {
        case points
        case colorName
        case rgb
    }
}

extension CGPoint: Hashable {
    public func hash(into hasher: inout Hasher) {
        hasher.combine(x)
        hasher.combine(y)
    }
}

struct CanvasExample: View {
    @State var lines: [Line] = []
    @State var selectedColor: Color = Color.black
    
    var body: some View {
        Canvas { ctx, size in
            for line in lines {
                var path = Path()
                path.addLines(line.points)
                
                ctx.stroke(path, with: .color(line.color), style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
            }
        }.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
            .onChanged({ value in
                let position = value.location
                
                if value.translation == .zero {
                    lines.append(Line(points: [position], color: selectedColor))
                } else {
                    guard let lastIndex = lines.indices.last else {
                        return
                    }
                    
                    lines[lastIndex].points.append(position)
                }
            })
          )
        .onAppear {
            lines.append(Line(points: [position], color: .black))
        }
    }
}

Any help would be greatly appreciated. Thank you.

Several errors that cause this code not to compile.

                ctx.stroke(path, with: .color(line.color), style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))

Value of type 'Line' has no member 'color'

                    lines.append(Line(points: [position], color: selectedColor))

Extra argument 'color' in call

        .onAppear {
            lines.append(Line(points: [position], color: .black))
        }

Cannot convert value of type '(CGPoint) -> some View' to expected element type 'Array<CGPoint>.ArrayLiteralElement' (aka 'CGPoint')

Extra argument 'color' in call

In addition, position is not defined in the scope of onAppear.

Why do you need Line to be Coddle ? In fact, as defined, it does not conform to the protocol.

Where is the scrollView ?

Please correct it so that can test.

Here is how I changed to test, but without ScrollView:

struct Line: /*Codable, */Hashable {
    var points: [CGPoint] = []
    var colorName: String?
    var rgb: [Double]?
    var color: Color?
    
//    private enum CodingKeys: String, CodingKey {
//        case points
//        case colorName
//        case rgb
//    }

}

extension CGPoint: Hashable {
    public func hash(into hasher: inout Hasher) {
        hasher.combine(x)
        hasher.combine(y)
    }
}

struct CanvasExample: View {
    @State var lines: [Line] = []
    @State var selectedColor: Color = Color.black
    
    var body: some View {
        Canvas { ctx, size in
            for line in lines {
                var path = Path()
                path.addLines(line.points)
                
                ctx.stroke(path, with: .color(line.color ?? Color.red), style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
            }
        }.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
            .onChanged({ value in
                let position = value.location
                
                if value.translation == .zero {
                    lines.append(Line(points: [position], color: selectedColor))
                } else {
                    guard let lastIndex = lines.indices.last else {
                        return
                    }
                    
                    lines[lastIndex].points.append(position)
                }
            })
          )
        .onAppear {
            lines.append(Line(points: [CGPoint.zero/*position*/], color: Color.black))
        }
    }
}

@Claude31 Thank you for your response. Below is revised code, with the ScrollView implemented, so the drawing aspect isn't going to work unless that's removed. I put a comment in .onAppear { } where I'm assuming code needs to be written to plot the points from lines onto the canvas. I looked at the official documentation for canvas' that draws upon loading but I can't seem to figure out how to apply it to what I'm doing.

import SwiftUI

struct LineData {
    var points: [CGPoint]
    var color: Color
}

struct CanvasExample: View {
    @State var lines: [LineData] = []
    @State var selectedColor: Color = Color.black
    
    var body: some View {
        ScrollView (showsIndicators: false) {
            VStack {
                Canvas { ctx, size in
                    for line in lines {
                        var path = Path()
                        path.addLines(line.points)
                        
                        ctx.stroke(path, with: .color(line.color), style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
                    }
                }.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
                    .onChanged({ value in
                        let position = value.location
                        
                        if value.translation == .zero {
                            lines.append(LineData(points: [position], color: selectedColor))
                        } else {
                            guard let lastIndex = lines.indices.last else {
                                return
                            }
                            
                            lines[lastIndex].points.append(position)
                        }
                    })
                  )
                .onAppear {
                    // code that takes data passed to variable lines and draws on canvas
                }
            }
        }
    }
}

#Preview {
    CanvasExample()
}

Thank you.

One thing I have noticed is the Canvas code doesn't run until there's a gesture, and I'm not sure why that is because the documentation has an example that displays a drawing upon loading the Canvas.

struct Line {
    var points: [CGPoint]
    var color: Color
}

var body: some View {
@State var lines: [Line] = []

Canvas { ctx, size in
        for line in lines {
         let color = line.colorName == nil ? getColor(colorName: nil, rgb: line.rgb) : getColor(colorName: line.colorName, rgb: nil)
                        var path = Path()
                        
                        print(line.points) // this is how I noticed this code doesn't run until the gesture runs. This print statement doesn't run upon loading the Canvas
                        
                        path.addLines(line.points)
                            
                        ctx.stroke(path, with: .color(color), style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
                    }
                }
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
                    .onChanged({ value in
                            draw(position: value.location, translation: value.translation)
                        }
                    })
                )
.onAppear {
                        lines = parseLinesString()
                       
                        for line in lines {
                            for point in line.points {
                                draw(position: point, translation: nil) 
                            }
                        }
                    }

func draw(position: CGPoint, translation: CGSize?) {
        if translation == .zero || translation == nil {
            lines.append(Line(points: [position], color: color.black))
        } else {
            guard let lastIndex = lines.indices.last else {
                return
            }
            
            lines[lastIndex].points.append(position)
            
            unsaved = true
        }
    }
                
}

As I mentioned previously, I'm not sure how to translate the documentation example to my application. If anyone could point me in the right direction that would be greatly appreciated. Thank you.

One thing I now realize is that it's not practical to have a Canvas inside a ScrollView because it interferes with the drawing gesture. However, I'm still unsure of how to draw on a Canvas upon loading.

Using Swift UI Canvas
 
 
Q