Skip to content

Instantly share code, notes, and snippets.

@michaelevensen
Created August 21, 2019 11:04
Show Gist options
  • Save michaelevensen/c68943de0d2eebdedd9606f526f42175 to your computer and use it in GitHub Desktop.
Save michaelevensen/c68943de0d2eebdedd9606f526f42175 to your computer and use it in GitHub Desktop.
A class for easily unwrapping, storing and mapping FireStore results to models. Inspired by https://gist.github.com/michaelevensen/c74b79292fa8dba437552f7a9f10a5dc.
//
// LocalCollection.swift
// Slow
//
// Created by Michael Nino Evensen on 20/08/2019.
// Copyright © 2019 Michael Nino Evensen. All rights reserved.
//
import FirebaseFirestore
/// A type that can be initialized from a Firestore document.
public protocol DocumentSerializable {
/// Initializes an instance from a Firestore document. May fail if the
/// document is missing required fields.
init?(document: QueryDocumentSnapshot)
/// Initializes an instance from a Firestore document. May fail if the
/// document does not exist or is missing required fields.
init?(document: DocumentSnapshot)
/// The document reference of the object in Firestore.
var reference: DocumentReference { get }
/// The representation of a document-serializable object in Firestore.
var data: [String: Any] { get }
}
final class LocalCollection<T: DocumentSerializable> {
private(set) var items: [T]
private(set) var documents: [DocumentSnapshot] = []
// Firestore Query
let query: Query
// Completion handler for passing an array of document changes (DocumentChange) on update to specified closure.
private let updateHandler: (([DocumentChange]) -> ())?
// Completion handler for single query fetches.
private let completionHandler: ((_ completed: Bool, _ error: LocalCollectionError?) -> ())?
// Snapshot listener.
private var listener: ListenerRegistration? {
didSet {
// Remove old snapshot listener if new is set.
oldValue?.remove()
}
}
/// Return item count.
var count: Int {
return self.items.count
}
subscript(index: Int) -> T {
return self.items[index]
}
// Initialise models from [QueryDocumentSnapshot].
fileprivate func items(from documents: [QueryDocumentSnapshot]) -> [T] {
return documents.compactMap({
if let item = T(document: $0) {
return item
}
else {
debugPrint(LocalCollectionError.couldNotInitialise(type: T.self, dictionary: $0.data()))
return nil
}
})
}
/// Init from query for single fetches.
init(query: Query, completion: @escaping (_ completed: Bool, _ error: LocalCollectionError?) -> ()) {
// Initialise items array.
self.items = []
// Assign
self.query = query
self.completionHandler = completion
// Don't set an update handler
self.updateHandler = nil
}
/// Init from query for snapshot listening.
init(query: Query, updateHandler: @escaping ([DocumentChange]) -> ()) {
// Initialise items array.
self.items = []
// Assign
self.query = query
self.updateHandler = updateHandler
// Don't set a completion handler
self.completionHandler = nil
}
/// Return index of document (DocumentSnapshot) in collection.
func index(of document: DocumentSnapshot) -> Int? {
return self.documents.firstIndex(where: { $0.documentID == document.documentID })
}
/// Get documents for initialised query. Single fetch.
func get() {
guard let completionHandler = self.completionHandler else {
debugPrint(LocalCollectionError.noCompletionHandlerSet)
return
}
self.query.getDocuments { (querySnapshot, error) in
if let error = error {
completionHandler(false, LocalCollectionError.couldNotGetDocuments(query: self.query, error))
return
}
// Unwrap query snapshot
guard let snapshot = querySnapshot else { return }
// Initialise [Model]'s
self.items = self.items(from: snapshot.documents)
// Set documents
self.documents = snapshot.documents
// Return fetched results
// - Note: Here we don't need to assign an updateHandler since this is a single fetch and doesn't need to listen to document changes.
completionHandler(true, nil)
}
}
/// Start listening to changes for initialised query.
func startListening() {
// Check that listener doesn't already exist.
guard self.listener == nil else { return }
// Check that updateHandler is set for snapshot listening.
guard let updateHandler = self.updateHandler else {
debugPrint(LocalCollectionError.noUpdateHandlerSet)
return
}
// Add snapshot listener
self.listener = self.query.addSnapshotListener { [unowned self] querySnapshot, error in
if let error = error {
debugPrint(LocalCollectionError.snapshotListenFailure(query: querySnapshot, error))
return
}
// Unwrap query snapshot
guard let snapshot = querySnapshot else { return }
// Initialise [Model]'s
self.items = self.items(from: snapshot.documents)
// Set documents
self.documents = snapshot.documents
// Set [DocumentChanges] array to updateHandler
updateHandler(snapshot.documentChanges)
}
}
/// Stop listening to changes.
func stopListening() {
self.listener = nil
}
deinit {
if self.listener != nil { self.stopListening() }
}
}
extension LocalCollection {
enum LocalCollectionError: Error {
case snapshotListenFailure(query: QuerySnapshot?, _ error: Error)
case noCompletionHandlerSet
case noUpdateHandlerSet
case couldNotGetDocuments(query: Query, _ error: Error)
case couldNotInitialise(type: T.Type, dictionary: [String: Any])
}
}
/// A type that can be initialized from a Firestore document.
//public protocol LocalCollectionView {
// associatedtype T
//
// var baseQuery: Query { get }
// var dataSource: T { get set }
//
// func dataSourceForQuery(_ query: Query) -> T
//}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment