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