Last active
February 6, 2021 13:52
-
-
Save pteasima/32e30fd6d5db6cc8e3974631a37ec286 to your computer and use it in GitHub Desktop.
Firebase + Combine extensions
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
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() | |
} | |
} |
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
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 | |
) | |
} | |
} |
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
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() | |
} | |
} |
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
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