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:
-
The place of the Sticky Notes doesn’t align with the seen notice on the display.
-
When the Sticky Word is moved or resized, the interactive body (used for context menus or gestures) doesn’t observe the notice.
-
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!