Files
bible/Bible-Week/ReadingPlanSchedule.swift
Matheus A Silveira 80185726af Adiciona Plano Bíblia em 180 Dias com lembrete diário e ícone do app
- 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>
2026-09-04 18:54:48 -03:00

115 lines
3.7 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// ReadingPlanSchedule.swift
// Bible-Week
//
// Plano "Bíblia em 180 Dias": três trilhas paralelas por dia
// (História, Sabedoria e Profetas, Novo Testamento), distribuindo os
// capítulos de cada trilha uniformemente pelos 180 dias.
//
import Foundation
/// As três trilhas de leitura diária do plano.
enum PlanTrack: String, CaseIterable, Codable, Sendable {
case history = "history"
case wisdom = "wisdom"
case newTestament = "nt"
var displayName: String {
switch self {
case .history: "História"
case .wisdom: "Sabedoria e Profetas"
case .newTestament: "Novo Testamento"
}
}
/// Livros da trilha, em ordem canônica.
var books: [Book] {
switch self {
case .history: BibleCanon.books(in: 1...17) // Gênesis...Ester
case .wisdom: BibleCanon.books(in: 18...39) // Jó...Malaquias
case .newTestament: BibleCanon.books(in: 40...66) // Mateus...Apocalipse
}
}
}
/// Referência a um capítulo específico de um livro.
struct ChapterRef: Hashable, Sendable {
let bookCode: String
let bookName: String
let chapter: Int
}
/// Os capítulos de uma trilha em um dia do plano.
struct TrackReading: Hashable, Sendable, Identifiable {
let track: PlanTrack
let chapters: [ChapterRef]
var id: PlanTrack { track }
/// Rótulo legível, ex.: "Gênesis 3436" ou "Obadias 1 e Jonas 12".
var label: String {
// Agrupa capítulos consecutivos do mesmo livro em intervalos.
var runs: [(name: String, first: Int, last: Int)] = []
for ref in chapters {
if var last = runs.last, last.name == ref.bookName, last.last == ref.chapter - 1 {
last.last = ref.chapter
runs[runs.count - 1] = last
} else {
runs.append((ref.bookName, ref.chapter, ref.chapter))
}
}
let parts = runs.map { run in
run.first == run.last ? "\(run.name) \(run.first)" : "\(run.name) \(run.first)\(run.last)"
}
return parts.joined(separator: " e ")
}
}
/// A leitura completa de um dia (as três trilhas).
struct DayReading: Sendable {
let day: Int
let readings: [TrackReading]
func reading(for track: PlanTrack) -> TrackReading? {
readings.first { $0.track == track }
}
}
/// Cronograma fixo e determinístico do plano de 180 dias.
enum PlanSchedule {
static let totalDays = 180
static let days: [DayReading] = generate()
/// Leitura do dia `n` (1-based, limitado a 1...180).
static func day(_ n: Int) -> DayReading {
days[max(1, min(n, totalDays)) - 1]
}
private static func generate() -> [DayReading] {
let trackChapters: [PlanTrack: [ChapterRef]] = Dictionary(
uniqueKeysWithValues: PlanTrack.allCases.map { track in
let refs = track.books.flatMap { book in
(1...book.chapterCount).map {
ChapterRef(bookCode: book.code, bookName: book.name, chapter: $0)
}
}
return (track, refs)
}
)
return (0..<totalDays).map { index in
let readings = PlanTrack.allCases.map { track -> TrackReading in
let all = trackChapters[track]!
// Partição uniforme: o dia i recebe as posições
// [i*T/180, (i+1)*T/180), cobrindo tudo sem sobreposição.
let start = index * all.count / totalDays
let end = (index + 1) * all.count / totalDays
return TrackReading(track: track, chapters: Array(all[start..<end]))
}
return DayReading(day: index + 1, readings: readings)
}
}
}