From 17f4329ff2c656aa7e044cdbcd8f813571340548 Mon Sep 17 00:00:00 2001 From: oliverhnat Date: Wed, 10 Dec 2025 14:55:40 +0100 Subject: [PATCH] feat(notes): make the canvas resizable, add pinch/double click gestures, make sure the notes don't cut off --- WorterBuch/HandwritingCanvasView.swift | 10 +++ WorterBuch/ScrollableCanvasView.swift | 97 +++++++++++++++++++++++--- 2 files changed, 97 insertions(+), 10 deletions(-) diff --git a/WorterBuch/HandwritingCanvasView.swift b/WorterBuch/HandwritingCanvasView.swift index 283ff04..7bf79b5 100644 --- a/WorterBuch/HandwritingCanvasView.swift +++ b/WorterBuch/HandwritingCanvasView.swift @@ -25,6 +25,11 @@ struct HandwritingCanvasView: UIViewRepresentable { // Store canvas in coordinator for later access context.coordinator.canvasView = canvasView + // Restore saved tool if available + if let savedTool = Coordinator.savedTool { + canvasView.tool = savedTool + } + return canvasView } @@ -45,6 +50,8 @@ struct HandwritingCanvasView: UIViewRepresentable { } class Coordinator: NSObject, PKCanvasViewDelegate { + static var savedTool: PKTool? + var drawing: Binding var onDrawingChanged: ((PKDrawing) -> Void)? var canvasView: PKCanvasView? @@ -78,6 +85,9 @@ struct HandwritingCanvasView: UIViewRepresentable { func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) { drawing.wrappedValue = canvasView.drawing onDrawingChanged?(canvasView.drawing) + + // Save the current tool for persistence + Coordinator.savedTool = canvasView.tool } } } diff --git a/WorterBuch/ScrollableCanvasView.swift b/WorterBuch/ScrollableCanvasView.swift index b885447..713d1cc 100644 --- a/WorterBuch/ScrollableCanvasView.swift +++ b/WorterBuch/ScrollableCanvasView.swift @@ -30,21 +30,47 @@ struct ScrollableCanvasView: UIViewRepresentable { // Allow finger touches to pass through for scrolling canvasView.allowsFingerDrawing = false - // Set initial canvas size - will be updated in updateUIView with proper bounds - let canvasWidth: CGFloat = 800 // Placeholder width - let canvasHeight: CGFloat = 3000 // Start with a large canvas + // Set initial canvas size based on drawing bounds + let drawingBounds = drawing.bounds + let canvasWidth: CGFloat + let canvasHeight: CGFloat + + if drawingBounds.isEmpty { + canvasWidth = 2000 + canvasHeight = 3000 + } else { + canvasWidth = max(drawingBounds.maxX + 500, 2000) + canvasHeight = max(drawingBounds.maxY + 500, 3000) + } + canvasView.frame = CGRect(x: 0, y: 0, width: canvasWidth, height: canvasHeight) scrollView.addSubview(canvasView) scrollView.contentSize = canvasView.frame.size - // Allow scrolling beyond content + // Allow scrolling beyond content in both directions scrollView.alwaysBounceVertical = true + scrollView.alwaysBounceHorizontal = true + + // Enable zooming + scrollView.minimumZoomScale = 0.25 // Zoom out to 25% (see full canvas) + scrollView.maximumZoomScale = 2.0 // Zoom in to 200% + scrollView.delegate = context.coordinator // Store references in coordinator context.coordinator.canvasView = canvasView context.coordinator.scrollView = scrollView + // Add double-tap gesture for zoom toggle + let doubleTapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleDoubleTap(_:))) + doubleTapGesture.numberOfTapsRequired = 2 + scrollView.addGestureRecognizer(doubleTapGesture) + + // Restore saved tool if available + if let savedTool = Coordinator.savedTool { + canvasView.tool = savedTool + } + return scrollView } @@ -56,23 +82,24 @@ struct ScrollableCanvasView: UIViewRepresentable { } canvasView.isUserInteractionEnabled = isEditable - // Update canvas width to match scroll view bounds (once they're valid) - let canvasWidth = scrollView.bounds.width > 0 ? scrollView.bounds.width : 800 - // Expand canvas if drawing extends beyond current bounds let drawingBounds = drawing.bounds + let currentWidth = canvasView.frame.width let currentHeight = canvasView.frame.height + let requiredWidth: CGFloat let requiredHeight: CGFloat if drawingBounds.isEmpty { + requiredWidth = 2000 requiredHeight = 3000 } else { + requiredWidth = max(drawingBounds.maxX + 500, 2000) // Add padding to the right requiredHeight = max(drawingBounds.maxY + 500, 3000) // Add padding below } // Update canvas size if needed - if requiredHeight > currentHeight || abs(canvasView.frame.width - canvasWidth) > 1 { - canvasView.frame = CGRect(x: 0, y: 0, width: canvasWidth, height: requiredHeight) + if requiredWidth > currentWidth || requiredHeight > currentHeight { + canvasView.frame = CGRect(x: 0, y: 0, width: requiredWidth, height: requiredHeight) scrollView.contentSize = canvasView.frame.size } @@ -86,7 +113,9 @@ struct ScrollableCanvasView: UIViewRepresentable { Coordinator(drawing: $drawing, onDrawingChanged: onDrawingChanged) } - class Coordinator: NSObject, PKCanvasViewDelegate { + class Coordinator: NSObject, PKCanvasViewDelegate, UIScrollViewDelegate { + static var savedTool: PKTool? + var drawing: Binding var onDrawingChanged: ((PKDrawing) -> Void)? var canvasView: PKCanvasView? @@ -121,6 +150,54 @@ struct ScrollableCanvasView: UIViewRepresentable { func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) { drawing.wrappedValue = canvasView.drawing onDrawingChanged?(canvasView.drawing) + + // Save the current tool for persistence + Coordinator.savedTool = canvasView.tool + } + + // MARK: - UIScrollViewDelegate for zooming + + func viewForZooming(in scrollView: UIScrollView) -> UIView? { + return canvasView + } + + func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { + // Snap back to normal zoom (1.0) if close enough + if scale > 0.9 && scale < 1.1 { + UIView.animate(withDuration: 0.2) { + scrollView.setZoomScale(1.0, animated: false) + } + } + } + + @objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) { + guard let scrollView = scrollView else { return } + + let currentScale = scrollView.zoomScale + + if abs(currentScale - 1.0) < 0.01 { + // Currently at 100%, zoom in to 1.5x + let location = gesture.location(in: canvasView) + let zoomRect = zoomRect(for: 1.5, center: location) + scrollView.zoom(to: zoomRect, animated: true) + } else { + // Not at 100%, zoom back to 100% + scrollView.setZoomScale(1.0, animated: true) + } + } + + private func zoomRect(for scale: CGFloat, center: CGPoint) -> CGRect { + guard let scrollView = scrollView else { return .zero } + + var zoomRect = CGRect.zero + zoomRect.size.width = scrollView.frame.size.width / scale + zoomRect.size.height = scrollView.frame.size.height / scale + + // Center on the tap location + zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0) + zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0) + + return zoomRect } } }