mycode/myApp/HaruDanim/Shared/LocalPrefs.swift
songyc macbook 28a453485d feat(main): 배치 편집을 즐겨찾기 섹션 인지형으로 — 끌어서 지정·해제 + '지정 순서' 규칙
즐겨찾기 v2(e09a0b7) 후속 실사용 피드백 세 가지 반영:
①즐겨찾기 안에서 순서 바꾸기가 어렵다 ②새로 지정하면 꼬리표(전역)
순서에 끼어들어 예측이 안 된다 ③배치 편집이 즐겨찾기가 없다는 듯
통짜 그리드로 떠서 디테일이 떨어진다.

- 배치 편집도 즐겨찾기/나머지 섹션 구조 그대로(항상 펼침): 섹션 안
  드래그는 실시간 순서 변경, **섹션 사이 드래그는 드롭 확정 시점에
  즐겨찾기 지정·해제 + 그 위치로 이동**(commitDrop). 호버 중 교차
  이동은 자기 섹션 안 점프로 보여 moveDragging이 무시
- 빈 섹션은 점선 드롭 존(호버 하이라이트)이 받는다 — 즐겨찾기 0개
  상태에서도 끌어서 지정 가능, 전부 즐겨찾기면 해제 존 표시
- **새 즐겨찾기는 배치 순서상 블록 끝으로**(LocalPrefs.placeNewFavoriteAtEnd
  — 롱프레스 메뉴·행동 탭 스와이프·상세 토글 3경로 공통). 해제는
  위치 무이동(나머지 상단 근처에 남음), 첫 즐겨찾기도 무이동
- 도움말 본문에 순서 규칙·끌어서 지정 사용법 반영, en/ja 번역 +
  stale 1키 정리 (드롭 존 문구 2키 포함 missing/stale 0)
- 검증: Debug/Store 빌드, 아이폰·아이패드 편집 모드 + 브라우즈
  스크린샷 (isFavorite는 모델 필드라 DataChange.commit 경유 유지)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:18:45 +09:00

181 lines
8.6 KiB
Swift

//
// LocalPrefs.swift
// Haru_Danim
//
// UI (CloudKit )
//
// : ·· "" CloudKit ,
// " "( , , , )
// App Group UserDefaults iCloud .
// · ·App Intents App Group defaults
// .
//
// @Model (Action.sortOrder, Action.promptsForNote, Goal.showsOnMain,
// Goal.isCollapsed) CloudKit ( ),
// UI . 1 .
//
import Foundation
import SwiftData
nonisolated enum LocalPrefsKeys {
/// Action.uuid
static let actionOrder = "local.actionOrder"
/// Goal.uuid
static let pinnedGoals = "local.pinnedGoals"
/// Goal.uuid
static let collapsedGoals = "local.collapsedGoals"
/// Action.uuid
static let notePromptActions = "local.notePromptActions"
/// Goal.uuid .
/// ( ) · .
/// .
static let watchGoals = "local.watchGoals"
/// ' ' ( ) Bool
static let mainOthersCollapsed = "local.mainOthersCollapsed"
}
nonisolated enum LocalPrefs {
static var defaults: UserDefaults { AppGroup.defaults }
// MARK: ( UUID )
static func idList(_ raw: String) -> [UUID] {
raw.split(separator: ",").compactMap { UUID(uuidString: String($0)) }
}
static func rawValue(_ ids: [UUID]) -> String {
ids.map(\.uuidString).joined(separator: ",")
}
static func contains(_ id: UUID, in raw: String) -> Bool {
raw.contains(id.uuidString)
}
/// ( , , ) id raw
static func toggling(_ id: UUID, in raw: String) -> String {
var ids = idList(raw)
if let index = ids.firstIndex(of: id) {
ids.remove(at: index)
} else {
ids.append(id)
}
return rawValue(ids)
}
// MARK:
/// .
/// : ( CloudKit )
/// . () sortOrder .
static func orderedActions(_ actions: [Action], raw: String) -> [Action] {
var position: [UUID: Int] = [:]
for (index, id) in idList(raw).enumerated() {
position[id] = index
}
return actions.sorted {
switch (position[$0.uuid], position[$1.uuid]) {
case let (a?, b?): return a < b
case (.some, nil): return true
case (nil, .some): return false
case (nil, nil):
if $0.sortOrder != $1.sortOrder { return $0.sortOrder < $1.sortOrder }
return $0.createdAt < $1.createdAt
}
}
}
static func orderedActions(_ actions: [Action]) -> [Action] {
orderedActions(actions, raw: defaults.string(forKey: LocalPrefsKeys.actionOrder) ?? "")
}
/// ( )
static func saveActionOrder(_ actions: [Action]) {
defaults.set(rawValue(actions.map(\.uuid)), forKey: LocalPrefsKeys.actionOrder)
}
/// .
/// ( )
/// (sortOrder) .
static func appendActionToOrder(_ id: UUID) {
guard let raw = defaults.string(forKey: LocalPrefsKeys.actionOrder) else { return }
var ids = idList(raw)
guard !ids.contains(id) else { return }
ids.append(id)
defaults.set(rawValue(ids), forKey: LocalPrefsKeys.actionOrder)
}
/// '' : .
/// " "
/// ( ),
/// ( ) (§6.1 v2)
static func placeNewFavoriteAtEnd(_ action: Action, context: ModelContext) {
let all = (try? context.fetch(FetchDescriptor<Action>())) ?? []
var ordered = orderedActions(all)
guard let from = ordered.firstIndex(where: { $0.uuid == action.uuid }) else { return }
let moved = ordered.remove(at: from)
guard let lastFavorite = ordered.lastIndex(where: { $0.isFavorite }) else { return }
ordered.insert(moved, at: lastFavorite + 1)
saveActionOrder(ordered)
}
// MARK: ( )
static func promptsForNote(_ id: UUID) -> Bool {
contains(id, in: defaults.string(forKey: LocalPrefsKeys.notePromptActions) ?? "")
}
static func setPromptsForNote(_ id: UUID, enabled: Bool) {
let raw = defaults.string(forKey: LocalPrefsKeys.notePromptActions) ?? ""
let current = contains(id, in: raw)
guard current != enabled else { return }
defaults.set(toggling(id, in: raw), forKey: LocalPrefsKeys.notePromptActions)
}
// MARK: ( )
static func showsOnWatch(_ id: UUID) -> Bool {
contains(id, in: defaults.string(forKey: LocalPrefsKeys.watchGoals) ?? "")
}
static func setShowsOnWatch(_ id: UUID, enabled: Bool) {
let raw = defaults.string(forKey: LocalPrefsKeys.watchGoals) ?? ""
guard contains(id, in: raw) != enabled else { return }
defaults.set(toggling(id, in: raw), forKey: LocalPrefsKeys.watchGoals)
}
// MARK: @Model 1
/// , ( ).
/// (: CloudKit )
/// .
@MainActor
static func adoptLegacyModelValuesIfNeeded(context: ModelContext) {
let actions = (try? context.fetch(FetchDescriptor<Action>(
sortBy: [SortDescriptor(\.sortOrder), SortDescriptor(\.createdAt)]
))) ?? []
if defaults.string(forKey: LocalPrefsKeys.actionOrder) == nil, !actions.isEmpty {
saveActionOrder(actions)
}
if defaults.string(forKey: LocalPrefsKeys.notePromptActions) == nil, !actions.isEmpty {
defaults.set(
rawValue(actions.filter(\.promptsForNote).map(\.uuid)),
forKey: LocalPrefsKeys.notePromptActions
)
}
let goals = (try? context.fetch(FetchDescriptor<Goal>())) ?? []
if defaults.string(forKey: LocalPrefsKeys.pinnedGoals) == nil, !goals.isEmpty {
defaults.set(
rawValue(goals.filter(\.showsOnMain).map(\.uuid)),
forKey: LocalPrefsKeys.pinnedGoals
)
}
if defaults.string(forKey: LocalPrefsKeys.collapsedGoals) == nil, !goals.isEmpty {
defaults.set(
rawValue(goals.filter(\.isCollapsed).map(\.uuid)),
forKey: LocalPrefsKeys.collapsedGoals
)
}
}
}