Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/firestore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -514,9 +514,8 @@ jobs:
spm-binary:
uses: ./.github/workflows/common.yml
with:
target: FirebaseFirestore
target: FirebaseFirestoreTests
platforms: iOS
buildonly_platforms: iOS

check-firestore-internal-public-headers:
needs: check
Expand Down
5 changes: 5 additions & 0 deletions Firestore/Swift/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Unreleased
- [added] Added `AsyncSequence` support for `Query.snapshots` and
`DocumentReference.snapshots`, providing a modern, structured-concurrency
alternative to `addSnapshotListener`.

# 10.17.0
- [deprecated] All of the public API from `FirebaseFirestoreSwift` can now
be accessed through the `FirebaseFirestore` module. Therefore,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#if SWIFT_PACKAGE
@_exported import FirebaseFirestoreInternalWrapper
#else
@_exported import FirebaseFirestoreInternal
#endif // SWIFT_PACKAGE
import Foundation

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
public extension DocumentReference {
/// An asynchronous sequence of document snapshots.
///
/// This stream emits a new `DocumentSnapshot` every time the underlying data changes.
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
var snapshots: DocumentSnapshotsSequence {
return snapshots(includeMetadataChanges: false)
}

/// An asynchronous sequence of document snapshots.
///
/// - Parameter includeMetadataChanges: Whether to receive events for metadata-only changes.
/// - Returns: A `DocumentSnapshotsSequence` of `DocumentSnapshot` events.
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
func snapshots(includeMetadataChanges: Bool) -> DocumentSnapshotsSequence {
return DocumentSnapshotsSequence(self, includeMetadataChanges: includeMetadataChanges)
}

/// An `AsyncSequence` that emits `DocumentSnapshot` values whenever the document data changes.
///
/// This struct is the concrete type returned by the `DocumentReference.snapshots` property.
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
@frozen
struct DocumentSnapshotsSequence: AsyncSequence, Sendable {
public typealias Element = DocumentSnapshot
public typealias Failure = Error
public typealias AsyncIterator = Iterator

@usableFromInline
let documentReference: DocumentReference
@usableFromInline
let includeMetadataChanges: Bool

/// Creates a new sequence for monitoring document snapshots.
/// - Parameters:
/// - documentReference: The `DocumentReference` instance to monitor.
/// - includeMetadataChanges: Whether to receive events for metadata-only changes.
@inlinable
public init(_ documentReference: DocumentReference, includeMetadataChanges: Bool) {
self.documentReference = documentReference
self.includeMetadataChanges = includeMetadataChanges
}

/// Creates and returns an iterator for this asynchronous sequence.
/// - Returns: An `Iterator` for `DocumentSnapshotsSequence`.
@inlinable
public func makeAsyncIterator() -> Iterator {
Iterator(documentReference: documentReference, includeMetadataChanges: includeMetadataChanges)
}

/// The asynchronous iterator for `DocumentSnapshotsSequence`.
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
@frozen
public struct Iterator: AsyncIteratorProtocol {
public typealias Element = DocumentSnapshot
@usableFromInline
let stream: AsyncThrowingStream<DocumentSnapshot, Error>
@usableFromInline
var streamIterator: AsyncThrowingStream<DocumentSnapshot, Error>.Iterator

/// Initializes the iterator with the provided `DocumentReference` instance.
/// This sets up the `AsyncThrowingStream` and registers the necessary listener.
/// - Parameters:
/// - documentReference: The `DocumentReference` instance to monitor.
/// - includeMetadataChanges: Whether to receive events for metadata-only changes.
@inlinable
init(documentReference: DocumentReference, includeMetadataChanges: Bool) {
stream = AsyncThrowingStream { continuation in
let listener = documentReference
.addSnapshotListener(includeMetadataChanges: includeMetadataChanges) { snapshot, error in
if let error = error {
continuation.finish(throwing: error)
} else if let snapshot = snapshot {
continuation.yield(snapshot)
}
}

continuation.onTermination = { @Sendable _ in
listener.remove()
}
}
streamIterator = stream.makeAsyncIterator()
}

/// Produces the next element in the asynchronous sequence.
///
/// Returns a `DocumentSnapshot` value or `nil` if the sequence has terminated.
/// Throws an error if the underlying listener encounters an issue.
/// - Returns: An optional `DocumentSnapshot` object.
@inlinable
public mutating func next() async throws -> Element? {
try await streamIterator.next()
}
}
}
}

