
- Comprehensive README update documenting v2.0 architectural changes - Updated git remote to ssh://rockvilleav@git.rockvilletollandsda.church:10443/RTSDA/RTSDA-iOS.git - Documented unified ChurchService and 60% code reduction - Added new features: Home Feed, responsive reading, enhanced UI - Corrected license information (GPL v3 with church content copyright) - Updated build instructions and technical stack details
404 lines
13 KiB
Swift
404 lines
13 KiB
Swift
import Foundation
|
|
import Observation
|
|
|
|
// MARK: - Media Type Enum
|
|
enum MediaType: String, CaseIterable {
|
|
case sermons = "sermons"
|
|
case livestreams = "livestreams"
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .sermons:
|
|
return "Sermons"
|
|
case .livestreams:
|
|
return "Live Archives"
|
|
}
|
|
}
|
|
|
|
var icon: String {
|
|
switch self {
|
|
case .sermons:
|
|
return "play.rectangle.fill"
|
|
case .livestreams:
|
|
return "dot.radiowaves.left.and.right"
|
|
}
|
|
}
|
|
}
|
|
|
|
@Observable
|
|
class ChurchDataService {
|
|
// MARK: - Properties
|
|
|
|
// State
|
|
var isLoading = false
|
|
var error: Error?
|
|
|
|
// Content
|
|
var events: [ChurchEvent] = []
|
|
var sermons: [Sermon] = []
|
|
var livestreamArchives: [Sermon] = []
|
|
var bulletins: [ChurchBulletin] = []
|
|
var dailyVerse: BibleVerse?
|
|
|
|
// Stream status
|
|
var isStreamLive = false
|
|
|
|
// Media Type Selection
|
|
var currentMediaType: MediaType = .sermons
|
|
|
|
// Feed
|
|
var feedItems: [FeedItem] = []
|
|
|
|
// MARK: - Public Methods
|
|
|
|
/// Load all content for the home feed
|
|
func loadHomeFeed() async {
|
|
await withTaskGroup(of: Void.self) { group in
|
|
group.addTask { await self.loadEvents() }
|
|
group.addTask { await self.loadRecentSermons() }
|
|
group.addTask { await self.loadRecentBulletins() }
|
|
group.addTask { await self.loadDailyVerse() }
|
|
group.addTask { await self.loadStreamStatus() }
|
|
}
|
|
|
|
await updateFeed()
|
|
}
|
|
|
|
/// Load upcoming events (for home feed - limited)
|
|
func loadEvents() async {
|
|
do {
|
|
isLoading = true
|
|
error = nil
|
|
|
|
let eventsJson = fetchEventsJson()
|
|
|
|
// Use Rust function for JSON parsing - NO business logic in Swift!
|
|
let eventsArrayJson = parseEventsFromJson(eventsJson: eventsJson)
|
|
let data = eventsArrayJson.data(using: .utf8) ?? Data()
|
|
let allEvents = try JSONDecoder().decode([ChurchEvent].self, from: data)
|
|
|
|
await MainActor.run {
|
|
// For home feed, limit to 5 events
|
|
self.events = Array(allEvents.prefix(5))
|
|
self.isLoading = false
|
|
|
|
}
|
|
} catch {
|
|
print("❌ Failed to load events: \(error)")
|
|
await MainActor.run {
|
|
self.error = error
|
|
self.isLoading = false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Load all events (for events list view)
|
|
func loadAllEvents() async {
|
|
do {
|
|
isLoading = true
|
|
error = nil
|
|
|
|
let eventsJson = fetchEventsJson()
|
|
|
|
// Use Rust function for JSON parsing - NO business logic in Swift!
|
|
let eventsArrayJson = parseEventsFromJson(eventsJson: eventsJson)
|
|
let data = eventsArrayJson.data(using: .utf8) ?? Data()
|
|
let allEvents = try JSONDecoder().decode([ChurchEvent].self, from: data)
|
|
|
|
await MainActor.run {
|
|
self.events = allEvents
|
|
self.isLoading = false
|
|
|
|
}
|
|
} catch {
|
|
print("❌ Failed to load all events: \(error)")
|
|
await MainActor.run {
|
|
self.error = error
|
|
self.isLoading = false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Load recent sermons
|
|
func loadRecentSermons() async {
|
|
do {
|
|
let sermonsJson = fetchSermonsJson()
|
|
|
|
// Use Rust function for JSON parsing - NO business logic in Swift!
|
|
let validatedJson = parseSermonsFromJson(sermonsJson: sermonsJson)
|
|
let data = validatedJson.data(using: .utf8) ?? Data()
|
|
let sermonList = try JSONDecoder().decode([Sermon].self, from: data)
|
|
|
|
await MainActor.run {
|
|
// Get the 5 most recent sermons
|
|
self.sermons = Array(sermonList.prefix(5))
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
self.error = error
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Load all sermons (for Watch tab)
|
|
func loadAllSermons() async {
|
|
do {
|
|
isLoading = true
|
|
error = nil
|
|
|
|
let sermonsJson = fetchSermonsJson()
|
|
|
|
// Use Rust function for JSON parsing - NO business logic in Swift!
|
|
let validatedJson = parseSermonsFromJson(sermonsJson: sermonsJson)
|
|
let data = validatedJson.data(using: .utf8) ?? Data()
|
|
let sermonList = try JSONDecoder().decode([Sermon].self, from: data)
|
|
|
|
await MainActor.run {
|
|
self.sermons = sermonList
|
|
self.isLoading = false
|
|
|
|
}
|
|
} catch {
|
|
print("❌ Failed to load sermons: \(error)")
|
|
await MainActor.run {
|
|
self.error = error
|
|
self.isLoading = false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Load all livestream archives (for Watch tab)
|
|
func loadAllLivestreamArchives() async {
|
|
do {
|
|
isLoading = true
|
|
error = nil
|
|
|
|
let archivesJson = fetchLivestreamArchiveJson()
|
|
|
|
// Use Rust function for JSON parsing - NO business logic in Swift!
|
|
let validatedJson = parseSermonsFromJson(sermonsJson: archivesJson)
|
|
let data = validatedJson.data(using: .utf8) ?? Data()
|
|
let archiveList = try JSONDecoder().decode([Sermon].self, from: data)
|
|
|
|
await MainActor.run {
|
|
self.livestreamArchives = archiveList
|
|
self.isLoading = false
|
|
|
|
}
|
|
} catch {
|
|
print("❌ Failed to load livestream archives: \(error)")
|
|
await MainActor.run {
|
|
self.error = error
|
|
self.isLoading = false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Load content based on current media type
|
|
func loadCurrentMediaType() async {
|
|
switch currentMediaType {
|
|
case .sermons:
|
|
await loadAllSermons()
|
|
case .livestreams:
|
|
await loadAllLivestreamArchives()
|
|
}
|
|
}
|
|
|
|
/// Switch media type and load appropriate content
|
|
func switchMediaType(to newType: MediaType) async {
|
|
await MainActor.run {
|
|
self.currentMediaType = newType
|
|
}
|
|
await loadCurrentMediaType()
|
|
}
|
|
|
|
/// Get current content based on media type
|
|
var currentContent: [Sermon] {
|
|
switch currentMediaType {
|
|
case .sermons:
|
|
return sermons
|
|
case .livestreams:
|
|
return livestreamArchives
|
|
}
|
|
}
|
|
|
|
/// Load recent bulletins
|
|
func loadRecentBulletins() async {
|
|
do {
|
|
let bulletinsJson = fetchBulletinsJson()
|
|
|
|
// Use Rust function for JSON parsing - NO business logic in Swift!
|
|
let validatedJson = parseBulletinsFromJson(bulletinsJson: bulletinsJson)
|
|
let data = validatedJson.data(using: .utf8) ?? Data()
|
|
let bulletinList = try JSONDecoder().decode([ChurchBulletin].self, from: data)
|
|
|
|
await MainActor.run {
|
|
// Get the 3 most recent bulletins
|
|
self.bulletins = Array(bulletinList.prefix(3))
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
self.error = error
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Load all bulletins (for Discover tab)
|
|
func loadAllBulletins() async {
|
|
do {
|
|
isLoading = true
|
|
error = nil
|
|
|
|
let bulletinsJson = fetchBulletinsJson()
|
|
|
|
// Use Rust function for JSON parsing - NO business logic in Swift!
|
|
let validatedJson = parseBulletinsFromJson(bulletinsJson: bulletinsJson)
|
|
let data = validatedJson.data(using: .utf8) ?? Data()
|
|
let bulletinList = try JSONDecoder().decode([ChurchBulletin].self, from: data)
|
|
|
|
await MainActor.run {
|
|
self.bulletins = bulletinList
|
|
self.isLoading = false
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
self.error = error
|
|
self.isLoading = false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Load daily Bible verse
|
|
func loadDailyVerse() async {
|
|
do {
|
|
let verseJson = fetchRandomBibleVerseJson()
|
|
|
|
// Use Rust function for JSON parsing - NO business logic in Swift!
|
|
let validatedJson = parseBibleVerseFromJson(verseJson: verseJson)
|
|
let data = validatedJson.data(using: .utf8) ?? Data()
|
|
let verse = try JSONDecoder().decode(BibleVerse.self, from: data)
|
|
|
|
await MainActor.run {
|
|
self.dailyVerse = verse
|
|
|
|
}
|
|
} catch {
|
|
print("❌ Failed to load daily verse: \(error)")
|
|
print("📖 Raw JSON was: \(fetchRandomBibleVerseJson())")
|
|
}
|
|
}
|
|
|
|
/// Search Bible verses
|
|
func searchVerses(_ query: String) async -> [BibleVerse] {
|
|
do {
|
|
let versesJson = fetchBibleVerseJson(query: query)
|
|
|
|
// Use Rust function for JSON parsing - NO business logic in Swift!
|
|
let validatedJson = parseBibleVerseFromJson(verseJson: versesJson)
|
|
let data = validatedJson.data(using: .utf8) ?? Data()
|
|
return try JSONDecoder().decode([BibleVerse].self, from: data)
|
|
} catch {
|
|
print("Failed to search verses: \(error)")
|
|
return []
|
|
}
|
|
}
|
|
|
|
/// Load stream status to check if live
|
|
func loadStreamStatus() async {
|
|
// Use Rust function directly - NO JSON parsing in Swift!
|
|
// Note: Using simple extraction until getStreamLiveStatus() binding is available
|
|
let statusJson = fetchStreamStatusJson()
|
|
let isLive = statusJson.contains("\"is_live\":true")
|
|
await MainActor.run {
|
|
self.isStreamLive = isLive
|
|
}
|
|
}
|
|
|
|
/// Submit contact form
|
|
func submitContact(name: String, email: String, subject: String, message: String, phone: String = "") async -> Bool {
|
|
|
|
|
|
let resultJson = submitContactV2Json(
|
|
name: name,
|
|
email: email,
|
|
subject: subject,
|
|
message: message,
|
|
phone: phone
|
|
)
|
|
|
|
|
|
|
|
// Use Rust function for JSON parsing - NO business logic in Swift!
|
|
let validatedJson = parseContactResultFromJson(resultJson: resultJson)
|
|
let data = validatedJson.data(using: .utf8) ?? Data()
|
|
|
|
do {
|
|
let result = try JSONDecoder().decode(ContactSubmissionResult.self, from: data)
|
|
return result.success
|
|
} catch {
|
|
|
|
return false
|
|
}
|
|
}
|
|
|
|
// MARK: - Private Methods
|
|
|
|
/// Create unified feed from all content
|
|
private func updateFeed() async {
|
|
await MainActor.run {
|
|
guard let eventsJson = try? JSONEncoder().encode(events),
|
|
let sermonsJson = try? JSONEncoder().encode(sermons),
|
|
let bulletinsJson = try? JSONEncoder().encode(bulletins),
|
|
let verseJson = try? JSONEncoder().encode(dailyVerse),
|
|
let eventsJsonString = String(data: eventsJson, encoding: .utf8),
|
|
let sermonsJsonString = String(data: sermonsJson, encoding: .utf8),
|
|
let bulletinsJsonString = String(data: bulletinsJson, encoding: .utf8),
|
|
let verseJsonString = String(data: verseJson, encoding: .utf8) else {
|
|
// Simple fallback - empty feed
|
|
self.feedItems = []
|
|
return
|
|
}
|
|
|
|
let feedJson = generateHomeFeedJson(
|
|
eventsJson: eventsJsonString,
|
|
sermonsJson: sermonsJsonString,
|
|
bulletinsJson: bulletinsJsonString,
|
|
verseJson: verseJsonString
|
|
)
|
|
|
|
guard let data = feedJson.data(using: .utf8),
|
|
let rustFeedItems = try? JSONDecoder().decode([RustFeedItem].self, from: data) else {
|
|
// Simple fallback - empty feed
|
|
self.feedItems = []
|
|
return
|
|
}
|
|
|
|
// Convert to Swift FeedItem models
|
|
self.feedItems = rustFeedItems.compactMap { rustItem -> FeedItem? in
|
|
let timestamp = parseDate(rustItem.timestamp) ?? Date()
|
|
|
|
switch rustItem.feedType {
|
|
case .event(let event):
|
|
return FeedItem(type: .event(event), timestamp: timestamp)
|
|
case .sermon(let sermon):
|
|
return FeedItem(type: .sermon(sermon), timestamp: timestamp)
|
|
case .bulletin(let bulletin):
|
|
return FeedItem(type: .bulletin(bulletin), timestamp: timestamp)
|
|
case .verse(let verse):
|
|
return FeedItem(type: .verse(verse), timestamp: timestamp)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func parseDate(_ dateString: String) -> Date? {
|
|
let formatter = ISO8601DateFormatter()
|
|
return formatter.date(from: dateString)
|
|
}
|
|
}
|
|
|
|
// MARK: - Singleton Access
|
|
extension ChurchDataService {
|
|
static let shared = ChurchDataService()
|
|
}
|