Skip to content

Commit dc40c86

Browse files
authored
Adding AddReviewView for SwiftUI Firestore sample (#1137)
1 parent f19058e commit dc40c86

File tree

8 files changed

+238
-38
lines changed

8 files changed

+238
-38
lines changed

firestore/FirestoreExample.xcodeproj/project.pbxproj

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
8DF77E1A1EEB45E500CB2330 /* RestaurantDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DF77E191EEB45E500CB2330 /* RestaurantDetailViewController.swift */; };
2323
8DF77E1C1EEB645100CB2330 /* StarsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DF77E1B1EEB645100CB2330 /* StarsView.swift */; };
2424
8E1E43B125F961EF00BC64D3 /* Restaurant+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E1E43B025F961EF00BC64D3 /* Restaurant+Extension.swift */; };
25+
8E39DB9326153D25009CC10E /* WriteReviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E39DB9226153D25009CC10E /* WriteReviewView.swift */; };
2526
8E4C62D125E9CFE0001678A1 /* FirestoreSwiftUIExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E4C62D025E9CFE0001678A1 /* FirestoreSwiftUIExampleApp.swift */; };
2627
8E4C62D325E9CFE0001678A1 /* RestaurantListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E4C62D225E9CFE0001678A1 /* RestaurantListView.swift */; };
2728
8E4C62D525E9CFE1001678A1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8E4C62D425E9CFE1001678A1 /* Assets.xcassets */; };
@@ -32,6 +33,7 @@
3233
8E4C63B225F05E76001678A1 /* StarsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E4C63B125F05E76001678A1 /* StarsView.swift */; };
3334
8E4C63D825F6D641001678A1 /* Firestore+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E4C63D725F6D641001678A1 /* Firestore+Extension.swift */; };
3435
8E4C63E425F6D8C8001678A1 /* PriceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E4C63E325F6D8C8001678A1 /* PriceView.swift */; };
36+
8E722C0B261CD4210047AA1A /* RestaurantHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E722C0A261CD4210047AA1A /* RestaurantHeaderView.swift */; };
3537
8E919E71260D3945007C62C4 /* RestaurantViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E919E70260D3945007C62C4 /* RestaurantViewModel.swift */; };
3638
8E919E77260D394C007C62C4 /* Review.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E919E76260D394C007C62C4 /* Review.swift */; };
3739
8E919E7F260D3956007C62C4 /* RestaurantImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E919E7C260D3956007C62C4 /* RestaurantImageView.swift */; };
@@ -81,6 +83,7 @@
8183
8DF77E191EEB45E500CB2330 /* RestaurantDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestaurantDetailViewController.swift; sourceTree = "<group>"; };
8284
8DF77E1B1EEB645100CB2330 /* StarsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StarsView.swift; sourceTree = "<group>"; };
8385
8E1E43B025F961EF00BC64D3 /* Restaurant+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Restaurant+Extension.swift"; sourceTree = "<group>"; };
86+
8E39DB9226153D25009CC10E /* WriteReviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteReviewView.swift; sourceTree = "<group>"; };
8487
8E4C62CE25E9CFE0001678A1 /* FirestoreSwiftUIExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FirestoreSwiftUIExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
8588
8E4C62D025E9CFE0001678A1 /* FirestoreSwiftUIExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirestoreSwiftUIExampleApp.swift; sourceTree = "<group>"; };
8689
8E4C62D225E9CFE0001678A1 /* RestaurantListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestaurantListView.swift; sourceTree = "<group>"; };
@@ -93,6 +96,7 @@
9396
8E4C63B125F05E76001678A1 /* StarsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarsView.swift; sourceTree = "<group>"; };
9497
8E4C63D725F6D641001678A1 /* Firestore+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Firestore+Extension.swift"; sourceTree = "<group>"; };
9598
8E4C63E325F6D8C8001678A1 /* PriceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceView.swift; sourceTree = "<group>"; };
99+
8E722C0A261CD4210047AA1A /* RestaurantHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestaurantHeaderView.swift; sourceTree = "<group>"; };
96100
8E919E70260D3945007C62C4 /* RestaurantViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestaurantViewModel.swift; sourceTree = "<group>"; };
97101
8E919E76260D394C007C62C4 /* Review.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Review.swift; sourceTree = "<group>"; };
98102
8E919E7C260D3956007C62C4 /* RestaurantImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestaurantImageView.swift; sourceTree = "<group>"; };
@@ -238,12 +242,14 @@
238242
isa = PBXGroup;
239243
children = (
240244
8E4C62D225E9CFE0001678A1 /* RestaurantListView.swift */,
245+
8E4C62E625E9D191001678A1 /* RestaurantItemView.swift */,
241246
8E919E7D260D3956007C62C4 /* RestaurantDetailView.swift */,
247+
8E722C0A261CD4210047AA1A /* RestaurantHeaderView.swift */,
242248
8E919E7C260D3956007C62C4 /* RestaurantImageView.swift */,
243249
8E919E7E260D3956007C62C4 /* ReviewView.swift */,
244-
8E4C62E625E9D191001678A1 /* RestaurantItemView.swift */,
245250
8E4C63B125F05E76001678A1 /* StarsView.swift */,
246251
8E4C63E325F6D8C8001678A1 /* PriceView.swift */,
252+
8E39DB9226153D25009CC10E /* WriteReviewView.swift */,
247253
);
248254
path = Views;
249255
sourceTree = "<group>";
@@ -484,12 +490,14 @@
484490
files = (
485491
8E1E43B125F961EF00BC64D3 /* Restaurant+Extension.swift in Sources */,
486492
8E4C62D325E9CFE0001678A1 /* RestaurantListView.swift in Sources */,
493+
8E722C0B261CD4210047AA1A /* RestaurantHeaderView.swift in Sources */,
487494
8E4C634925EDB793001678A1 /* Restaurant.swift in Sources */,
488495
8E919E80260D3956007C62C4 /* RestaurantDetailView.swift in Sources */,
489496
8E4C637825EEF4CA001678A1 /* RestaurantListViewModel.swift in Sources */,
490497
8E4C62D125E9CFE0001678A1 /* FirestoreSwiftUIExampleApp.swift in Sources */,
491498
8E919E81260D3956007C62C4 /* ReviewView.swift in Sources */,
492499
8E4C63B225F05E76001678A1 /* StarsView.swift in Sources */,
500+
8E39DB9326153D25009CC10E /* WriteReviewView.swift in Sources */,
493501
8E919E77260D394C007C62C4 /* Review.swift in Sources */,
494502
8E919E7F260D3956007C62C4 /* RestaurantImageView.swift in Sources */,
495503
8E4C63E425F6D8C8001678A1 /* PriceView.swift in Sources */,

firestore/FirestoreSwiftUIExample/Extensions/Restaurant+Extension.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
import Firebase
2121

2222
extension Restaurant {
23+
var ratingsCollection: CollectionReference? {
24+
return reference?.collection("ratings")
25+
}
26+
2327
static let cities = [
2428
"Albuquerque",
2529
"Arlington",

firestore/FirestoreSwiftUIExample/ViewModels/RestaurantViewModel.swift

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,50 @@ class RestaurantViewModel: ObservableObject {
3535
unsubscribe()
3636
}
3737

38+
func add(review: Review) {
39+
db.runTransaction({ (transaction, errorPointer) -> Any? in
40+
let restaurantRef = self.restaurant.reference!
41+
let restaurantDocument: DocumentSnapshot
42+
do {
43+
try restaurantDocument = transaction.getDocument(restaurantRef)
44+
} catch let fetchError as NSError {
45+
errorPointer?.pointee = fetchError
46+
return nil
47+
}
48+
49+
guard let ratingCount = restaurantDocument.data()?["numRatings"] as? Int else {
50+
errorPointer?.pointee = self.getNSError(document: restaurantDocument)
51+
return nil
52+
}
53+
54+
guard let averageRating = restaurantDocument.data()?["avgRating"] as? Float else {
55+
errorPointer?.pointee = self.getNSError(document: restaurantDocument)
56+
return nil
57+
}
58+
59+
let newAverage = (Float(ratingCount) * averageRating + Float(review.rating))
60+
/ Float(ratingCount + 1)
61+
62+
transaction.setData([
63+
"numRatings": ratingCount + 1,
64+
"avgRating": newAverage
65+
], forDocument: restaurantRef, merge: true)
66+
67+
let reviewDocument = self.restaurant.ratingsCollection!.document()
68+
do {
69+
_ = try transaction.setData(from: review, forDocument: reviewDocument)
70+
} catch {
71+
fatalError("Unable to add review: \(error.localizedDescription).")
72+
}
73+
74+
return nil
75+
}) { (object, error) in
76+
if let error = error {
77+
print("Transaction failed: \(error)")
78+
}
79+
}
80+
}
81+
3882
func unsubscribe() {
3983
if listener != nil {
4084
listener?.remove()
@@ -44,8 +88,8 @@ class RestaurantViewModel: ObservableObject {
4488

4589
func subscribe() {
4690
if listener == nil {
47-
48-
listener = restaurant.reference!.collection("ratings").addSnapshotListener { [weak self] (querySnapshot, error) in
91+
listener = restaurant.ratingsCollection?.addSnapshotListener {
92+
[weak self] (querySnapshot, error) in
4993
guard let documents = querySnapshot?.documents else {
5094
print("Error fetching documents: \(error!)")
5195
return
@@ -60,9 +104,17 @@ class RestaurantViewModel: ObservableObject {
60104
return nil
61105
}
62106
}
63-
64-
print(self.reviews)
65107
}
66108
}
67109
}
110+
111+
func getNSError(document: DocumentSnapshot) -> NSError {
112+
return NSError(
113+
domain: "AppErrorDomain",
114+
code: -1,
115+
userInfo: [
116+
NSLocalizedDescriptionKey: "Unable to retrieve value from snapshot \(document)"
117+
]
118+
)
119+
}
68120
}

firestore/FirestoreSwiftUIExample/Views/RestaurantDetailView.swift

Lines changed: 13 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,48 +21,31 @@ import SwiftUI
2121
import SDWebImageSwiftUI
2222

2323
struct RestaurantDetailView: View {
24+
var restaurant: Restaurant
2425
@ObservedObject var viewModel: RestaurantViewModel
26+
@State var showAddReviewView = false
27+
28+
init(restaurant: Restaurant) {
29+
self.restaurant = restaurant
30+
viewModel = RestaurantViewModel(restaurant: restaurant)
31+
}
2532

2633
var body: some View {
2734
let restaurant = viewModel.restaurant
2835

2936
VStack {
30-
VStack {
31-
Spacer()
32-
.frame(height: 100)
33-
VStack(alignment: .leading) {
34-
HStack {
35-
Text(restaurant.name)
36-
.font(.title2)
37-
.bold()
38-
.frame(alignment: .leading)
39-
Spacer()
40-
PriceView(price: restaurant.price, color: Color.white)
41-
}
42-
StarsView(
43-
rating: Int(restaurant.averageRating.rounded()),
44-
color: Color.white,
45-
outlineColor: Color.white)
46-
HStack {
47-
Text(restaurant.category)
48-
Text("")
49-
Text(restaurant.city)
50-
}
51-
.font(.subheadline)
52-
}
53-
.padding()
54-
.foregroundColor(Color.white)
55-
.background(TransparentRectangleView())
56-
}
57-
.background(RestaurantImageView(imageURL: restaurant.photo, isThumbnail: false))
37+
RestaurantHeaderView(restaurant: restaurant)
5838
List(viewModel.reviews) { review in
5939
ReviewView(review: review)
6040
}
6141
}
42+
.sheet(isPresented: $showAddReviewView) {
43+
WriteReviewView(restaurant: restaurant, showAddReviewView: self.$showAddReviewView)
44+
}
6245
.navigationBarTitle(restaurant.name, displayMode: .inline)
6346
.toolbar {
6447
Button("Add") {
65-
print("add")
48+
self.showAddReviewView = true
6649
}
6750
}
6851
.onAppear() {
@@ -87,6 +70,6 @@ struct RestaurantDetailView_Previews: PreviewProvider {
8770
let restaurant = Restaurant(name: "Pizza Place", category: "Pizza", city: "Austin", price: 2,
8871
ratingCount: 1, averageRating: 4,
8972
photo: Restaurant.imageURL(forName: "Pizza Place"))
90-
RestaurantDetailView(viewModel: RestaurantViewModel(restaurant: restaurant))
73+
RestaurantDetailView(restaurant: restaurant)
9174
}
9275
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//
2+
// RestaurantHeaderView.swift
3+
// FirestoreSwiftUIExample
4+
//
5+
// Copyright (c) 2021 Google Inc.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
import SwiftUI
21+
22+
struct RestaurantHeaderView: View {
23+
var restaurant: Restaurant
24+
25+
var body: some View {
26+
VStack {
27+
Spacer()
28+
.frame(height: 100)
29+
VStack(alignment: .leading) {
30+
HStack {
31+
Text(restaurant.name)
32+
.font(.title2)
33+
.bold()
34+
.frame(alignment: .leading)
35+
Spacer()
36+
PriceView(price: restaurant.price, color: Color.white)
37+
}
38+
StarsView(
39+
rating: Int(restaurant.averageRating.rounded()),
40+
color: Color.white,
41+
outlineColor: Color.white)
42+
HStack {
43+
Text(restaurant.category)
44+
Text("")
45+
Text(restaurant.city)
46+
}
47+
.font(.subheadline)
48+
}
49+
.padding()
50+
.foregroundColor(Color.white)
51+
.background(TransparentRectangleView())
52+
}
53+
.background(RestaurantImageView(imageURL: restaurant.photo, isThumbnail: false))
54+
}
55+
}
56+
57+
struct RestaurantHeaderView_Previews: PreviewProvider {
58+
static var previews: some View {
59+
let restaurant = Restaurant(name: "Pizza Place", category: "Pizza", city: "Austin", price: 2,
60+
ratingCount: 1, averageRating: 4,
61+
photo: Restaurant.imageURL(forName: "Pizza Place"))
62+
RestaurantHeaderView(restaurant: restaurant)
63+
}
64+
}

firestore/FirestoreSwiftUIExample/Views/RestaurantItemView.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ struct RestaurantItemView: View {
2323
var restaurant: Restaurant
2424

2525
var body: some View {
26-
NavigationLink(
27-
destination: RestaurantDetailView(viewModel: RestaurantViewModel(restaurant: restaurant))) {
26+
NavigationLink(destination: RestaurantDetailView(restaurant: restaurant)) {
2827
HStack {
2928
RestaurantImageView(imageURL: restaurant.photo, isThumbnail: true)
3029
VStack(alignment: .leading) {

firestore/FirestoreSwiftUIExample/Views/StarsView.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ struct StarsView: View {
3333
}
3434
}
3535
}
36-
36+
3737
func getStar(num: Int, rating: Int) -> Image {
3838
num > rating ? Image(systemName: "star") : Image(systemName: "star.fill")
3939
}
@@ -43,8 +43,33 @@ struct StarsView: View {
4343
}
4444
}
4545

46+
struct StarsInputView: View {
47+
@EnvironmentObject var newReview: NewReview
48+
49+
var body: some View {
50+
HStack {
51+
ForEach(1...5, id: \.self) { i in
52+
getStar(num: i)
53+
.resizable()
54+
.frame(width: 50.0, height: 50.0)
55+
.foregroundColor(newReview.rating == 0 ? Color.gray : Color.yellow)
56+
.onTapGesture { newReview.rating = i }
57+
}
58+
}
59+
}
60+
61+
func getStar(num: Int) -> Image {
62+
if newReview.rating == 0 {
63+
return Image(systemName: "star")
64+
} else {
65+
return num > newReview.rating ? Image(systemName: "star") : Image(systemName: "star.fill")
66+
}
67+
}
68+
}
69+
4670
struct StarsView_Previews: PreviewProvider {
4771
static var previews: some View {
4872
StarsView(rating: 3, color: Color.purple, outlineColor: Color.black)
73+
StarsInputView()
4974
}
5075
}

0 commit comments

Comments
 (0)