Skip to content

Instantly share code, notes, and snippets.

@pteasima
Last active February 6, 2021 13:52
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pteasima/32e30fd6d5db6cc8e3974631a37ec286 to your computer and use it in GitHub Desktop.
Save pteasima/32e30fd6d5db6cc8e3974631a37ec286 to your computer and use it in GitHub Desktop.
Firebase + Combine extensions
import FirebaseAuth
import Combine
extension PublishersNamespace where Base: FirebaseAuth.Auth {
var currentUser: AnyPublisher<User?, Never> {
let userSubject = PassthroughSubject<User?, Never>()
let handle = base.addStateDidChangeListener { auth, user in
userSubject.send(user)
}
return userSubject
.prepend(base.currentUser)
.removeDuplicates()
.handleEvents(receiveCancel: { [weak base] in
base?.removeStateDidChangeListener(handle)
})
.eraseToAnyPublisher()
}
}
import Foundation
import Tagged
import FirebaseFirestore
import FirebaseFirestoreSwift
@dynamicMemberLookup struct Document<Value> {
typealias ID = Tagged<Document, String>
var value: Value
let id: ID
let snapshot: DocumentSnapshot
subscript<Subject>(dynamicMember keyPath: WritableKeyPath<Value, Subject>) -> Subject {
get { value[keyPath: keyPath] }
set { value[keyPath: keyPath] = newValue }
}
subscript<Subject>(dynamicMember keyPath: KeyPath<Value, Subject>) -> Subject {
value[keyPath: keyPath]
}
final class MockSnapshot: DocumentSnapshot {
init(mockThatShit: ()) { } // param is needed to prevent some clashes / subclassing issues but dont remember exactly
}
}
extension Document: Identifiable {
var identity: ID { return id }
}
extension Document: Equatable where Value: Equatable { }
extension Document: Hashable where Value: Hashable { }
extension Document where Value: Decodable {
init(from snapshot: DocumentSnapshot) throws {
guard let value = try snapshot.data(as: Value.self) else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "no data in snapshot"))
}
self.init(
value: value,
id: .init(rawValue: snapshot.documentID),
snapshot: snapshot
)
}
}
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
import Combine
extension PublishersNamespace where Base: DocumentReference {
func observe<T: Decodable>(type: T.Type = T.self, includeMetadataChanges: Bool = false) -> AnyPublisher<Document<T>, Error> {
var registration: ListenerRegistration?
let subject = PassthroughSubject<Document<T>, Error>()
return subject
.handleEvents(
receiveSubscription:
{ [base, weak subject] subscription in
registration = base.addSnapshotListener(includeMetadataChanges: includeMetadataChanges) { (snapshot, error) in
if let error = error {
subject?.send(completion: .failure(error))
return
}
do {
guard let document = try snapshot.map(Document<T>.init(from:)) else {
subject?.send(completion: .failure(DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "nil or empty snapshot"))))
return
}
subject?.send(document)
} catch {
subject?.send(completion: .failure(error))
}
}
},
receiveCancel: {
registration?.remove()
})
.eraseToAnyPublisher()
}
func write<T: Encodable>(_ value: T, merge: Bool = false) -> AnyPublisher<(), Error> {
Future { [base] promise in
do {
try base.setData(from: value, merge: merge) { error in
promise(error.map(Result.failure) ?? .success(()))
}
} catch {
promise(.failure(error))
}
}
.eraseToAnyPublisher()
}
func write<T: Encodable>(_ value: T, mergeFields: [String]) -> AnyPublisher<(), Error> {
Future { [base] promise in
do {
try base.setData(from: value, mergeFields: mergeFields) { error in
promise(error.map(Result.failure) ?? .success(()))
}
} catch {
promise(.failure(error))
}
}
.eraseToAnyPublisher()
}
}
extension PublishersNamespace where Base: Query {
private func observeSnapshot(includeMetadataChanges: Bool) -> AnyPublisher<QuerySnapshot, Error> {
var registration: ListenerRegistration?
let subject = PassthroughSubject<QuerySnapshot, Error>()
return subject
.handleEvents(
receiveSubscription:
{ [base, weak subject] subscription in
registration = base.addSnapshotListener(includeMetadataChanges: includeMetadataChanges) { (snapshot, error) in
if let error = error {
subject?.send(completion: .failure(error))
return
}
guard let snapshot = snapshot else {
subject?.send(completion: .failure(DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "no snapshot"))))
return
}
subject?.send(snapshot)
}
},
receiveCancel: {
registration?.remove()
})
.eraseToAnyPublisher()
}
func observeDocuments<T: Decodable>(type: T.Type = T.self, includeMetadataChanges: Bool = false) -> AnyPublisher<[Document<T>], Error> {
observeSnapshot(includeMetadataChanges: includeMetadataChanges)
.tryMap { snapshot in
try snapshot.documents.map(Document<T>.init(from:))
}
.eraseToAnyPublisher()
}
func observeFirstDocument<T: Decodable>(type: T.Type = T.self, includeMetadataChanges: Bool = false) -> AnyPublisher<Document<T>?, Error> {
observeSnapshot(includeMetadataChanges: includeMetadataChanges)
.tryMap { snapshot in
try snapshot.documents.first.map(Document<T>.init(from:))
}
.eraseToAnyPublisher()
}
}
public protocol PublishersProvider {}
extension PublishersProvider {
public var publishers: PublishersNamespace<Self> {
.init(self)
}
}
public struct PublishersNamespace<Base> {
public let base: Base
fileprivate init(_ base: Base) {
self.base = base
}
}
import Foundation
extension NSObject: PublishersProvider { }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment