perf(diary): 펜슬 저장 디바운스 — 획마다 전체 직렬화·블랍 재기록하던 버벅임 해소
- 획 종료 시 최신 PKDrawing만 보관, 1.2초 조용하면 백그라운드 직렬화 후 1회 저장 (세대 가드로 역전 방지) - 플러시: 비활성 전환(페이지 이탈·배치 모드·그리기 끔)·dismantleUIView·willResignActive — 유실 창 최대 1.2초 - CloudKit 트래픽도 획당 1회→묶음 1회로 감소 - -diaryStrokeTest 검증 인자: 프로그램 획 → 배달 확인(strokes=3, delivered 정상) + 재실행 복원 스크린샷 확인 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNxA172hNDRpDsJ2qztKPf
This commit is contained in:
parent
5b526fa650
commit
6c3b918311
@ -1286,6 +1286,41 @@ struct DiaryPencilCanvas: UIViewRepresentable {
|
||||
canvas.drawing = drawing
|
||||
}
|
||||
context.coordinator.canvas = canvas
|
||||
#if DEBUG
|
||||
// 검증용(-diaryStrokeTest): 3초 뒤 빨간 대각선 획을 추가하고 델리게이트 경로로
|
||||
// 디바운스 파이프라인을 태운 뒤, 6.5초 시점 상태를 Documents/diary-stroke-test.txt로
|
||||
// 남긴다 — strokes(획 수)와 delivered(실제 저장 배달 횟수)로 판정 (§14)
|
||||
if isActive, UserDefaults.standard.bool(forKey: "diaryStrokeTest") {
|
||||
let coordinator = context.coordinator
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak canvas] in
|
||||
guard let canvas else { return }
|
||||
var drawing = canvas.drawing
|
||||
let points = stride(from: 0.0, through: 1.0, by: 0.05).map { t in
|
||||
PKStrokePoint(
|
||||
location: CGPoint(x: 80 + 600 * t, y: 150 + 700 * t),
|
||||
timeOffset: t, size: CGSize(width: 6, height: 6),
|
||||
opacity: 1, force: 1, azimuth: 0, altitude: .pi / 2
|
||||
)
|
||||
}
|
||||
drawing.strokes.append(PKStroke(
|
||||
ink: PKInk(.pen, color: .systemRed),
|
||||
path: PKStrokePath(controlPoints: points, creationDate: .now)
|
||||
))
|
||||
canvas.drawing = drawing
|
||||
coordinator.canvasViewDrawingDidChange(canvas)
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 6.5) { [weak canvas] in
|
||||
guard let canvas else { return }
|
||||
let lines = [
|
||||
"strokes=\(canvas.drawing.strokes.count)",
|
||||
"delivered=\(coordinator.debugDeliveredCount)",
|
||||
]
|
||||
let url = URL.documentsDirectory.appending(path: "diary-stroke-test.txt")
|
||||
try? lines.joined(separator: "\n")
|
||||
.write(to: url, atomically: true, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return canvas
|
||||
}
|
||||
|
||||
@ -1299,6 +1334,11 @@ struct DiaryPencilCanvas: UIViewRepresentable {
|
||||
context.coordinator.setActive(isActive)
|
||||
}
|
||||
|
||||
static func dismantleUIView(_ uiView: SharpPencilCanvasView, coordinator: Coordinator) {
|
||||
// 화면에서 떨어져 나갈 때(일기 상세 닫힘 등) 디바운스 창의 미저장 획을 저장
|
||||
coordinator.flush()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class Coordinator: NSObject, PKCanvasViewDelegate {
|
||||
let onChange: (Data) -> Void
|
||||
@ -1306,8 +1346,78 @@ struct DiaryPencilCanvas: UIViewRepresentable {
|
||||
private let toolPicker = PKToolPicker()
|
||||
private var observing = false
|
||||
|
||||
// 저장 디바운스(1.4 펜슬 최적화): 예전에는 획을 뗄 때마다 전체 드로잉을
|
||||
// 직렬화(dataRepresentation)해 externalStorage 블랍을 통째로 재기록했다 —
|
||||
// 획이 쌓일수록 비용이 선형 증가해 연속 필기가 버벅였다(양식 페이지의 큰
|
||||
// 래스터와 겹치면 더 체감). 이제 획 종료 시엔 최신 PKDrawing(값 타입)만
|
||||
// 보관하고, 1.2초 조용하면 백그라운드에서 직렬화해 1회 저장한다.
|
||||
// 유실 방지 플러시 지점: 비활성 전환(페이지 이탈·배치 모드·그리기 끔)·
|
||||
// 뷰 해체(dismantleUIView)·앱 비활성(willResignActive).
|
||||
private var pendingDrawing: PKDrawing?
|
||||
private var saveTask: Task<Void, Never>?
|
||||
/// 배달 세대 — 비행 중인 옛 직렬화 결과가 더 새 결과를 덮지 않게 한다
|
||||
private var deliverGeneration = 0
|
||||
private var resignObserver: NSObjectProtocol?
|
||||
#if DEBUG
|
||||
/// 검증용(-diaryStrokeTest): onChange(실제 저장)가 몇 번 배달됐는지
|
||||
var debugDeliveredCount = 0
|
||||
#endif
|
||||
|
||||
init(onChange: @escaping (Data) -> Void) {
|
||||
self.onChange = onChange
|
||||
super.init()
|
||||
resignObserver = NotificationCenter.default.addObserver(
|
||||
forName: UIApplication.willResignActiveNotification,
|
||||
object: nil, queue: .main
|
||||
) { [weak self] _ in
|
||||
Task { @MainActor in self?.flush() }
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let resignObserver {
|
||||
NotificationCenter.default.removeObserver(resignObserver)
|
||||
}
|
||||
}
|
||||
|
||||
/// 디바운스 창이 열려 있으면 지금 즉시 직렬화·저장 (페이지 이탈 직전 등)
|
||||
func flush() {
|
||||
saveTask?.cancel()
|
||||
saveTask = nil
|
||||
guard let drawing = pendingDrawing else { return }
|
||||
pendingDrawing = nil
|
||||
deliverGeneration += 1
|
||||
#if DEBUG
|
||||
debugDeliveredCount += 1
|
||||
#endif
|
||||
onChange(drawing.dataRepresentation())
|
||||
}
|
||||
|
||||
private func scheduleSave() {
|
||||
saveTask?.cancel()
|
||||
saveTask = Task { @MainActor [weak self] in
|
||||
try? await Task.sleep(for: .seconds(1.2))
|
||||
guard !Task.isCancelled else { return }
|
||||
self?.deliverPending()
|
||||
}
|
||||
}
|
||||
|
||||
private func deliverPending() {
|
||||
guard let drawing = pendingDrawing else { return }
|
||||
pendingDrawing = nil
|
||||
deliverGeneration += 1
|
||||
let generation = deliverGeneration
|
||||
Task { @MainActor [weak self] in
|
||||
// 직렬화는 페이지가 커질수록 비싸다 — 메인 밖에서 수행
|
||||
let data = await Task.detached(priority: .userInitiated) {
|
||||
drawing.dataRepresentation()
|
||||
}.value
|
||||
guard let self, generation == self.deliverGeneration else { return }
|
||||
#if DEBUG
|
||||
self.debugDeliveredCount += 1
|
||||
#endif
|
||||
self.onChange(data)
|
||||
}
|
||||
}
|
||||
|
||||
/// 양식 페이지에서는 팔레트가 고른 색도 라이트 기준으로 해석·저장되게 한다 —
|
||||
@ -1334,13 +1444,18 @@ struct DiaryPencilCanvas: UIViewRepresentable {
|
||||
canvas.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
} else if canvas.isFirstResponder {
|
||||
canvas.resignFirstResponder()
|
||||
} else {
|
||||
// 비활성 전환(페이지 이탈·배치 모드·그리기 끔) — 미저장 획 즉시 저장
|
||||
flush()
|
||||
if canvas.isFirstResponder {
|
||||
canvas.resignFirstResponder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
|
||||
onChange(canvasView.drawing.dataRepresentation())
|
||||
pendingDrawing = canvasView.drawing
|
||||
scheduleSave()
|
||||
// 새 획의 렌더 레이어가 기본 해상도로 생겼을 수 있어 다시 맞춘다
|
||||
(canvasView as? SharpPencilCanvasView)?.applySharpness()
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user