Skip to content

Instantly share code, notes, and snippets.

@frboulais
Last active April 2, 2022 05:57
Show Gist options
  • Save frboulais/7815a6206e45343c1fbd18ac04053ddf to your computer and use it in GitHub Desktop.
Save frboulais/7815a6206e45343c1fbd18ac04053ddf to your computer and use it in GitHub Desktop.
🔥 Help Center [Firestore x Combine x SwiftUI x Codable]
struct HelpdeskArticle: Codable {
@DocumentID var id: String?
let title: String
let content: String
let locale: String
}
struct HelpdeskView: View {
@StateObject var viewModel = HelpdeskViewModel()
var body: some View {
Group {
if let sections = viewModel.sections {
if sections.isEmpty {
Text("No article available for your language")
.foregroundStyle(.secondary)
} else {
List {
ForEach(sections) { section in
Section {
if viewModel.isSectionExpanded(section) {
Text(section.content)
}
} header: {
Button {
viewModel.toggleSection(section)
} label: {
HStack {
Text(section.title)
.font(.headline)
Spacer()
Image(systemName: "chevron.right")
.imageScale(.medium)
.rotationEffect(.degrees(viewModel.isSectionExpanded(section) ? 90 : 0))
}
}
}
.foregroundColor(.asset(.midnight))
}
.listRowSeparator(.hidden)
}
.listStyle(.plain)
}
} else if let error = viewModel.error {
Text(error.localizedDescription)
.foregroundStyle(.secondary)
} else {
ProgressView()
}
}
.task {
await viewModel.fetchArticles()
}
.navigationTitle("HELP CENTER")
}
}
class HelpdeskViewModel: ObservableObject {
struct Section: Equatable, Identifiable {
let id: String?
let title: String
let content: AttributedString
init(article: HelpdeskArticle) throws {
self.id = article.id
self.title = article.title
self.content = try .init(markdown: article.content)
}
}
@Injected var firestoreManager: FirestoreManager
@Published var sections: [Section]?
@Published var error: Error?
@Published var expandedSection: Section?
@MainActor func fetchArticles() async {
do {
guard let languageCode = Locale.current.languageCode else {
return
}
sections = try await firestoreManager.helpdesk(for: languageCode)
.map { try .init(article: $0) }
} catch {
self.error = error
}
}
func isSectionExpanded(_ section: Section) -> Bool {
section == expandedSection
}
func toggleSection(_ section: Section) {
withAnimation {
expandedSection = isSectionExpanded(section) ? nil : section
}
}
}
protocol FirestoreManagerProtocol {
func helpdesk(for locale: String) async throws -> [HelpdeskArticle]
}
class FirestoreManager: FirestoreManagerProtocol
func helpdesk(for locale: String) async throws -> [HelpdeskArticle] {
try await firestore.collection(.helpdesk)
.whereField(.locale, isEqualTo: locale)
.getDocuments()
.documents.map { try $0.data(as: HelpdeskArticle.self) }
}
}
fileprivate extension Firestore {
enum Collection: String {
case users, helpdesk
}
func collection(_ collection: Collection) -> CollectionReference {
self.collection(collection.rawValue)
}
}
fileprivate extension DocumentReference {
enum Collection: String {
case entries
}
func collection(_ collection: Collection) -> CollectionReference {
self.collection(collection.rawValue)
}
}
fileprivate extension Query {
enum Field: String {
case locale
}
func whereField(_ field: Field, isEqualTo value: Any) -> Query {
self.whereField(field.rawValue, isEqualTo: value)
}
}
extension Resolver {
public static func registerFirestoreManager() {
register { FirestoreManager() }
.implements(FirestoreManagerProtocol.self)
.scope(.application)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment