Skip to content

Commit f9781a0

Browse files
authored
Merge c6f5f9c into cc3ed01
2 parents cc3ed01 + c6f5f9c commit f9781a0

File tree

7 files changed

+322
-4
lines changed

7 files changed

+322
-4
lines changed

Example/FirestoreSample/FirestoreSample.xcodeproj/project.pbxproj

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,18 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
880B696E2A581B71005F7798 /* FavouriteFruitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880B696D2A581B71005F7798 /* FavouriteFruitView.swift */; };
11+
880B69702A5829D2005F7798 /* FavouriteFruitsAsyncSequenceViewModelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880B696F2A5829D2005F7798 /* FavouriteFruitsAsyncSequenceViewModelView.swift */; };
1012
8817084726B95A63009E9281 /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 8817084626B95A63009E9281 /* FirebaseFirestore */; };
1113
8817084926B95A63009E9281 /* FirebaseFirestoreSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 8817084826B95A63009E9281 /* FirebaseFirestoreSwift */; };
1214
88327B8826D62908002AA6D9 /* FavouriteFruitsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88327B8726D62908002AA6D9 /* FavouriteFruitsView.swift */; };
1315
8844BA6126E0DD3F000786F0 /* FavouriteFruitsMappingErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8844BA6026E0DD3F000786F0 /* FavouriteFruitsMappingErrorView.swift */; };
16+
887FEFE32A5574EB00FB2756 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 887FEFE22A5574EA00FB2756 /* GoogleService-Info.plist */; };
17+
887FEFE52A5576FA00FB2756 /* FavouriteFruitsAsyncSequenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887FEFE42A5576FA00FB2756 /* FavouriteFruitsAsyncSequenceView.swift */; };
1418
88D5E37826EBD2F200808AFF /* FavouriteFruitsMappingErrorView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D5E37726EBD2F200808AFF /* FavouriteFruitsMappingErrorView2.swift */; };
1519
88D9354A2A39CB3E00FD8AFF /* FavouriteFruitsAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D935492A39CB3E00FD8AFF /* FavouriteFruitsAnimationView.swift */; };
1620
88D9354C2A39D72300FD8AFF /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 88D9354B2A39D72300FD8AFF /* FirebaseAuth */; };
1721
88E5A79A2A39DDE400462B64 /* FavouriteFruitsNoAnimationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E5A7992A39DDE400462B64 /* FavouriteFruitsNoAnimationsView.swift */; };
18-
88E5A79C2A39E11A00462B64 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 88E5A79B2A39E11A00462B64 /* GoogleService-Info.plist */; };
1922
88FBD98826B9485F00982BF2 /* FirestoreSampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FBD98726B9485F00982BF2 /* FirestoreSampleApp.swift */; };
2023
88FBD98C26B9486100982BF2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88FBD98B26B9486100982BF2 /* Assets.xcassets */; };
2124
88FBD98F26B9486100982BF2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88FBD98E26B9486100982BF2 /* Preview Assets.xcassets */; };
@@ -24,12 +27,15 @@
2427

