In this tutorial i'm going to create a simple Library App CRUD (Create, Read, Update, and Delete) App Using Firebase With SwiftUI and SDWebImageSwiftUI
https://console.firebase.google.com/
SDWebImageSwiftUI
https://github.com/SDWebImage/SDWebImageSwiftUI.git
ContentView.swift
// // ContentView.swift // DevSwiftUI // // Created by Cairocoders // import SwiftUI import Firebase import SDWebImageSwiftUI struct ContentView: View { @StateObject var viewModel = BooksViewModel() @State var presentAddBookSheet = false private var addButton: some View { Button(action: { self.presentAddBookSheet.toggle() }) { Image(systemName: "plus") } } private func bookRowView(book: Book) -> some View { NavigationLink(destination: BookDetailsView(book: book)) { VStack(alignment: .leading) { HStack { AnimatedImage(url: URL(string: book.image)!).resizable().frame(width: 65, height: 65).clipShape(Circle()) VStack(alignment: .leading) { Text(book.title) .fontWeight(.bold) Text(book.author) } } } } } var body: some View { NavigationView { List { ForEach (viewModel.books) { book in bookRowView(book: book) } .onDelete() { indexSet in viewModel.removeBooks(atOffsets: indexSet) } } .navigationBarTitle("Books") .navigationBarItems(trailing: addButton) .onAppear() { print("BooksListView appears. Subscribing to data updates.") self.viewModel.subscribe() } .sheet(isPresented: self.$presentAddBookSheet) { BookEditView() } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }Book.swift
// // Book.swift // DevSwiftUI // // Created by Cairocoders // import Foundation import FirebaseFirestoreSwift struct Book: Identifiable, Codable { @DocumentID var id: String? var title: String var author: String var numberOfPages: Int var image: String enum CodingKeys: String, CodingKey { case id case title case author case numberOfPages = "pages" case image } }BookDetailsView.swift
// // BookDetailsView.swift // DevSwiftUI // // Created by Cairocoders // import SwiftUI import SDWebImageSwiftUI struct BookDetailsView: View { @Environment(\.presentationMode) var presentationMode @State var presentEditBookSheet = false var book: Book private func editButton(action: @escaping () -> Void) -> some View { Button(action: { action() }) { Text("Edit") } } var body: some View { Form { Section(header: Text("Book")) { Text(book.title) Text("\(book.numberOfPages) pages") } Section(header: Text("Author")) { Text(book.author) } Section(header: Text("Photo")) { AnimatedImage(url: URL(string: book.image)!).resizable().frame(width: 300, height: 300) } } .navigationBarTitle(book.title) .navigationBarItems(trailing: editButton { self.presentEditBookSheet.toggle() }) .onAppear() { print("BookDetailsView.onAppear() for \(self.book.title)") } .onDisappear() { print("BookDetailsView.onDisappear()") } .sheet(isPresented: self.$presentEditBookSheet) { BookEditView(viewModel: BookViewModel(book: book), mode: .edit) { result in if case .success(let action) = result, action == .delete { self.presentationMode.wrappedValue.dismiss() } } } } } struct BookDetailsView_Previews: PreviewProvider { static var previews: some View { let book = Book(title: "Coder", author: "Cairocoders", numberOfPages: 23, image: "photo1") return NavigationView { BookDetailsView(book: book) } } }BookEditView.swift
// // BookEditView.swift // DevSwiftUI // // Created by Cairocoders // import SwiftUI enum Mode { case new case edit } enum Action { case delete case done case cancel } struct BookEditView: View { @Environment(\.presentationMode) private var presentationMode @State var presentActionSheet = false @ObservedObject var viewModel = BookViewModel() var mode: Mode = .new var completionHandler: ((Result<Action, Error>) -> Void)? var cancelButton: some View { Button(action: { self.handleCancelTapped() }) { Text("Cancel") } } var saveButton: some View { Button(action: { self.handleDoneTapped() }) { Text(mode == .new ? "Done" : "Save") } .disabled(!viewModel.modified) } var body: some View { NavigationView { Form { Section(header: Text("Book")) { TextField("Title", text: $viewModel.book.title) TextField("Number of pages", value: $viewModel.book.numberOfPages, formatter: NumberFormatter()) } Section(header: Text("Author")) { TextField("Author", text: $viewModel.book.author) } Section(header: Text("Photo")) { TextField("Image", text: $viewModel.book.image) } if mode == .edit { Section { Button("Delete book") { self.presentActionSheet.toggle() } .foregroundColor(.red) } } } .navigationTitle(mode == .new ? "New book" : viewModel.book.title) .navigationBarTitleDisplayMode(mode == .new ? .inline : .large) .navigationBarItems( leading: cancelButton, trailing: saveButton ) .actionSheet(isPresented: $presentActionSheet) { ActionSheet(title: Text("Are you sure?"), buttons: [ .destructive(Text("Delete book"), action: { self.handleDeleteTapped() }), .cancel() ]) } } } func handleCancelTapped() { self.dismiss() } func handleDoneTapped() { self.viewModel.handleDoneTapped() self.dismiss() } func handleDeleteTapped() { viewModel.handleDeleteTapped() self.dismiss() self.completionHandler?(.success(.delete)) } func dismiss() { self.presentationMode.wrappedValue.dismiss() } } struct BookEditView_Previews: PreviewProvider { static var previews: some View { let book = Book(title: "Coder", author: "Cairocoders", numberOfPages: 89, image: "photo1") let bookViewModel = BookViewModel(book: book) return BookEditView(viewModel: bookViewModel, mode: .edit) } }BookViewModel.swift
// // BookViewModel.swift // DevSwiftUI // // Created by Cairocoders // import Foundation import Combine import FirebaseFirestore class BookViewModel: ObservableObject { @Published var book: Book @Published var modified = false private var cancellables = Set<AnyCancellable>() init(book: Book = Book(title: "", author: "", numberOfPages: 0, image: "")) { self.book = book self.$book .dropFirst() .sink { [weak self] book in self?.modified = true } .store(in: &self.cancellables) } private var db = Firestore.firestore() private func addBook(_ book: Book) { do { let _ = try db.collection("books").addDocument(from: book) } catch { print(error) } } private func updateBook(_ book: Book) { if let documentId = book.id { do { try db.collection("books").document(documentId).setData(from: book) } catch { print(error) } } } private func updateOrAddBook() { if let _ = book.id { self.updateBook(self.book) } else { addBook(book) } } private func removeBook() { if let documentId = book.id { db.collection("books").document(documentId).delete { error in if let error = error { print(error.localizedDescription) } } } } func handleDoneTapped() { self.updateOrAddBook() } func handleDeleteTapped() { self.removeBook() } }BooksViewModel.swift
// // BooksViewModel.swift // DevSwiftUI // // Created by Cairocoders // import Foundation import Combine import FirebaseFirestore class BooksViewModel: ObservableObject { @Published var books = [Book]() private var db = Firestore.firestore() private var listenerRegistration: ListenerRegistration? deinit { unsubscribe() } func unsubscribe() { if listenerRegistration != nil { listenerRegistration?.remove() listenerRegistration = nil } } func subscribe() { if listenerRegistration == nil { listenerRegistration = db.collection("books").addSnapshotListener { (querySnapshot, error) in guard let documents = querySnapshot?.documents else { print("No documents") return } self.books = documents.compactMap { queryDocumentSnapshot in try? queryDocumentSnapshot.data(as: Book.self) } } } } func removeBooks(atOffsets indexSet: IndexSet) { let books = indexSet.lazy.map { self.books[$0] } books.forEach { book in if let documentId = book.id { db.collection("books").document(documentId).delete { error in if let error = error { print("Unable to remove document: \(error.localizedDescription)") } } } } } }DevSwiftUIApp.swift
// // DevSwiftUIApp.swift // DevSwiftUI // // Created by Cairocoders // import SwiftUI import Firebase @main struct DevSwiftUIApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: NSObject,UIApplicationDelegate{ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { FirebaseApp.configure() return true } }