article

Wednesday, June 2, 2021

SwiftUI Firestore CRUD Create, Read, Update and Delete

SwiftUI Firestore CRUD Create, Read, Update and Delete

Movie list screen is the homepage of the application it shows a list of all the movie records
Tap list item display details
tapping add plus button dialog form display add new records tap done button to add new movie
cancel button to dismiss dialog
tapping edit button to edit dialog form then save or Delete record

Swift packages https://github.com/firebase/firebase-ios-sdk

ContentView.swift
 
//
//  ContentView.swift
//  DevSwiftUI
//
//  Created by Cairocoders
//

import SwiftUI

struct ContentView: View {
    
    @StateObject var viewModel = MoviesViewModel() //MovieViewModel.swift
    @State var presentAddMovieSheet = false
    
    
    private var addButton: some View {
      Button(action: { self.presentAddMovieSheet.toggle() }) {
        Image(systemName: "plus")
      }
    }
    
    private func movieRowView(movie: Movie) -> some View {
       NavigationLink(destination: MovieDetailsView(movie: movie)) { //MovieDetailsView.swift
         VStack(alignment: .leading) {
           Text(movie.title)
             .font(.headline)
           //Text(movie.description)
           //  .font(.subheadline)
            Text(movie.year)
             .font(.subheadline)
         }
       }
    }
    
    var body: some View {
      NavigationView {
        List {
          ForEach (viewModel.movies) { movie in
            movieRowView(movie: movie)
          }
          .onDelete() { indexSet in
            //viewModel.removeMovies(atOffsets: indexSet)
            viewModel.removeMovies(atOffsets: indexSet)
          }
        }
        .navigationBarTitle("Movie")
        .navigationBarItems(trailing: addButton)
        .onAppear() {
          print("MoviesListView appears. Subscribing to data updates.")
          self.viewModel.subscribe()
        }
        .sheet(isPresented: self.$presentAddMovieSheet) {
          MovieEditView() //MovieEditView.swift
        }
        
      }// End Navigation
    }// End Body
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Models/Movie.swift
 
//
//  Movie.swift
//  DevSwiftUI
//
//  Created by Cairocoders
//

import Foundation
import Foundation
import FirebaseFirestoreSwift

struct Movie: Identifiable, Codable {
  @DocumentID var id: String?
  var title: String
  var description: String
  var year: String
  
  enum CodingKeys: String, CodingKey {
    case id
    case title
    case description
    case year
  }
}
DevSwiftUIApp.swift
 
//
//  DevSwiftUIApp.swift
//  DevSwiftUI
//
//  Created by Cairocoders
//

import SwiftUI
import Firebase

@main
struct DevSwiftUIApp: App {
    
    init() {
    FirebaseApp.configure()
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
ViewModels/MoviesViewModel.swift
 
//
//  MoviesViewModel.swift
//  DevSwiftUI
//
//  Created by Cairocoders on 6/3/21.
//

import Foundation
import Combine
import FirebaseFirestore

class MoviesViewModel: ObservableObject {
  @Published var movies = [Movie]()
  
  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("movielist").addSnapshotListener { (querySnapshot, error) in
        guard let documents = querySnapshot?.documents else {
          print("No documents")
          return
        }
        
        self.movies = documents.compactMap { queryDocumentSnapshot in
          try? queryDocumentSnapshot.data(as: Movie.self)
        }
      }
    }
  }
  
  func removeMovies(atOffsets indexSet: IndexSet) {
    let movies = indexSet.lazy.map { self.movies[$0] }
    movies.forEach { movie in
      if let documentId = movie.id {
        db.collection("movielist").document(documentId).delete { error in
          if let error = error {
            print("Unable to remove document: \(error.localizedDescription)")
          }
        }
      }
    }
  }

  
}
ViewModels/MovieViewModel.swift
 
//
//  MovieViewModel.swift
//  DevSwiftUI
//
//  Created by Cairocoders
//

import Foundation
import Combine
import FirebaseFirestore

class MovieViewModel: ObservableObject {
  
  @Published var movie: Movie
  @Published var modified = false
  
  private var cancellables = Set<anycancellable>()
  
  init(movie: Movie = Movie(title: "", description: "", year: "")) {
    self.movie = movie
    
    self.$movie
      .dropFirst()
      .sink { [weak self] movie in
        self?.modified = true
      }
      .store(in: &self.cancellables)
  }
  
  // Firestore
  
  private var db = Firestore.firestore()
  