2528
/* Begin PBXFileReference section */
2629
8809D0AE26B9520B00DE7864 /* firebase-ios-sdk */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "firebase-ios-sdk"; path = ../..; sourceTree = "<group>"; };
30+
880B696D2A581B71005F7798 /* FavouriteFruitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavouriteFruitView.swift; sourceTree = "<group>"; };
31+
880B696F2A5829D2005F7798 /* FavouriteFruitsAsyncSequenceViewModelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavouriteFruitsAsyncSequenceViewModelView.swift; sourceTree = "<group>"; };
2732
88327B8726D62908002AA6D9 /* FavouriteFruitsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavouriteFruitsView.swift; sourceTree = "<group>"; };
2833
8844BA6026E0DD3F000786F0 /* FavouriteFruitsMappingErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavouriteFruitsMappingErrorView.swift; sourceTree = "<group>"; };
34+
887FEFE22A5574EA00FB2756 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
35+
887FEFE42A5576FA00FB2756 /* FavouriteFruitsAsyncSequenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavouriteFruitsAsyncSequenceView.swift; sourceTree = "<group>"; };
2936
88D5E37726EBD2F200808AFF /* FavouriteFruitsMappingErrorView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavouriteFruitsMappingErrorView2.swift; sourceTree = "<group>"; };
3037
88D935492A39CB3E00FD8AFF /* FavouriteFruitsAnimationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavouriteFruitsAnimationView.swift; sourceTree = "<group>"; };
3138
88E5A7992A39DDE400462B64 /* FavouriteFruitsNoAnimationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavouriteFruitsNoAnimationsView.swift; sourceTree = "<group>"; };
32-
88E5A79B2A39E11A00462B64 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
3339
88FBD98426B9485F00982BF2 /* FirestoreSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FirestoreSample.app; sourceTree = BUILT_PRODUCTS_DIR; };
3440
88FBD98726B9485F00982BF2 /* FirestoreSampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirestoreSampleApp.swift; sourceTree = "<group>"; };
3541
88FBD98B26B9486100982BF2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -90,7 +96,7 @@
9096
88FBD99526B948A100982BF2 /* App */,
9197
88FBD99626B948A900982BF2 /* Views */,
9298
88FBD98B26B9486100982BF2 /* Assets.xcassets */,
93-
88E5A79B2A39E11A00462B64 /* GoogleService-Info.plist */,
99+
887FEFE22A5574EA00FB2756 /* GoogleService-Info.plist */,
94100
88FBD98D26B9486100982BF2 /* Preview Content */,
95101
);
96102
path = FirestoreSample;
@@ -120,6 +126,9 @@
120126
88D5E37726EBD2F200808AFF /* FavouriteFruitsMappingErrorView2.swift */,
121127
88E5A7992A39DDE400462B64 /* FavouriteFruitsNoAnimationsView.swift */,
122128
88D935492A39CB3E00FD8AFF /* FavouriteFruitsAnimationView.swift */,
129+
880B696F2A5829D2005F7798 /* FavouriteFruitsAsyncSequenceViewModelView.swift */,
130+
887FEFE42A5576FA00FB2756 /* FavouriteFruitsAsyncSequenceView.swift */,
131+
880B696D2A581B71005F7798 /* FavouriteFruitView.swift */,
123132
88FBD99726B948E200982BF2 /* MenuView.swift */,
124133
);
125134
path = Views;
@@ -188,8 +197,8 @@
188197
isa = PBXResourcesBuildPhase;
189198
buildActionMask = 2147483647;
190199
files = (
191-
88E5A79C2A39E11A00462B64 /* GoogleService-Info.plist in Resources */,
192200
88FBD98F26B9486100982BF2 /* Preview Assets.xcassets in Resources */,
201+
887FEFE32A5574EB00FB2756 /* GoogleService-Info.plist in Resources */,
193202
88FBD98C26B9486100982BF2 /* Assets.xcassets in Resources */,
194203
);
195204
runOnlyForDeploymentPostprocessing = 0;
@@ -203,9 +212,12 @@
203212
files = (
204213
88FBD99826B948E200982BF2 /* MenuView.swift in Sources */,
205214
88E5A79A2A39DDE400462B64 /* FavouriteFruitsNoAnimationsView.swift in Sources */,
215+
880B69702A5829D2005F7798 /* FavouriteFruitsAsyncSequenceViewModelView.swift in Sources */,
206216
88327B8826D62908002AA6D9 /* FavouriteFruitsView.swift in Sources */,
207217
88D9354A2A39CB3E00FD8AFF /* FavouriteFruitsAnimationView.swift in Sources */,
218+
880B696E2A581B71005F7798 /* FavouriteFruitView.swift in Sources */,
208219
88FBD98826B9485F00982BF2 /* FirestoreSampleApp.swift in Sources */,
220+
887FEFE52A5576FA00FB2756 /* FavouriteFruitsAsyncSequenceView.swift in Sources */,
209221
88D5E37826EBD2F200808AFF /* FavouriteFruitsMappingErrorView2.swift in Sources */,
210222
8844BA6126E0DD3F000786F0 /* FavouriteFruitsMappingErrorView.swift in Sources */,
211223
);
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import SwiftUI
18+
import FirebaseFirestore
19+
import FirebaseFirestoreSwift
20+
21+
private struct Fruit: Codable, Identifiable, Equatable {
22+
@DocumentID var id: String?
23+
var name: String
24+
var isFavourite: Bool
25+
}
26+
27+
extension Fruit {
28+
static let empty = Fruit(name: "", isFavourite: false)
29+
}
30+
31+
struct FavouriteFruitView: View {
32+
@State fileprivate var fruit: Fruit = .empty
33+
34+
var body: some View {
35+
VStack {
36+
Text(fruit.name)
37+
Toggle(isOn: $fruit.isFavourite, label: {
38+
Text("Is favourite")
39+
})
40+
}
41+
.task {
42+
do {
43+
let doc = Firestore.firestore().collection("fruits").document("1uxSAlCvYWmfrXN7rn2s")
44+
for try await fruit in doc.snapshotSequence(Fruit.self) {
45+
self.fruit = fruit
46+
}
47+
} catch {
48+
print(error)
49+
}
50+
}
51+
.navigationTitle("Fruit")
52+
}
53+
}
54+
55+
struct FavouriteFruitView_Previews: PreviewProvider {
56+
static var previews: some View {
57+
FavouriteFruitView()
58+
}
59+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import SwiftUI
18+
import FirebaseFirestore
19+
import FirebaseFirestoreSwift
20+
21+
private struct Fruit: Codable, Identifiable, Equatable {
22+
@DocumentID var id: String?
23+
var name: String
24+
var isFavourite: Bool
25+
}
26+
27+
struct FavouriteFruitsAsyncSequenceView: View {
28+
@State fileprivate var fruits: [Fruit] = []
29+
30+
@State var showOnlyFavourites = true
31+
32+
var body: some View {
33+
List(fruits) { fruit in
34+
Text(fruit.name)
35+
}
36+
.task {
37+
do {
38+
let collection = Firestore.firestore().collection("fruits")
39+
for try await fruits in collection.snapshotSequence(Fruit.self) {
40+
self.fruits = fruits
41+
}
42+
} catch {
43+
print(error)
44+
}
45+
}
46+
.animation(.default, value: fruits)
47+
.navigationTitle("Fruits")
48+
.toolbar {
49+
ToolbarItem(placement: .navigationBarTrailing) {
50+
Button(action: toggleFilter) {
51+
Image(systemName: showOnlyFavourites
52+
? "line.3.horizontal.decrease.circle.fill"
53+
: "line.3.horizontal.decrease.circle")
54+
}
55+
}
56+
}
57+
}
58+
59+
func toggleFilter() {
60+
showOnlyFavourites.toggle()
61+
}
62+
}
63+
64+
struct FavouriteFruitsAsyncSequenceView_Previews: PreviewProvider {
65+
static var previews: some View {
66+
FavouriteFruitsAsyncSequenceView()
67+
}
68+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import SwiftUI
18+
import FirebaseFirestore
19+
import FirebaseFirestoreSwift
20+
21+
private struct Fruit: Codable, Identifiable, Equatable {
22+
@DocumentID var id: String?
23+
var name: String
24+
var isFavourite: Bool
25+
}
26+
27+
class ViewModel: ObservableObject {
28+
@Published fileprivate var fruits = [Fruit]()
29+
30+
private var firestore = Firestore.firestore()
31+
32+
@MainActor
33+
func subscribe() async {
34+
let collection = Firestore.firestore().collection("fruits")
35+
do {
36+
for try await fruits in collection.snapshotSequence(Fruit.self) {
37+
self.fruits = fruits
38+
}
39+
} catch {
40+
print(error)
41+
}
42+
}
43+
}
44+
45+
struct FavouriteFruitsAsyncSequenceViewModelView: View {
46+
@StateObject var viewModel = ViewModel()
47+
48+
@State var showOnlyFavourites = true
49+
50+
var body: some View {
51+
List(viewModel.fruits) { fruit in
52+
Text(fruit.name)
53+
}
54+
.task {
55+
await viewModel.subscribe()
56+
}
57+
.animation(.default, value: viewModel.fruits)
58+
.navigationTitle("Fruits")
59+
.toolbar {
60+
ToolbarItem(placement: .navigationBarTrailing) {
61+
Button(action: toggleFilter) {
62+
Image(systemName: showOnlyFavourites
63+
? "line.3.horizontal.decrease.circle.fill"
64+
: "line.3.horizontal.decrease.circle")
65+
}
66+
}
67+
}
68+
}
69+
70+
func toggleFilter() {
71+
showOnlyFavourites.toggle()
72+
}
73+
}
74+
75+
struct FavouriteFruitsAsyncSequenceViewModelView_Previews: PreviewProvider {
76+
static var previews: some View {
77+
FavouriteFruitsAsyncSequenceViewModelView()
78+
}
79+
}

Example/FirestoreSample/FirestoreSample/Views/MenuView.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ struct MenuView: View {
3636
Label("With Animations", systemImage: "shippingbox")
3737
}
3838
}
39+
Section(header: Text("Async")) {
40+
NavigationLink(destination: FavouriteFruitsAsyncSequenceView()) {
41+
Label("AsyncSequence (Collection)", systemImage: "shippingbox")
42+
}
43+
NavigationLink(destination: FavouriteFruitsAsyncSequenceViewModelView()) {
44+
Label("AsyncSequence (Collection) / view model", systemImage: "shippingbox")
45+
}
46+
NavigationLink(destination: FavouriteFruitView()) {
47+
Label("AsyncSequence (Document)", systemImage: "shippingbox")
48+
}
49+
}
3950
}
4051
.listStyle(InsetGroupedListStyle())
4152
.navigationTitle("Firestore")
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import FirebaseFirestore
18+
import Foundation
19+
20+
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
21+
public extension DocumentReference {
22+
func snapshotSequence<T>(_ type: T.Type,
23+
includeMetadataChanges: Bool = false)
24+
-> AsyncThrowingStream<T, Error> where T: Decodable {
25+
.init { continuation in
26+
let listener =
27+
addSnapshotListener(includeMetadataChanges: includeMetadataChanges) { documentSnapshot, error in
28+
do {
29+
if let result = try documentSnapshot?.data(as: T.self) {
30+
continuation.yield(result)
31+
}
32+
} catch {
33+
continuation.finish(throwing: error)
34+
}
35+
}
36+
37+
continuation.onTermination = { @Sendable _ in
38+
listener.remove()
39+
}
40+
}
41+
}
42+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import FirebaseFirestore
18+
import Foundation
19+
20+
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
21+
public extension Query {
22+
func snapshotSequence<T>(_ type: T.Type,
23+
includeMetadataChanges: Bool = false) -> AsyncThrowingStream<
24+
[T],
25+
Error
26+
> where T: Decodable {
27+
.init { continuation in
28+
let listener =
29+
addSnapshotListener(includeMetadataChanges: includeMetadataChanges) { querySnapshot, error in
30+
do {
31+
guard let documents = querySnapshot?.documents else {
32+
continuation.yield([])
33+
return
34+
}
35+
let results = try documents.map { try $0.data(as: T.self) }
36+
continuation.yield(results)
37+
} catch {
38+
continuation.finish(throwing: error)
39+
}
40+
}
41+
42+
continuation.onTermination = { @Sendable _ in
43+
listener.remove()
44+
}
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)