fix(diary): pencil-only drawing with finger panning, and truly sharp strokes

- 실기기에서 drawingPolicy .pencilOnly — 손가락은 절대 필기되지 않고,
  확대 상태에서 한 손가락으로 페이지를 상하좌우 이동(팬)한다. 이동/확대
  제스처는 allowedTouchTypes=direct로 손가락 전용이라 펜슬 획을 뺏지 않음
  (시뮬레이터는 입력 수단이 없어 anyInput 유지). 배치 모드에선 한 손가락을
  요소 드래그에 양보(팬 두 손가락)
- 선명도 재수정: SharpPencilCanvasView가 목표 래스터 배율(기기 스케일 ×
  현재 줌, 상한 ×3)을 뷰+레이어 트리에 유지하고, 획을 그릴 때마다·레이아웃
  때마다 재적용 — PencilKit이 새 획 렌더 레이어를 기본 해상도로 만들어
  흐려지던 문제와, 기존 1.5배 슈퍼샘플링의 다운샘플 소프트닝을 함께 제거
  (화면 픽셀과 1:1 매칭이 가장 선명)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
This commit is contained in:
songyc macbook 2026-07-13 17:31:09 +09:00
parent 4c3b5a6c03
commit 5f9593923a

View File

@ -259,8 +259,15 @@ private struct DiaryZoomContainer<Content: View>: UIViewRepresentable {
scroll.bounces = false
scroll.bouncesZoom = true
scroll.backgroundColor = .clear
// ,
scroll.panGestureRecognizer.minimumNumberOfTouches = 2
// / .
// ( ) .
scroll.panGestureRecognizer.allowedTouchTypes = [
NSNumber(value: UITouch.TouchType.direct.rawValue)
]
scroll.panGestureRecognizer.minimumNumberOfTouches = 1
scroll.pinchGestureRecognizer?.allowedTouchTypes = [
NSNumber(value: UITouch.TouchType.direct.rawValue)
]
let hosting = UIHostingController(rootView: content())
hosting.view.backgroundColor = .clear
@ -276,6 +283,8 @@ private struct DiaryZoomContainer<Content: View>: UIViewRepresentable {
func updateUIView(_ scroll: DiaryZoomScrollView, context: Context) {
context.coordinator.hosting?.rootView = content()
scroll.pinchGestureRecognizer?.isEnabled = zoomEnabled
// ( )
scroll.panGestureRecognizer.minimumNumberOfTouches = zoomEnabled ? 1 : 2
}
final class Coordinator: NSObject, UIScrollViewDelegate {
@ -333,37 +342,28 @@ final class DiaryZoomScrollView: UIScrollView {
y: contentSize.height / 2 + dy)
}
/// .
/// 768×1024pt fit/ ,
/// PKCanvasView contentScaleFactor
/// ( ) .
/// 1.5 + ( × 2.5) .
/// .
/// 768×1024pt + ,
/// 1:1 (contentsScale = × ) ( )
/// ( ) . ( × 3).
/// · SharpPencilCanvasView ( ).
func updateCanvasSharpness() {
guard let zoomedView else { return }
let display = max(traitCollection.displayScale, 1)
let target = display * min(max(zoomScale, 1) * 1.5, 2.5)
let target = display * min(max(zoomScale, 0.7), 3)
guard abs(target - lastCanvasScale) > 0.01 else { return }
lastCanvasScale = target
Self.applyCanvasScale(target, in: zoomedView)
Self.assignCanvasScale(target, in: zoomedView)
}
/// PKCanvasView + contentsScale
private static func applyCanvasScale(_ scale: CGFloat, in view: UIView) {
if view is PKCanvasView {
setScaleRecursively(scale, view: view)
///
private static func assignCanvasScale(_ scale: CGFloat, in view: UIView) {
if let canvas = view as? SharpPencilCanvasView {
canvas.targetContentsScale = scale
return
}
for subview in view.subviews {
applyCanvasScale(scale, in: subview)
}
}
private static func setScaleRecursively(_ scale: CGFloat, view: UIView) {
view.contentScaleFactor = scale
view.layer.contentsScale = scale
view.setNeedsDisplay()
for subview in view.subviews {
setScaleRecursively(scale, view: subview)
assignCanvasScale(scale, in: subview)
}
}
}
@ -496,6 +496,48 @@ struct DiaryLineShape: Shape {
// MARK: - PencilKit
/// PKCanvasView.
/// PencilKit
/// contentsScale .
final class SharpPencilCanvasView: PKCanvasView {
/// ( × )
var targetContentsScale: CGFloat = 0 {
didSet {
if abs(targetContentsScale - oldValue) > 0.01 { applySharpness() }
}
}
override func layoutSubviews() {
super.layoutSubviews()
applySharpness()
}
func applySharpness() {
guard targetContentsScale > 0 else { return }
Self.apply(targetContentsScale, view: self)
}
private static func apply(_ scale: CGFloat, view: UIView) {
if abs(view.contentScaleFactor - scale) > 0.01 {
view.contentScaleFactor = scale
}
apply(scale, layer: view.layer)
for subview in view.subviews {
apply(scale, view: subview)
}
}
private static func apply(_ scale: CGFloat, layer: CALayer) {
if abs(layer.contentsScale - scale) > 0.01 {
layer.contentsScale = scale
layer.setNeedsDisplay()
}
for sublayer in layer.sublayers ?? [] {
apply(scale, layer: sublayer)
}
}
}
struct DiaryPencilCanvas: UIViewRepresentable {
let drawingData: Data
/// ( ) &
@ -506,12 +548,17 @@ struct DiaryPencilCanvas: UIViewRepresentable {
Coordinator(onChange: onChange)
}
func makeUIView(context: Context) -> PKCanvasView {
let canvas = PKCanvasView()
func makeUIView(context: Context) -> SharpPencilCanvasView {
let canvas = SharpPencilCanvasView()
canvas.backgroundColor = .clear
canvas.isOpaque = false
// · ( )
#if targetEnvironment(simulator)
// ()
canvas.drawingPolicy = .anyInput
#else
// : /
canvas.drawingPolicy = .pencilOnly
#endif
canvas.delegate = context.coordinator
if let drawing = try? PKDrawing(data: drawingData), !drawingData.isEmpty {
canvas.drawing = drawing
@ -520,14 +567,14 @@ struct DiaryPencilCanvas: UIViewRepresentable {
return canvas
}
func updateUIView(_ canvas: PKCanvasView, context: Context) {
func updateUIView(_ canvas: SharpPencilCanvasView, context: Context) {
context.coordinator.setActive(isActive)
}
@MainActor
final class Coordinator: NSObject, PKCanvasViewDelegate {
let onChange: (Data) -> Void
weak var canvas: PKCanvasView?
weak var canvas: SharpPencilCanvasView?
private let toolPicker = PKToolPicker()
private var observing = false
@ -556,6 +603,8 @@ struct DiaryPencilCanvas: UIViewRepresentable {
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
onChange(canvasView.drawing.dataRepresentation())
//
(canvasView as? SharpPencilCanvasView)?.applySharpness()
}
}
}