-1.9 C
New York
Friday, January 10, 2025

ios – SwiftUI Sticky Notes: Incorrect Place and Body Alignment in a Canvas-like View


I’m engaged on a SwiftUI mission the place I’m making an attempt to implement interactive Sticky Notes that may be moved, resized, and rotated on a Canvas-like view. The Sticky Notes must also have editable textual content fields. Nevertheless, I’ve encountered an issue the place:

  1. The place of the Sticky Notes doesn’t align with the seen notice on the display.

  2. When the Sticky Word is moved or resized, the interactive body (used for context menus or gestures) doesn’t observe the notice.

  3. The rotation and scaling transformations don’t apply constantly to the textual content or body.

I’ve tried debugging the difficulty by inspecting the frames and positions in each native and world coordinate areas, and it looks as if the worldwide offset of the Canvas is perhaps interfering with the Sticky Notes’ alignment.

I’ve my Code beneath:


struct StickyNoteView: View {
    @Binding var stickyNote: StickyNote
    @State non-public var isEditing: Bool = false
    var onDelete: () -> Void
    var onDuplicate: () -> Void

    var physique: some View {
        InteractiveElementView(factor: $stickyNote) {
            ZStack {
                Rectangle()
                    .fill(stickyNote.colour.toColor())
                SelectableTextEditor(textual content: $stickyNote.textual content, isEditing: $isEditing)
                    .padding()
                    .background(Colour.clear)
            }
            .highPriorityGesture(
                TapGesture(depend: 2).onEnded {
                    isEditing = true
                }
            )
        }
        .contextMenu {
            Button("Löschen") {
                onDelete()
            }
            Button("Duplizieren") {
                onDuplicate()
            }
        }
    }
}
import PencilKit

struct NotesDetailView: View {
    @Surroundings(.presentationMode) var presentationMode
    @Surroundings(NotesViewModel.self) non-public var viewModel
    let noteIndex: Int

    @State non-public var canvasView = PKCanvasView()
    @State non-public var toolPicker = PKToolPicker()
    @State non-public var isShowingImagePicker = false
    @State non-public var isInteractingWithElement = false

    var physique: some View {
        @Bindable var viewModel = viewModel

        VStack {
            GeometryReader { geometry in
                ZStack {
                    CanvasRepresentable(
                        canvasView: $canvasView,
                        toolPicker: $toolPicker,
                        notice: $viewModel.notes[noteIndex],
                        isInteractingWithElement: $isInteractingWithElement
                    )
                    .disabled(isInteractingWithElement) // Disable Canvas when interacting with StickyNotes
                    .body(width: geometry.dimension.width, top: geometry.dimension.top)
                    .onAppear {
                        toolPicker.addObserver(canvasView)
                        toolPicker.setVisible(true, forFirstResponder: canvasView)
                        canvasView.becomeFirstResponder()
                    }
                    .onDisappear {
                        saveDrawing()
                    }

                    // Overlay StickyNotes
                    ForEach($viewModel.notes[noteIndex].stickyNotes) { $stickyNote in
                        StickyNoteView(
                            stickyNote: $stickyNote,
                            onDelete: {
                                if let index = viewModel.notes[noteIndex].stickyNotes.firstIndex(the place: { $0.id == stickyNote.id }) {
                                    viewModel.notes[noteIndex].stickyNotes.take away(at: index)
                                }
                            },
                            onDuplicate: {
                                let newStickyNote = stickyNote.duplicate()
                                viewModel.notes[noteIndex].stickyNotes.append(newStickyNote)
                            }
                        )
                    }

                    // Overlay NoteImages
                    ForEach($viewModel.notes[noteIndex].photographs) { $noteImage in
                        NoteImageView(
                            noteImage: $noteImage,
                            onDelete: {
                                if let index = viewModel.notes[noteIndex].photographs.firstIndex(the place: { $0.id == noteImage.id }) {
                                    viewModel.notes[noteIndex].photographs.take away(at: index)
                                }
                            },
                            onDuplicate: {
                                let newImage = noteImage.duplicate()
                                viewModel.notes[noteIndex].photographs.append(newImage)
                            }
                        )
                    }
                }
            }
        }
        .navigationBarBackButtonHidden(true)
        .toolbar {
            // Linker Button: Zurück zur Notizen-Übersicht + Titel
            ToolbarItem(placement: .navigationBarLeading) {
                HStack {
                    Button(motion: {
                        presentationMode.wrappedValue.dismiss()
                    }) {
                        Picture(systemName: "chevron.left")
                            .font(.title2)
                    }
                    Textual content(viewModel.notes[noteIndex].title)
                        .font(.headline)
                        .lineLimit(1)
                        .truncationMode(.tail)
                }
            }

            // Mittiges Menü
            ToolbarItem(placement: .principal) {
                NotesMenu(
                    isShowingImagePicker: $isShowingImagePicker,
                    notice: $viewModel.notes[noteIndex]
                )
            }

            // Rechter Button: Fertig
            ToolbarItem(placement: .navigationBarTrailing) {
                Button("Fertig") {
                    presentationMode.wrappedValue.dismiss()
                }
                .font(.title2)
            }
        }
    }