  private func addMovie(_ movie: Movie) {
    do {
      let _ = try db.collection("movielist").addDocument(from: movie)
    }
    catch {
      print(error)
    }
  }
  
  private func updateMovie(_ movie: Movie) {
    if let documentId = movie.id {
      do {
        try db.collection("movielist").document(documentId).setData(from: movie)
      }
      catch {
        print(error)
      }
    }
  }
  
  private func updateOrAddMovie() {
    if let _ = movie.id {
      self.updateMovie(self.movie)
    }
    else {
      addMovie(movie)
    }
  }
  
  private func removeMovie() {
    if let documentId = movie.id {
      db.collection("movielist").document(documentId).delete { error in
        if let error = error {
          print(error.localizedDescription)
        }
      }
    }
  }
  
  // UI handlers
  
  func handleDoneTapped() {
    self.updateOrAddMovie()
  }
  
  func handleDeleteTapped() {
    self.removeMovie()
  }
  
}
View/MovieDetailsView.swift
 
//
//  MovieDetailsView.swift
//  DevSwiftUI
//
//  Created by Cairocoders
//

import SwiftUI

struct MovieDetailsView: View {
    @Environment(\.presentationMode) var presentationMode
    @State var presentEditMovieSheet = false
    
    var movie: Movie
    
    private func editButton(action: @escaping () -> Void) -> some View {
      Button(action: { action() }) {
        Text("Edit")
      }
    }
    
    var body: some View {
      Form {
        Section(header: Text("Movie")) {
          Text(movie.title)
          Text(movie.description)
            
        }
        
        Section(header: Text("Year")) {
            Text(movie.year)
        }
      }
      .navigationBarTitle(movie.title)
      .navigationBarItems(trailing: editButton {
        self.presentEditMovieSheet.toggle()
      })
      .onAppear() {
        print("MovieDetailsView.onAppear() for \(self.movie.title)")
      }
      .onDisappear() {
        print("MovieDetailsView.onDisappear()")
      }
      .sheet(isPresented: self.$presentEditMovieSheet) {
        MovieEditView(viewModel: MovieViewModel(movie: movie), mode: .edit) { result in
          if case .success(let action) = result, action == .delete {
            self.presentationMode.wrappedValue.dismiss()
          }
        }
      }
    }
    
  }

struct MovieDetailsView_Previews: PreviewProvider {
    static var previews: some View {
        let movie = Movie(title: "title movie", description: "this is a sample description", year: "2021")
        return
          NavigationView {
            MovieDetailsView(movie: movie)
          }
    }
}
View/MovieEditView.swift
 
//
//  MovieEditView.swift
//  DevSwiftUI
//
//  Created by Cairocoders
//

import SwiftUI

enum Mode {
  case new
  case edit
}

enum Action {
  case delete
  case done
  case cancel
}

struct MovieEditView: View {
    @Environment(\.presentationMode) private var presentationMode
    @State var presentActionSheet = false
    
    @ObservedObject var viewModel = MovieViewModel()
    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("Movie")) {
            TextField("Title", text: $viewModel.movie.title)
            TextField("Year", text: $viewModel.movie.year)
          }
          
          Section(header: Text("Description")) {
            TextField("Description", text: $viewModel.movie.description)
          }
          
          if mode == .edit {
            Section {
              Button("Delete Movie") { self.presentActionSheet.toggle() }
                .foregroundColor(.red)
            }
          }
        }
        .navigationTitle(mode == .new ? "New Movie" : viewModel.movie.title)
        .navigationBarTitleDisplayMode(mode == .new ? .inline : .large)
        .navigationBarItems(
          leading: cancelButton,
          trailing: saveButton
        )
        .actionSheet(isPresented: $presentActionSheet) {
          ActionSheet(title: Text("Are you sure?"),
                      buttons: [
                        .destructive(Text("Delete Movie"),
                                     action: { self.handleDeleteTapped() }),
                        .cancel()
                      ])
        }
      }
    }
    
    // Action Handlers
    
    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 MovieEditView_Previews: PreviewProvider {
//    static var previews: some View {
//        MovieEditView()
//    }
//}

struct MovieEditView_Previews: PreviewProvider {
  static var previews: some View {
    let movie = Movie(title: "Sample title", description: "Sample Description", year: "2020")
    let movieViewModel = MovieViewModel(movie: movie)
    return MovieEditView(viewModel: movieViewModel, mode: .edit)
  }
}

Related Post