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>
This commit is contained in:
2026-09-04 18:54:48 -03:00
parent 4d36df488f
commit 80185726af
19 changed files with 1583 additions and 110 deletions

View File

@@ -0,0 +1,93 @@
//
// BooksView.swift
// Bible-Week
//
// Aba Bíblia: lista os livros agrupados por testamento.
//
import SwiftUI
struct BooksView: View {
@State private var books: [Book] = []
@State private var isLoading = false
@State private var errorMessage: String?
/// Livros do Antigo Testamento, em ordem canônica.
private var oldTestament: [Book] {
books.filter { $0.testament == "OT" }.sorted { $0.bookOrder < $1.bookOrder }
}
/// Livros do Novo Testamento, em ordem canônica.
private var newTestament: [Book] {
books.filter { $0.testament == "NT" }.sorted { $0.bookOrder < $1.bookOrder }
}
var body: some View {
NavigationStack {
Group {
if isLoading {
ProgressView("Carregando livros...")
} else if let errorMessage {
ContentUnavailableView {
Label("Não foi possível carregar", systemImage: "wifi.slash")
} description: {
Text(errorMessage)
} actions: {
Button("Tentar novamente") { Task { await loadBooks() } }
}
} else {
bookList
}
}
.navigationTitle("Bíblia")
}
.task {
if books.isEmpty { await loadBooks() }
}
}
private var bookList: some View {
List {
Section("Antigo Testamento") {
ForEach(oldTestament) { book in
bookRow(book)
}
}
Section("Novo Testamento") {
ForEach(newTestament) { book in
bookRow(book)
}
}
}
.navigationDestination(for: Book.self) { book in
ChapterReaderView(book: book)
}
}
private func bookRow(_ book: Book) -> some View {
NavigationLink(value: book) {
HStack {
Text(book.name)
Spacer()
Text("\(book.chapterCount) cap.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
private func loadBooks() async {
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
books = try await BibleAPI.books()
} catch {
errorMessage = "Verifique sua conexão e tente novamente."
}
}
}
#Preview {
BooksView()
}