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