- 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>
115 lines
3.7 KiB
Swift
115 lines
3.7 KiB
Swift
//
|
||
// 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 34–36" ou "Obadias 1 e Jonas 1–2".
|
||
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)
|
||
}
|
||
}
|
||
}
|