Created
August 21, 2019 11:04
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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