    non-public func saveDrawing() {
        // Zeichnung speichern, wenn die Ansicht geschlossen wird
        let newDrawingData = canvasView.drawing.dataRepresentation()
        if newDrawingData != viewModel.notes[noteIndex].drawingData {
            viewModel.notes[noteIndex].drawingData = newDrawingData
        }
    }
}
import PencilKit

struct CanvasRepresentable: UIViewRepresentable {
    @Binding var canvasView: PKCanvasView
    @Binding var toolPicker: PKToolPicker
    @Binding var notice: Word
    @Binding var isInteractingWithElement: Bool

    func makeUIView(context: Context) -> PKCanvasView {
        updateBackground(for: canvasView)
        canvasView.drawingPolicy = .anyInput
        canvasView.delegate = context.coordinator

        // Zeichnung laden, falls vorhanden
        if let information = notice.drawingData, let drawing = strive? PKDrawing(information: information) {
            canvasView.drawing = drawing
        }

        toolPicker.addObserver(canvasView)
        toolPicker.setVisible(true, forFirstResponder: canvasView)
        canvasView.becomeFirstResponder()

        return canvasView
    }

    func updateUIView(_ uiView: PKCanvasView, context: Context) {
        uiView.isUserInteractionEnabled = !isInteractingWithElement

        if let information = notice.drawingData, let newDrawing = strive? PKDrawing(information: information) {
            if newDrawing != uiView.drawing {
                uiView.drawing = newDrawing
            }
        }
        updateBackground(for: uiView)
    }

    non-public func updateBackground(for canvasView: PKCanvasView) {
        change notice.background {
        case .none:
            canvasView.backgroundColor = UIColor.systemBackground
        case .grid:
            canvasView.backgroundColor = UIColor(patternImage: generateGridBackground())
        case .dotted:
            canvasView.backgroundColor = UIColor(patternImage: generateDottedBackground())
        case .lined:
            canvasView.backgroundColor = UIColor(patternImage: generateLinedBackground())
        }
    }

    // Dynamisch generierte Hintergründe
    non-public func generateGridBackground() -> UIImage {
        let renderer = UIGraphicsImageRenderer(dimension: CGSize(width: 30, top: 30))
        return renderer.picture { context in
            let path = UIBezierPath()
            path.transfer(to: CGPoint(x: 0, y: 15))
            path.addLine(to: CGPoint(x: 30, y: 15))
            path.transfer(to: CGPoint(x: 15, y: 0))
            path.addLine(to: CGPoint(x: 15, y: 30))
            UIColor.lightGray.setStroke()
            path.lineWidth = 0.5
            path.stroke()
        }
    }

    non-public func generateDottedBackground() -> UIImage {
        let renderer = UIGraphicsImageRenderer(dimension: CGSize(width: 10, top: 10))
        return renderer.picture { context in
            let path = UIBezierPath(ovalIn: CGRect(x: 4, y: 4, width: 2, top: 2))
            UIColor.systemGray.setFill()
            path.fill()
        }
    }

    non-public func generateLinedBackground() -> UIImage {
        let renderer = UIGraphicsImageRenderer(dimension: CGSize(width: 30, top: 30))
        return renderer.picture { context in
            let path = UIBezierPath()
            path.transfer(to: CGPoint(x: 0, y: 15))
            path.addLine(to: CGPoint(x: 30, y: 15))
            UIColor.lightGray.setStroke()
            path.lineWidth = 0.5
            path.stroke()
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, PKCanvasViewDelegate {
        var dad or mum: CanvasRepresentable

        init(_ dad or mum: CanvasRepresentable) {
            self.dad or mum = dad or mum
        }

        func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
            let newDrawingData = canvasView.drawing.dataRepresentation()
            if newDrawingData != dad or mum.notice.drawingData {
                DispatchQueue.important.async {
                    self.dad or mum.notice.drawingData = newDrawingData
                }
            }
        }
    }
}

Any recommendation on easy methods to resolve this may be vastly appreciated!

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles