Files
bible/Bible-Week/TrackReaderView.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

114 lines
3.5 KiB
Swift

//
// TrackReaderView.swift
// Bible-Week
//
// Leitura de uma trilha do dia: carrega os capítulos em sequência
// e permite marcar a trilha como concluída.
//
import SwiftUI
struct TrackReaderView: View {
let day: Int
let reading: TrackReading
@Bindable var plan: ReadingPlanState
@State private var chapters: [Chapter] = []
@State private var isLoading = false
@State private var errorMessage: String?
private var isCompleted: Bool {
plan.isCompleted(day: day, track: reading.track)
}
var body: some View {
Group {
if isLoading {
ProgressView("Carregando leitura...")
} else if let errorMessage {
ContentUnavailableView {
Label("Erro ao carregar", systemImage: "exclamationmark.triangle")
} description: {
Text(errorMessage)
} actions: {
Button("Tentar novamente") { Task { await load() } }
}
} else {
content
}
}
.navigationTitle(reading.label)
.navigationBarTitleDisplayMode(.inline)
.safeAreaInset(edge: .bottom) {
markButton
}
.task { await load() }
}
private var content: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 16) {
ForEach(chapters, id: \.self) { chapter in
Text("\(chapter.bookName) \(chapter.chapter)")
.font(.title3.bold())
.padding(.top, 8)
ForEach(chapter.verses) { verse in
Text("\(Text("\(verse.verse)").font(.caption2).foregroundColor(.accentColor).baselineOffset(4)) \(verse.text)")
.font(.body)
.fixedSize(horizontal: false, vertical: true)
}
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
}
}
private var markButton: some View {
Button {
toggleCompletion()
} label: {
Label(
isCompleted ? "Concluído" : "Marcar como concluído",
systemImage: isCompleted ? "checkmark.circle.fill" : "circle"
)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.tint(isCompleted ? .green : .accentColor)
.controlSize(.large)
.padding()
.background(.bar)
.accessibilityLabel(
isCompleted
? "\(reading.track.displayName) concluída. Toque para desmarcar."
: "Marcar \(reading.track.displayName) como concluída"
)
}
private func toggleCompletion() {
withAnimation {
plan.setCompleted(!isCompleted, day: day, track: reading.track)
}
if plan.isCompleted {
// Plano 180/180 concluído: remove o lembrete diário.
ReadingReminderService.cancelDailyReminder()
}
}
private func load() async {
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
var loaded: [Chapter] = []
for ref in reading.chapters {
loaded.append(try await BibleAPI.chapter(book: ref.bookCode, chapter: ref.chapter))
}
chapters = loaded
} catch {
errorMessage = "Verifique sua conexão e tente novamente."
}
}
}