μμ±μΌ: 2025-01-25 λΈλμΉ: feature/ux-improvements
PrayAnswer μ±μ μΆκ° κΈ°λ₯ κ°λ° κ³νμμ λλ€.
- Phase 1: μλ μΈλΆμ€μ (λ μ§, μκ°, λ°λ³΅ νμ 컀μ€ν°λ§μ΄μ§)
- Phase 2: D-Day μΊλ¦°λ μ°λ (iOS μΊλ¦°λ μ±μ μ΄λ²€νΈ μΆκ°)
- Phase 3: AI μμ± μμ½ (Apple Foundation Models νμ©) - iOS 26+ νμ
| κΈ°λ₯ | μν | ꡬν νμΌ | μ€λͺ |
|---|---|---|---|
| D-Day μλ¦Ό | β μλ£ | NotificationManager.swift |
D-7, D-3, D-1, D-Day κ³ μ μλ¦Ό (μ€μ 9μ) |
| D-Day μΆμ | β μλ£ | Prayer.swift |
targetDate μμ±, μ± λ΄λΆ μ μ₯λ§ |
| μμ± λ Ήμ | β μλ£ | SpeechRecognitionManager.swift |
μ€μκ° Speech-to-Text (νκ΅μ΄) |
| μμ ― | β μλ£ | WidgetDataManager.swift |
μ¦κ²¨μ°ΎκΈ° κΈ°λ νμ |
- UI: SwiftUI
- λ°μ΄ν°: SwiftData
- μλ¦Ό: UserNotifications
- μμ±: Speech Framework, AVFoundation
- μμ ―: WidgetKit + App Groups
μ¬μ©μκ° μλ¦Ό λ μ§, μκ°, λ°λ³΅ νμλ₯Ό μμ λ‘κ² μ€μ ν μ μλλ‘ κ°μ
- μλ¦Ό μΌμ κ³ μ : D-7, D-3, D-1, D-Day
- μλ¦Ό μκ° κ³ μ : μ€μ 9μ
- λ°λ³΅ μλ¦Ό λ―Έμ§μ
// μ νμΌ: Models/NotificationSettings.swift
struct NotificationSettings: Codable {
var isEnabled: Bool = false
var notificationTime: Date = Calendar.current.date(from: DateComponents(hour: 9, minute: 0))!
var reminderDays: [Int] = [7, 3, 1, 0] // D-nμΌ λͺ©λ‘
var repeatType: RepeatType = .none
var repeatCount: Int? = nil
enum RepeatType: String, Codable, CaseIterable {
case none = "μμ"
case daily = "λ§€μΌ"
case weekly = "λ§€μ£Ό"
case custom = "μ¬μ©μ μ§μ "
}
}// Prayer.swiftμ μΆκ°
var notificationSettingsData: Data? // NotificationSettings JSON μ μ₯
var notificationSettings: NotificationSettings {
get {
guard let data = notificationSettingsData else { return NotificationSettings() }
return (try? JSONDecoder().decode(NotificationSettings.self, from: data)) ?? NotificationSettings()
}
set {
notificationSettingsData = try? JSONEncoder().encode(newValue)
}
}// μ νμΌ: Views/NotificationSettingsView.swift
struct NotificationSettingsView: View {
@Binding var settings: NotificationSettings
var body: some View {
Form {
// μλ¦Ό νμ±ν ν κΈ
Section("μλ¦Ό μ€μ ") {
Toggle("μλ¦Ό λ°κΈ°", isOn: $settings.isEnabled)
}
// μλ¦Ό μκ° μ ν
Section("μλ¦Ό μκ°") {
DatePicker("μκ°", selection: $settings.notificationTime, displayedComponents: .hourAndMinute)
}
// D-nμΌ μ ν (λ©ν° μ ν)
Section("μλ¦Ό μΌμ ") {
// D-30 ~ D-Day 체ν¬λ°μ€
}
// λ°λ³΅ μ€μ
Section("λ°λ³΅") {
Picker("λ°λ³΅ μ ν", selection: $settings.repeatType) { ... }
}
}
}
}// NotificationManager.swift μμ
func scheduleCustomNotifications(for prayer: Prayer) {
let settings = prayer.notificationSettings
guard settings.isEnabled, let targetDate = prayer.targetDate else { return }
cancelNotifications(for: prayer)
for daysBefore in settings.reminderDays {
// 컀μ€ν
μκ°μΌλ‘ μλ¦Ό μμ±
scheduleNotification(
for: prayer,
daysBefore: daysBefore,
time: settings.notificationTime
)
}
// λ°λ³΅ μλ¦Ό μ²λ¦¬
if settings.repeatType != .none {
scheduleRepeatingNotifications(for: prayer, settings: settings)
}
}AddPrayerView: μλ¦Ό μ€μ μΉμ μ "μΈλΆ μ€μ " λ²νΌ μΆκ°PrayerDetailView: μλ¦Ό μ€μ νΈμ§ κΈ°λ₯ μΆκ°
| νμΌ | μμ |
|---|---|
Models/NotificationSettings.swift |
π μλ‘ μμ± |
Models/Prayer.swift |
π notificationSettings μμ± μΆκ° |
Views/NotificationSettingsView.swift |
π μλ‘ μμ± |
Utils/NotificationManager.swift |
π 컀μ€ν μ€μΌμ€λ§ λ‘μ§ μΆκ° |
Views/AddPrayerView.swift |
π μΈλΆ μ€μ λ²νΌ μΆκ° |
Views/PrayerDetailView.swift |
π μλ¦Ό μ€μ νΈμ§ UI μΆκ° |
Utils/LocalizationKeys.swift |
π μ λ‘컬λΌμ΄μ μ΄μ ν€ μΆκ° |
D-Dayλ₯Ό iOS μΊλ¦°λ μ±μ μ΄λ²€νΈλ‘ μΆκ°νμ¬ μμ€ν μΊλ¦°λμ ν΅ν©
- EventKit: μΊλ¦°λ μ κ·Ό λ° μ΄λ²€νΈ μμ±
- EventKitUI (μ ν): λ€μ΄ν°λΈ μ΄λ²€νΈ νΈμ§ UI
// μ νμΌ: Utils/CalendarManager.swift
import EventKit
final class CalendarManager {
static let shared = CalendarManager()
private let eventStore = EKEventStore()
// MARK: - Permission
func requestAccess() async -> Bool {
do {
return try await eventStore.requestFullAccessToEvents()
} catch {
return false
}
}
// MARK: - Calendar Operations
func availableCalendars() -> [EKCalendar] {
return eventStore.calendars(for: .event)
}
// MARK: - Event Operations
func addEvent(for prayer: Prayer, to calendar: EKCalendar) throws -> String {
let event = EKEvent(eventStore: eventStore)
event.title = "π \(prayer.title)"
event.notes = prayer.content
event.startDate = prayer.targetDate
event.endDate = prayer.targetDate
event.isAllDay = true
event.calendar = calendar
// μλ¦Ό μΆκ° (D-1, D-Day)
event.addAlarm(EKAlarm(relativeOffset: -86400)) // 1μΌ μ
event.addAlarm(EKAlarm(relativeOffset: 0)) // λΉμΌ
try eventStore.save(event, span: .thisEvent)
return event.eventIdentifier
}
func removeEvent(identifier: String) throws { ... }
func updateEvent(identifier: String, with prayer: Prayer) throws { ... }
}// Prayer.swiftμ μΆκ°
var calendarEventIdentifier: String? // μΊλ¦°λ μ΄λ²€νΈ ID μ μ₯
var isAddedToCalendar: Bool { calendarEventIdentifier != nil }<key>NSCalendarsFullAccessUsageDescription</key>
<string>D-Dayλ₯Ό μΊλ¦°λμ μΆκ°νκΈ° μν΄ μΊλ¦°λ μ κ·Ό κΆνμ΄ νμν©λλ€.</string>// μ νμΌ: Views/Components/CalendarPickerView.swift
struct CalendarPickerView: View {
@State private var calendars: [EKCalendar] = []
@State private var selectedCalendar: EKCalendar?
var onSelect: (EKCalendar) -> Void
var body: some View {
List(calendars, id: \.calendarIdentifier) { calendar in
Button {
onSelect(calendar)
} label: {
HStack {
Circle().fill(Color(cgColor: calendar.cgColor)).frame(width: 12)
Text(calendar.title)
}
}
}
}
}// PrayerDetailView.swiftμ μΆκ°
Button {
showCalendarPicker = true
} label: {
Label(
prayer.isAddedToCalendar ? "μΊλ¦°λμμ 보기" : "μΊλ¦°λμ μΆκ°",
systemImage: "calendar.badge.plus"
)
}
.sheet(isPresented: $showCalendarPicker) {
CalendarPickerView { calendar in
addToCalendar(calendar: calendar)
}
}| νμΌ | μμ |
|---|---|
Utils/CalendarManager.swift |
π μλ‘ μμ± |
Models/Prayer.swift |
π calendarEventIdentifier μΆκ° |
Views/Components/CalendarPickerView.swift |
π μλ‘ μμ± |
Views/PrayerDetailView.swift |
π μΊλ¦°λ μΆκ° λ²νΌ |
Info.plist |
π μΊλ¦°λ κΆν μ€λͺ μΆκ° |
Utils/LocalizationKeys.swift |
π μ λ‘컬λΌμ΄μ μ΄μ ν€ |
π΄ iOS 26.0+ νμ (2025λ
κ°μ μΆμ μμ )
π΄ Apple Intelligence μ§μ κΈ°κΈ°: A17 Pro, M1 μ΄μ
π΄ νμ¬(2025λ
1μ) κ°λ° λΆκ° - iOS 26 λ² ν μΆμ ν μ§ν
μμ±μΌλ‘ λ Ήμν λ΄μ©μ AIκ° κΈ°λλ¬Έ νμμΌλ‘ μλ μ 리
- Foundation Models Framework: μ¨λλ°μ΄μ€ LLM
- LanguageModelSession: ν μ€νΈ μμ± API
// μ νμΌ: Utils/AISummarizationManager.swift
import FoundationModels
@available(iOS 26.0, *)
final class AISummarizationManager {
static let shared = AISummarizationManager()
private let instructions = """
λ€μ μμ± λ
Ήμ λ΄μ©μ κΈ°λλ¬ΈμΌλ‘ μ 리ν΄μ£ΌμΈμ:
1. ν΅μ¬ κΈ°λ λ΄μ©μ μΆμΆν©λλ€
2. λΆνμν λ§(μ΄, μ, κ·Έ...)μ μ κ±°ν©λλ€
3. λ¬Έμ₯μ μμ°μ€λ½κ² λ€λ¬μ΅λλ€
4. κΈ°λλ¬Έ νμμΌλ‘ ꡬμ±ν©λλ€ (κ°μ¬/κ°κ΅¬/κ²°μ¬)
"""
var isAvailable: Bool {
SystemLanguageModel.default.availability == .available
}
func summarize(text: String) async throws -> String {
let session = LanguageModelSession(instructions: instructions)
let response = try await session.respond(to: text)
return response.content
}
}// AddPrayerView.swiftμ VoiceRecordingOverlay μμ
if #available(iOS 26.0, *), AISummarizationManager.shared.isAvailable {
Button("AIλ‘ μ 리νκΈ°") {
Task {
isProcessing = true
let summarized = try await AISummarizationManager.shared.summarize(text: recognizedText)
showSummaryPreview = true
summarizedText = summarized
isProcessing = false
}
}
}// μ νμΌ: Views/Components/AISummaryPreviewView.swift
struct AISummaryPreviewView: View {
let originalText: String
@Binding var summarizedText: String
var onApply: () -> Void
var onCancel: () -> Void
var body: some View {
VStack {
Text("AI μ 리 κ²°κ³Ό").font(.headline)
// μλ³Έ vs μμ½ λΉκ΅
HStack {
VStack { Text("μλ³Έ"); Text(originalText) }
VStack { Text("μ 리λ¨"); TextEditor(text: $summarizedText) }
}
HStack {
Button("μ·¨μ", action: onCancel)
Button("μ μ©", action: onApply)
}
}
}
}// κΈ°λ₯ λΉνμ±ν λλ μλ΄ λ©μμ§ νμ
if #unavailable(iOS 26.0) {
Text("AI μ 리 κΈ°λ₯μ iOS 26 μ΄μμμ μ¬μ© κ°λ₯ν©λλ€")
.foregroundColor(.secondary)
}| νμΌ | μμ |
|---|---|
Utils/AISummarizationManager.swift |
π μλ‘ μμ± |
Views/Components/AISummaryPreviewView.swift |
π μλ‘ μμ± |
Views/AddPrayerView.swift |
π AI μ 리 λ²νΌ μΆκ° |
Utils/LocalizationKeys.swift |
π μ λ‘컬λΌμ΄μ μ΄μ ν€ |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Phase 1: μλ μΈλΆμ€μ β
β ββ μμ μμ: 2-3μΌ β
β ββ λμ΄λ: βββββ β
β ββ μν: β
μλ£ (2025-01-25) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Phase 2: μΊλ¦°λ μ°λ β
β ββ μμ μμ: 2-3μΌ β
β ββ λμ΄λ: βββββ β
β ββ μν: β
μλ£ (2025-01-25) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Phase 3: AI μμ± μμ½ β
β ββ μμ μμ: 3-4μΌ β
β ββ λμ΄λ: βββββ β
β ββ μν: β
μλ£ (2025-01-26) - iOS 26+ μ‘°κ±΄λΆ μ»΄νμΌ μ μ© β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- μλ¦Ό μκ° λ³κ²½ ν μ μ λ°μ‘ νμΈ
- 컀μ€ν D-nμΌ μ€μ λμ νμΈ
- λ°λ³΅ μλ¦Ό μ μ λμ νμΈ
- κΈ°μ‘΄ μλ¦Ό μ€μ κ³Όμ νΈνμ±
- μΊλ¦°λ κΆν μμ² μ μ λμ
- μ΄λ²€νΈ μμ±/μμ /μμ νμΈ
- μ¬λ¬ μΊλ¦°λμ μΆκ° ν μ€νΈ
- κΈ°λ μμ μ μΊλ¦°λ μ΄λ²€νΈ μμ
- AI λͺ¨λΈ κ°μ©μ± 체ν¬
- μμ½ νμ§ κ²μ¦ (νκ΅μ΄)
- iOS 26 λ―Έλ§ κΈ°κΈ° λμ
- μ€νλΌμΈ μν μ²λ¦¬
- Apple Foundation Models Documentation
- EventKit Framework
- UserNotifications Framework
- Speech Framework
Made with Claude Code