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