article

Tuesday, October 5, 2021

SwiftUI Firebase CRUD (create-read-update-delete) book Library app

SwiftUI Firebase CRUD (create-read-update-delete) book Library app

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
    }
}

Related Post