행동이 많아질수록 모음 탭이 복잡해진다는 실사용 피드백에 대한 1차 대응. 22개 시드로 iPhone·iPad·맥 4가지 레이아웃을 직접 시뮬레이션해 대안 A~F를 검토했고, 가장 저위험인 A(즐겨찾기 재사용)만 이번에 구현했다. - Action.isFavorite(행동 탭과 동일 필드)를 켠 행동을 모음 탭 상단에 가로 스크롤 한 줄로 노출 (전체 그리드에서는 제외하지 않음 — 숨기는 방식은 검토 후 기각) - 롱프레스 메뉴에 즐겨찾기 지정·해제 버튼 추가, 전체 그리드·즐겨찾기 줄이 메뉴 구성을 공유하도록 리팩터 - 도움말에 새 주제 추가 + 관련 en/ja 번역, CLAUDE.md §6.1 문서화 - 감사 겸 아이콘 전용 버튼 접근성 라벨 누락 3건 수정(일기 할 일 체크박스, 아이콘 선택기, 꼬리표 색 프리셋) — §15 컨벤션 위반이라 저위험 즉시 수정 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014CkLDi8Lr21vGqo4SjZaTp
96 lines
3.5 KiB
Swift
96 lines
3.5 KiB
Swift
//
|
|
// SymbolPickerView.swift
|
|
// Haru_Danim
|
|
//
|
|
// SF Symbols 아이콘 선택기 (검색 + 카테고리 탐색)
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct SymbolPickerView: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
@Binding var selection: String
|
|
|
|
@State private var searchText = ""
|
|
@State private var category: String
|
|
|
|
init(selection: Binding<String>) {
|
|
_selection = selection
|
|
_category = State(initialValue: SymbolCatalog.categories.first?.name ?? "")
|
|
}
|
|
|
|
private var symbols: [String] {
|
|
let trimmed = searchText.trimmingCharacters(in: .whitespaces).lowercased()
|
|
if !trimmed.isEmpty {
|
|
return SymbolCatalog.allSymbols.filter { $0.contains(trimmed) }
|
|
}
|
|
return SymbolCatalog.categories.first { $0.name == category }?.symbols ?? []
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
VStack(spacing: 0) {
|
|
if searchText.isEmpty {
|
|
categoryBar
|
|
}
|
|
ScrollView {
|
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 52), spacing: 10)], spacing: 10) {
|
|
ForEach(symbols, id: \.self) { symbol in
|
|
Button {
|
|
selection = symbol
|
|
dismiss()
|
|
} label: {
|
|
Image(systemName: symbol)
|
|
.font(.system(size: 22))
|
|
.foregroundStyle(symbol == selection ? Color.white : AppTheme.green)
|
|
.frame(width: 52, height: 52)
|
|
.background(
|
|
symbol == selection ? AppTheme.green : AppTheme.surface,
|
|
in: RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.accessibilityLabel(Text(verbatim: symbol))
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
.background(AppTheme.background)
|
|
.navigationTitle("아이콘 선택")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.searchable(text: $searchText, prompt: "심볼 이름 검색 (영문)")
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("닫기") { dismiss() }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var categoryBar: some View {
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 8) {
|
|
ForEach(SymbolCatalog.categories) { cat in
|
|
Button {
|
|
category = cat.name
|
|
} label: {
|
|
Text(cat.name)
|
|
.font(.subheadline.weight(category == cat.name ? .semibold : .regular))
|
|
.padding(.horizontal, 14)
|
|
.padding(.vertical, 7)
|
|
.background(
|
|
category == cat.name ? AppTheme.green : AppTheme.surface,
|
|
in: Capsule()
|
|
)
|
|
.foregroundStyle(category == cat.name ? Color.white : Color.primary)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.vertical, 8)
|
|
}
|
|
}
|
|
}
|