// Explicitly mark the Iterator as unavailable for Sendable conformance
@available(*, unavailable)
extension DocumentReference.DocumentSnapshotsSequence.Iterator: Sendable {}
126 changes: 126 additions & 0 deletions Firestore/Swift/Source/AsyncAwait/Query+AsyncSequence.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#if SWIFT_PACKAGE
@_exported import FirebaseFirestoreInternalWrapper
#else
@_exported import FirebaseFirestoreInternal
#endif // SWIFT_PACKAGE
import Foundation

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
public extension Query {
/// An asynchronous sequence of query snapshots.
///
/// This stream emits a new `QuerySnapshot` every time the underlying data changes.
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
var snapshots: QuerySnapshotsSequence {
return snapshots(includeMetadataChanges: false)
}

/// An asynchronous sequence of query snapshots.
///
/// - Parameter includeMetadataChanges: Whether to receive events for metadata-only changes.
/// - Returns: A `QuerySnapshotsSequence` of `QuerySnapshot` events.
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
func snapshots(includeMetadataChanges: Bool) -> QuerySnapshotsSequence {
return QuerySnapshotsSequence(self, includeMetadataChanges: includeMetadataChanges)
}

/// An `AsyncSequence` that emits `QuerySnapshot` values whenever the query data changes.
///
/// This struct is the concrete type returned by the `Query.snapshots` property.
///
/// - Important: This type is marked `Sendable` because `Query` itself is `Sendable`.
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
@frozen
struct QuerySnapshotsSequence: AsyncSequence, Sendable {
public typealias Element = QuerySnapshot
public typealias Failure = Error
public typealias AsyncIterator = Iterator

@usableFromInline
let query: Query
@usableFromInline
let includeMetadataChanges: Bool

/// Creates a new sequence for monitoring query snapshots.
/// - Parameters:
/// - query: The `Query` instance to monitor.
/// - includeMetadataChanges: Whether to receive events for metadata-only changes.
@inlinable
public init(_ query: Query, includeMetadataChanges: Bool) {
self.query = query
self.includeMetadataChanges = includeMetadataChanges
}

/// Creates and returns an iterator for this asynchronous sequence.
/// - Returns: An `Iterator` for `QuerySnapshotsSequence`.
@inlinable
public func makeAsyncIterator() -> Iterator {
Iterator(query: query, includeMetadataChanges: includeMetadataChanges)
}

/// The asynchronous iterator for `QuerySnapshotsSequence`.
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
@frozen
public struct Iterator: AsyncIteratorProtocol {
public typealias Element = QuerySnapshot
@usableFromInline
let stream: AsyncThrowingStream<QuerySnapshot, Error>
@usableFromInline
var streamIterator: AsyncThrowingStream<QuerySnapshot, Error>.Iterator

/// Initializes the iterator with the provided `Query` instance.
/// This sets up the `AsyncThrowingStream` and registers the necessary listener.
/// - Parameters:
/// - query: The `Query` instance to monitor.
/// - includeMetadataChanges: Whether to receive events for metadata-only changes.
@inlinable
init(query: Query, includeMetadataChanges: Bool) {
stream = AsyncThrowingStream { continuation in
let listener = query
.addSnapshotListener(includeMetadataChanges: includeMetadataChanges) { snapshot, error in
if let error = error {
continuation.finish(throwing: error)
} else if let snapshot = snapshot {
continuation.yield(snapshot)
}
}

continuation.onTermination = { @Sendable _ in
listener.remove()
}
}
streamIterator = stream.makeAsyncIterator()
}

/// Produces the next element in the asynchronous sequence.
///
/// Returns a `QuerySnapshot` value or `nil` if the sequence has terminated.
/// Throws an error if the underlying listener encounters an issue.
/// - Returns: An optional `QuerySnapshot` object.
@inlinable
public mutating func next() async throws -> Element? {
try await streamIterator.next()
}
}
}
}

// Explicitly mark the Iterator as unavailable for Sendable conformance
@available(*, unavailable)
extension Query.QuerySnapshotsSequence.Iterator: Sendable {}
Loading
Loading