iOS Databases : Module 1 Wrap-up Challenge

So, in the wrap-up challenge for Module 1 of the CWC+ iOS Database course, we challenged with implementing add/edit/delete database functions on a Reading List App. If we so choose, we can start from scratch and create the whole app from new. I don’t feel confident enough to wade through potential hours of code hiccups/challenges when my head is trying to stay focussed on the essentials of Firestore right now, so I’ll be using the provided “starter project”.

The “starter project” contains all of the display code, only with the database class and included functions missing those all-important database calls.

So, to start with, I’ll set up a new project and link it to Firebase as detailed here.

Once that’s done, I’m recreating the views in my new Xcode project and copying across the ones that Chris has provided with the challenge.

The first thing that’s popped up in front of me is that one of the views calls up –

import Combine

I’ve no idea what the Combine framework is, as it hasn’t been mentioned before. Probably a good job, then, that I decided not to start this project from scratch because I wouldn’t have been able to overcome a problem that hasn’t previously been covered in the tutorials.

With everything copied, a “quick” check to see if the project will build (discovering it needs to update target iOS version numbers), watching for the countdown as Xcode rushes its way tortoise-like through “Building | 379 / 3290” … Good cure for insomnia, that one… But, it turns out all is okay so far.

As I continue working through the project, I’m reasonably happy with my addBook(), addGenre(), and getGenres() methods, but I’m not entirely sure how to properly assign the data from Firebase to the appropriate class in the getBooksByGenre() method. I think it’s time to check out the supplied solution.

Hmm, the supplied solution uses code similar to this:

let title = data["title"] as? String ?? ""

Now, I’m pretty happy with what most of that does but what is as? ? We’ve used as (I think), but not as?, so we’ve not come across that before and no amount of Googling brings up any insight into it. So, presumably, I wasn’t going to be able to complete this part of the challenge anyway. Nice.

I’m presuming that the statement says something like “If data["title"] has a value, convert it to a String value type and, if not, assign it the Empty String – and assign whichever of the two you get to the variable title. Is that what it’s doing? Not sure, but one thing I am sure about is that I haven’t previously enountered this and so wouldn’t have used it in my code. Either trial-and-error would have forced me to write some convoluted code to achieve the same result, or else I just wouldn’t be able to complete the challenge.

In order to proceed, I’ll just copy those lines of code to my project. It’s not really the done thing, though, when you’re trying to complete a challenge yourself.

To hold myself accountable, I’m going to compare the methods below.

addBook()

Solution
let db = Firestore.firestore()
let books = db.collection("books")
books.document().setData(["title": book.title, "author": book.author, "genre": book.genre, "status": book.status, "pages": book.pages, "rating": book.rating])
Mine
let db = Firestore.firestore()
let newBook = db.collection("books")
    newBook.addDocument(data: [
        "title" : book.title,
        "author" : book.author,
        "genre" : book.genre,
        "status" : book.status,
        "pages" : book.pages,
        "rating" : book.rating
    ])

No real difference here. I just chose to use the more understandable .addDocument completion handler rather than .setData. That’s not going to set the world on fire.

deleteBook()

Solution
let db = Firestore.firestore()
let books = db.collection("books")
let bookDoc = books.document(book.id)
bookDoc.delete()
Mine
let db = Firestore.firestore()
let currentBook = db.collection("books").document(book.id)
currentBook.delete { error in
    if let error = error {
        print(error.localizedDescription)
    }
}

I chose to do some Error Handling with mine but, otherwise, the only difference is a personal preference. I went straight for the document when defining currentBook whereas the solution takes two steps to define bookDoc. I figure if I’m not going to be using the collection reference anywhere else in the method, why take two lines when you only need one?

updateBookData()

Solution
let db = Firestore.firestore()
let books = db.collection("books")
let bookDoc = books.document(book.id)
bookDoc.updateData(["genre": book.genre, "status": book.status, "rating": book.rating])
Mine
let db = Firestore.firestore()
let currentBook = db.collection("books").document(book.id)
currentBook.updateData([
    "title" : book.title,
    "author" : book.author,
    "genre" : book.genre,
    "status" : book.status,
    "pages" : String(book.pages),
    "rating" : String(book.rating)
]) { error in
    if let error = error {
        print(error.localizedDescription)
    }
}

Other than my choice to set up the method to allow for updating all fields (I don’t do half measures), and indulging in a touch of Error Handling again, the primary difference here is that Xcode threw up a “string conversion” error at me, leading my to having to do a touch of type casting. As before, I went straight for the document with a single currentBook declaration whilst the solution took two steps, but that’s really just personal preference.

getBooksByGenre()

Solution
let db = Firestore.firestore()
let consoles = db.collection("books")
let query = consoles.whereField("genre", in: [genre])
query.getDocuments { (querySnapshot, error) in
    if let error = error {
        print(error.localizedDescription)
        } else if let querySnapshot = querySnapshot {
            var allBooks: [Book] = []
            for doc in querySnapshot.documents {
            let data = doc.data()
            let id = doc.documentID
            let title = data["title"] as? String ?? ""
            let author = data["author"] as? String ?? ""
            let genre = data["genre"] as? String ?? ""
            let status = data["status"] as? String ?? ""
            let pages = data["pages"] as? Int ?? 0
            let rating = data["rating"] as? Int ?? 1
            allBooks.append(Book(id: id, title: title, author: author, genre: genre, status: status, pages: pages, rating: rating))
        }
        self.books[genre] = allBooks 
    } else {
        print("No data returned")
    }
}
Main
let db = Firestore.firestore()
let bookCollection = db.collection("books")
let genreQuery = bookCollection.whereField("genre", isEqualTo: genre)
genreQuery.getDocuments { querySnapshot, error in
    if let error = error {
        print(error.localizedDescription)
    } else if let querySnapshot = querySnapshot {
        var bookList: [Book] = []
        for doc in querySnapshot.documents {
            let bookData = doc.data()
            let docId = doc.documentID
            let title = bookData["title"] as? String ?? ""
            let author = bookData["author"] as? String ?? ""
            let genre = bookData["genre"] as? String ?? ""
            let status = bookData["status"] as? String ?? ""
            let pages = bookData["pages"] as? Int ?? 0
            let rating = bookData["rating"] as? Int ?? 1
            bookList.append(Book(id: docId, title: title, author: author, genre: genre, status: status, pages: pages, rating: rating))
        }
        self.books[genre] = bookList
    }
}

As mentioned above, all of the lines containing as? were taken from the solution. Other than that, the main difference here is that I used isEqualTo in my query – mainly because I didn’t see the point in using in: [array] when we’re only checking against a single value, and I wanted to see if it would work. It does!

addGenre()

Solution
let db = Firestore.firestore()
let genres = db.collection("genres")
genres.document(genre).setData([:])
Mine
let db = Firestore.firestore()
let newGenre = db.collection("genre")
newGenre.document(genre).setData([:])

No major revelation here, only an admission. I initially contemplated checking if the genre already existed in the database but then remembered that the handler .setData() doesn’t care if it does or doesn’t. If it already exists, it’ll overwrite it; if it doesn’t, it’ll create it. Either way, we achieve what we set out to achieve.

getGenres()

Solution
let db = Firestore.firestore()
let genres = db.collection("genres")
genres.getDocuments { (querySnapshot, error) in
    if let error = error {
        print(error.localizedDescription)
    } else if let querySnapshot = querySnapshot {
        var allGenres: [String] = []
        for doc in querySnapshot.documents {
            allGenres.append(doc.documentID)
        }
        self.genres = allGenres
    } else {
        print("No data returned")
    }
}
Main
let db = Firestore.firestore()
let getGenre = db.collection("genre")
getGenre.getDocuments { querySnapshot, error in
    if let error = error {
        print(error.localizedDescription)
    } else if let querySnapshot = querySnapshot {
        for doc in querySnapshot.documents {
            if(!self.genres.contains(doc.documentID)) {
                self.genres.append(doc.documentID)
            }
        }
    self.genres.sort()
    }
}

Here we have two ways of achieving the same thing. I instinctively went for writing direct to the genres class, but this had an odd side-effect in which some of the genres were duplicated. I threw in a if statement to add it only if it didn’t already exist, and that solved the problem.

I also noticed that the list of genres were displayed out of order so I added a quick .sort() modifier in there at the end.

Conclusion

Except for the requirement for the previously unknown as? code, this challenge was rewarding in that it encouraged me to go back over my notes in order to check which commands needed to be used. With my head set into all of that, I’m glad I decided against writing all of the display code from scratch as well.