- Leitor bíblico consumindo a API NBV (bible.falehandix.com.br): lista de livros por testamento e leitura de capítulos - Plano de 180 dias com três trilhas diárias (História, Sabedoria e Profetas, Novo Testamento) geradas deterministicamente sobre o cânon embutido (1.189 capítulos, cobertura testada) - Lembrete diário via notificação local recorrente (UNCalendarNotificationTrigger, identificador fixo, sem duplicatas), com pedido de autorização em contexto, configurações de horário/som, e deep link da notificação para a Leitura de Hoje - Estado do plano em SwiftData: progresso, pausa/retomada, reinício e conclusão preservando preferências do lembrete - 15 testes unitários (Testing) e 2 testes de UI (XCUIAutomation) - Ícone do app a partir do logo biblia.png Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
95 lines
2.5 KiB
Swift
95 lines
2.5 KiB
Swift
//
|
|
// ReadingPlanState.swift
|
|
// Bible-Week
|
|
//
|
|
// Estado persistente do Plano Bíblia em 180 Dias: progresso de leitura
|
|
// e preferências do lembrete diário.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftData
|
|
|
|
@Model
|
|
final class ReadingPlanState {
|
|
var startedAt: Date
|
|
var isPaused: Bool
|
|
|
|
/// Chaves "dia-trilha" das leituras concluídas, ex.: "34-nt".
|
|
var completedKeys: [String]
|
|
|
|
// Preferências do lembrete diário. Sobrevivem a pausa e reinício do plano.
|
|
var reminderEnabled: Bool
|
|
var reminderHour: Int
|
|
var reminderMinute: Int
|
|
var reminderSoundEnabled: Bool
|
|
var hasSeenReminderPrompt: Bool
|
|
|
|
init(startedAt: Date = .now) {
|
|
self.startedAt = startedAt
|
|
self.isPaused = false
|
|
self.completedKeys = []
|
|
self.reminderEnabled = false
|
|
self.reminderHour = 20
|
|
self.reminderMinute = 0
|
|
self.reminderSoundEnabled = true
|
|
self.hasSeenReminderPrompt = false
|
|
}
|
|
|
|
// MARK: - Progresso
|
|
|
|
private static func key(day: Int, track: PlanTrack) -> String {
|
|
"\(day)-\(track.rawValue)"
|
|
}
|
|
|
|
func isCompleted(day: Int, track: PlanTrack) -> Bool {
|
|
completedKeys.contains(Self.key(day: day, track: track))
|
|
}
|
|
|
|
func setCompleted(_ completed: Bool, day: Int, track: PlanTrack) {
|
|
let key = Self.key(day: day, track: track)
|
|
if completed {
|
|
if !completedKeys.contains(key) { completedKeys.append(key) }
|
|
} else {
|
|
completedKeys.removeAll { $0 == key }
|
|
}
|
|
}
|
|
|
|
func isDayCompleted(_ day: Int) -> Bool {
|
|
PlanTrack.allCases.allSatisfy { isCompleted(day: day, track: $0) }
|
|
}
|
|
|
|
/// Primeiro dia com leitura pendente (1...180). Se tudo concluído, 180.
|
|
var currentDay: Int {
|
|
(1...PlanSchedule.totalDays).first { !isDayCompleted($0) } ?? PlanSchedule.totalDays
|
|
}
|
|
|
|
var completedDaysCount: Int {
|
|
(1...PlanSchedule.totalDays).count { isDayCompleted($0) }
|
|
}
|
|
|
|
var isCompleted: Bool {
|
|
completedDaysCount == PlanSchedule.totalDays
|
|
}
|
|
|
|
/// Fração concluída do plano (0...1).
|
|
var progress: Double {
|
|
Double(completedDaysCount) / Double(PlanSchedule.totalDays)
|
|
}
|
|
|
|
// MARK: - Lembrete
|
|
|
|
/// Horário do lembrete formatado, ex.: "20:00".
|
|
var reminderTimeText: String {
|
|
String(format: "%02d:%02d", reminderHour, reminderMinute)
|
|
}
|
|
|
|
// MARK: - Ciclo de vida do plano
|
|
|
|
/// Recomeça o plano do dia 1, preservando as preferências de lembrete.
|
|
func restart() {
|
|
completedKeys = []
|
|
startedAt = .now
|
|
isPaused = false
|
|
}
|
|
}
|