From e9122e1cf5e583e05eded235b3bddbecbee5ba86 Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Mon, 13 Apr 2026 15:23:41 -0700 Subject: [PATCH 1/7] pre-Gemini wip --- .../FriendlyMeals.xcodeproj/project.pbxproj | 92 +++++++++---------- .../ViewModels/MealPlannerChatViewModel.swift | 2 +- .../MealPlannerSuggestionViewModel.swift | 2 +- .../Views/MealPlannerChatView.swift | 2 +- .../ViewModels/NutritionViewModel.swift | 2 +- .../FriendlyMeals/Shared/Models/Recipe.swift | 38 ++++++++ .../Services/GenerationConfig+Decodable.swift | 2 +- .../Shared/Services/RecipeStore.swift | 69 +++++++------- .../Shared/Services/RemoteConfigService.swift | 2 +- 9 files changed, 116 insertions(+), 95 deletions(-) diff --git a/FriendlyMeals/FriendlyMeals.xcodeproj/project.pbxproj b/FriendlyMeals/FriendlyMeals.xcodeproj/project.pbxproj index 17006eb..bf33cc0 100644 --- a/FriendlyMeals/FriendlyMeals.xcodeproj/project.pbxproj +++ b/FriendlyMeals/FriendlyMeals.xcodeproj/project.pbxproj @@ -8,13 +8,13 @@ /* Begin PBXBuildFile section */ 884B2CF22E144BA7007B2E0E /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 884B2CF12E144BA7007B2E0E /* MarkdownUI */; }; - 884ECEF72E8ABF9400E3B0CB /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 884ECEF62E8ABF9400E3B0CB /* FirebaseFirestore */; }; 887565682E86CCEE0041DDEF /* ConversationKit in Frameworks */ = {isa = PBXBuildFile; productRef = 887565672E86CCEE0041DDEF /* ConversationKit */; }; - 887565922E86D6D80041DDEF /* FirebaseRemoteConfig in Frameworks */ = {isa = PBXBuildFile; productRef = 887565912E86D6D80041DDEF /* FirebaseRemoteConfig */; }; - 88BE941E2E1413D000C81FF5 /* FirebaseAI in Frameworks */ = {isa = PBXBuildFile; productRef = 88BE941D2E1413D000C81FF5 /* FirebaseAI */; }; - 88C8FB7A2E13DF62001B86C4 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = 88C8FB792E13DF62001B86C4 /* FirebaseCore */; }; - 8DAB5BA32F04A0FE000D464B /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 8DAB5BA22F04A0FE000D464B /* FirebaseAuth */; }; - 8DAB5BA82F107BA3000D464B /* FirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 8DAB5BA72F107BA3000D464B /* FirebaseStorage */; }; + 8D772B902F8039530054270D /* FoundationModels.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8D772B8F2F8039530054270D /* FoundationModels.framework */; }; + 8D772B932F803C350054270D /* FirebaseAILogic in Frameworks */ = {isa = PBXBuildFile; productRef = 8D772B922F803C350054270D /* FirebaseAILogic */; }; + 8D772B952F803C350054270D /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 8D772B942F803C350054270D /* FirebaseAuth */; }; + 8D772B972F803C350054270D /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 8D772B962F803C350054270D /* FirebaseFirestore */; }; + 8D772B992F803C350054270D /* FirebaseRemoteConfig in Frameworks */ = {isa = PBXBuildFile; productRef = 8D772B982F803C350054270D /* FirebaseRemoteConfig */; }; + 8D772B9B2F803C350054270D /* FirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 8D772B9A2F803C350054270D /* FirebaseStorage */; }; 8DBC83FA2F17076D00DAC906 /* ConversationKit in Frameworks */ = {isa = PBXBuildFile; productRef = 88665A882E1D2DB00039990B /* ConversationKit */; }; 8DBC83FB2F17076D00DAC906 /* ConversationKit in Frameworks */ = {isa = PBXBuildFile; productRef = 883F6FA82E86969900519CE3 /* ConversationKit */; }; 8DBC83FC2F17076D00DAC906 /* ConversationKit in Frameworks */ = {isa = PBXBuildFile; productRef = 88C207D72E8AA5C6004CD918 /* ConversationKit */; }; @@ -23,6 +23,7 @@ /* Begin PBXFileReference section */ 88C8FB652E13D879001B86C4 /* FriendlyMeals.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FriendlyMeals.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8D772B8F2F8039530054270D /* FoundationModels.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FoundationModels.framework; path = System/Library/Frameworks/FoundationModels.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -38,18 +39,18 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 8D772B992F803C350054270D /* FirebaseRemoteConfig in Frameworks */, + 8D772B932F803C350054270D /* FirebaseAILogic in Frameworks */, 8DBC83FD2F17076D00DAC906 /* ConversationKit in Frameworks */, 8DBC83FC2F17076D00DAC906 /* ConversationKit in Frameworks */, + 8D772B972F803C350054270D /* FirebaseFirestore in Frameworks */, + 8D772B9B2F803C350054270D /* FirebaseStorage in Frameworks */, 8DBC83FB2F17076D00DAC906 /* ConversationKit in Frameworks */, + 8D772B952F803C350054270D /* FirebaseAuth in Frameworks */, 8DBC83FA2F17076D00DAC906 /* ConversationKit in Frameworks */, 887565682E86CCEE0041DDEF /* ConversationKit in Frameworks */, 884B2CF22E144BA7007B2E0E /* MarkdownUI in Frameworks */, - 88C8FB7A2E13DF62001B86C4 /* FirebaseCore in Frameworks */, - 887565922E86D6D80041DDEF /* FirebaseRemoteConfig in Frameworks */, - 884ECEF72E8ABF9400E3B0CB /* FirebaseFirestore in Frameworks */, - 8DAB5BA82F107BA3000D464B /* FirebaseStorage in Frameworks */, - 8DAB5BA32F04A0FE000D464B /* FirebaseAuth in Frameworks */, - 88BE941E2E1413D000C81FF5 /* FirebaseAI in Frameworks */, + 8D772B902F8039530054270D /* FoundationModels.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -76,6 +77,7 @@ 88C8FB782E13DF62001B86C4 /* Frameworks */ = { isa = PBXGroup; children = ( + 8D772B8F2F8039530054270D /* FoundationModels.framework */, ); name = Frameworks; sourceTree = ""; @@ -100,18 +102,17 @@ ); name = FriendlyMeals; packageProductDependencies = ( - 88C8FB792E13DF62001B86C4 /* FirebaseCore */, - 88BE941D2E1413D000C81FF5 /* FirebaseAI */, 884B2CF12E144BA7007B2E0E /* MarkdownUI */, 88665A882E1D2DB00039990B /* ConversationKit */, 883F6FA82E86969900519CE3 /* ConversationKit */, 887565672E86CCEE0041DDEF /* ConversationKit */, - 887565912E86D6D80041DDEF /* FirebaseRemoteConfig */, 88C207D72E8AA5C6004CD918 /* ConversationKit */, - 884ECEF62E8ABF9400E3B0CB /* FirebaseFirestore */, 88FF5D532ECF579900646871 /* ConversationKit */, - 8DAB5BA22F04A0FE000D464B /* FirebaseAuth */, - 8DAB5BA72F107BA3000D464B /* FirebaseStorage */, + 8D772B922F803C350054270D /* FirebaseAILogic */, + 8D772B942F803C350054270D /* FirebaseAuth */, + 8D772B962F803C350054270D /* FirebaseFirestore */, + 8D772B982F803C350054270D /* FirebaseRemoteConfig */, + 8D772B9A2F803C350054270D /* FirebaseStorage */, ); productName = FriendlyMeals; productReference = 88C8FB652E13D879001B86C4 /* FriendlyMeals.app */; @@ -142,9 +143,9 @@ mainGroup = 88C8FB5C2E13D879001B86C4; minimizedProjectReferenceProxies = 1; packageReferences = ( - 88C8FB772E13DEE1001B86C4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, 884B2CF02E144BA7007B2E0E /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, 88FF5D522ECF579900646871 /* XCRemoteSwiftPackageReference "ConversationKit" */, + 8D772B912F803C350054270D /* XCLocalSwiftPackageReference "../../firebase-ios-sdk" */, ); preferredProjectObjectVersion = 77; productRefGroup = 88C8FB662E13D879001B86C4 /* Products */; @@ -385,6 +386,13 @@ }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 8D772B912F803C350054270D /* XCLocalSwiftPackageReference "../../firebase-ios-sdk" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../../firebase-ios-sdk"; + }; +/* End XCLocalSwiftPackageReference section */ + /* Begin XCRemoteSwiftPackageReference section */ 884B2CF02E144BA7007B2E0E /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = { isa = XCRemoteSwiftPackageReference; @@ -394,14 +402,6 @@ minimumVersion = 2.4.1; }; }; - 88C8FB772E13DEE1001B86C4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/firebase/firebase-ios-sdk"; - requirement = { - kind = revision; - revision = 1f7389bad09aa93c9eeb508e70013207813a4df2; - }; - }; 88FF5D522ECF579900646871 /* XCRemoteSwiftPackageReference "ConversationKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/peterfriese/ConversationKit"; @@ -422,11 +422,6 @@ package = 884B2CF02E144BA7007B2E0E /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; productName = MarkdownUI; }; - 884ECEF62E8ABF9400E3B0CB /* FirebaseFirestore */ = { - isa = XCSwiftPackageProductDependency; - package = 88C8FB772E13DEE1001B86C4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; - productName = FirebaseFirestore; - }; 88665A882E1D2DB00039990B /* ConversationKit */ = { isa = XCSwiftPackageProductDependency; productName = ConversationKit; @@ -435,38 +430,33 @@ isa = XCSwiftPackageProductDependency; productName = ConversationKit; }; - 887565912E86D6D80041DDEF /* FirebaseRemoteConfig */ = { - isa = XCSwiftPackageProductDependency; - package = 88C8FB772E13DEE1001B86C4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; - productName = FirebaseRemoteConfig; - }; - 88BE941D2E1413D000C81FF5 /* FirebaseAI */ = { - isa = XCSwiftPackageProductDependency; - package = 88C8FB772E13DEE1001B86C4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; - productName = FirebaseAI; - }; 88C207D72E8AA5C6004CD918 /* ConversationKit */ = { isa = XCSwiftPackageProductDependency; productName = ConversationKit; }; - 88C8FB792E13DF62001B86C4 /* FirebaseCore */ = { - isa = XCSwiftPackageProductDependency; - package = 88C8FB772E13DEE1001B86C4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; - productName = FirebaseCore; - }; 88FF5D532ECF579900646871 /* ConversationKit */ = { isa = XCSwiftPackageProductDependency; package = 88FF5D522ECF579900646871 /* XCRemoteSwiftPackageReference "ConversationKit" */; productName = ConversationKit; }; - 8DAB5BA22F04A0FE000D464B /* FirebaseAuth */ = { + 8D772B922F803C350054270D /* FirebaseAILogic */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseAILogic; + }; + 8D772B942F803C350054270D /* FirebaseAuth */ = { isa = XCSwiftPackageProductDependency; - package = 88C8FB772E13DEE1001B86C4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; productName = FirebaseAuth; }; - 8DAB5BA72F107BA3000D464B /* FirebaseStorage */ = { + 8D772B962F803C350054270D /* FirebaseFirestore */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseFirestore; + }; + 8D772B982F803C350054270D /* FirebaseRemoteConfig */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseRemoteConfig; + }; + 8D772B9A2F803C350054270D /* FirebaseStorage */ = { isa = XCSwiftPackageProductDependency; - package = 88C8FB772E13DEE1001B86C4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; productName = FirebaseStorage; }; /* End XCSwiftPackageProductDependency section */ diff --git a/FriendlyMeals/FriendlyMeals/Features/MealPlanner/ViewModels/MealPlannerChatViewModel.swift b/FriendlyMeals/FriendlyMeals/Features/MealPlanner/ViewModels/MealPlannerChatViewModel.swift index 85ab877..9cc00ed 100644 --- a/FriendlyMeals/FriendlyMeals/Features/MealPlanner/ViewModels/MealPlannerChatViewModel.swift +++ b/FriendlyMeals/FriendlyMeals/Features/MealPlanner/ViewModels/MealPlannerChatViewModel.swift @@ -16,7 +16,7 @@ // limitations under the License. import ConversationKit -import FirebaseAI +import FirebaseAILogic import SwiftUI @Observable diff --git a/FriendlyMeals/FriendlyMeals/Features/MealPlanner/ViewModels/MealPlannerSuggestionViewModel.swift b/FriendlyMeals/FriendlyMeals/Features/MealPlanner/ViewModels/MealPlannerSuggestionViewModel.swift index 39b6537..1235e41 100644 --- a/FriendlyMeals/FriendlyMeals/Features/MealPlanner/ViewModels/MealPlannerSuggestionViewModel.swift +++ b/FriendlyMeals/FriendlyMeals/Features/MealPlanner/ViewModels/MealPlannerSuggestionViewModel.swift @@ -15,7 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import FirebaseAI +import FirebaseAILogic import FirebaseAuth import FirebaseRemoteConfig import SwiftUI diff --git a/FriendlyMeals/FriendlyMeals/Features/MealPlanner/Views/MealPlannerChatView.swift b/FriendlyMeals/FriendlyMeals/Features/MealPlanner/Views/MealPlannerChatView.swift index 5c9163d..352ce25 100644 --- a/FriendlyMeals/FriendlyMeals/Features/MealPlanner/Views/MealPlannerChatView.swift +++ b/FriendlyMeals/FriendlyMeals/Features/MealPlanner/Views/MealPlannerChatView.swift @@ -17,7 +17,7 @@ import SwiftUI import ConversationKit -import FirebaseAI +import FirebaseAILogic struct MealPlannerChatView: View { @State private var viewModel = MealPlannerChatViewModel() diff --git a/FriendlyMeals/FriendlyMeals/Features/Nutrition/ViewModels/NutritionViewModel.swift b/FriendlyMeals/FriendlyMeals/Features/Nutrition/ViewModels/NutritionViewModel.swift index 6edf7a4..dfc2df9 100644 --- a/FriendlyMeals/FriendlyMeals/Features/Nutrition/ViewModels/NutritionViewModel.swift +++ b/FriendlyMeals/FriendlyMeals/Features/Nutrition/ViewModels/NutritionViewModel.swift @@ -15,7 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import FirebaseAI +import FirebaseAILogic import Foundation import SwiftUI diff --git a/FriendlyMeals/FriendlyMeals/Shared/Models/Recipe.swift b/FriendlyMeals/FriendlyMeals/Shared/Models/Recipe.swift index 95017d1..4c34500 100644 --- a/FriendlyMeals/FriendlyMeals/Shared/Models/Recipe.swift +++ b/FriendlyMeals/FriendlyMeals/Shared/Models/Recipe.swift @@ -55,4 +55,42 @@ extension Recipe { servings: representable.servings ) } + + init(from result: PipelineResult) throws { + let imageURL = result.data["imageUri"] as? String + guard let title = result.data["title"] as? String, + let instructions = result.data["instructions"] as? String, + let ingredients = result.data["ingredients"] as? [String], + let authorID = result.data["authorId"] as? String, + let tags = result.data["tags"] as? [String], + let averageRating = result.data["averageRating"] as? Double, + let prepTime = result.data["prepTime"] as? String, + let cookTime = result.data["cookTime"] as? String, + let servings = result.data["servings"] as? String, + let documentID = result.id else { + let errorMessage = "Unable to initialize recipes from data: \(result.data)" + throw DecodingError.missingKeys(errorMessage) + } + + print("rating: \(result.data["averageRating"])") + + self.init( + title: title, + instructions: instructions, + ingredients: ingredients, + authorId: authorID, + tags: tags, + averageRating: averageRating, + imageUri: imageURL, + prepTime: prepTime, + cookTime: cookTime, + servings: servings + ) + + self.id = documentID + } + + enum DecodingError: Error { + case missingKeys(String) + } } diff --git a/FriendlyMeals/FriendlyMeals/Shared/Services/GenerationConfig+Decodable.swift b/FriendlyMeals/FriendlyMeals/Shared/Services/GenerationConfig+Decodable.swift index a715707..3d3d504 100644 --- a/FriendlyMeals/FriendlyMeals/Shared/Services/GenerationConfig+Decodable.swift +++ b/FriendlyMeals/FriendlyMeals/Shared/Services/GenerationConfig+Decodable.swift @@ -16,7 +16,7 @@ // limitations under the License. import Foundation -import FirebaseAI +import FirebaseAILogic extension ResponseModality: @retroactive Decodable { public init(from decoder: Decoder) throws { diff --git a/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift b/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift index ce38574..c7d4f32 100644 --- a/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift +++ b/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift @@ -23,7 +23,6 @@ import FirebaseAuth enum RecipeStoreError: Error { case missingRecipeID case likeDecodingError(String) - case recipeDecodingError(String) case reviewDecodingError(String) } @@ -61,14 +60,23 @@ class RecipeStore { filters = filters.where(Field("title").like("%\(configuration.recipeTitle)%")) } - if configuration.minimumRating > 0 { - filters = filters.where(Field("averageRating").greaterThanOrEqual(configuration.minimumRating)) - } - if !configuration.selectedTags.isEmpty { filters = filters.where(Field("tags").arrayContainsAny(Array(configuration.selectedTags))) } + if configuration.minimumRating > 0 { + let parentRestaurantID = "parentRestaurantID" + filters = filters.define([Field("__name__").as(parentRestaurantID)]) + .addFields([ + Subcollection("reviews") + .aggregate([Field("rating").average().as("averageRating")]) + .toScalarExpression() + .as("averageRating") + ]) + .where(Field("averageRating").exists() && Field("averageRating").greaterThanOrEqual(configuration.minimumRating) + ) + } + switch configuration.sortOption { case .alphabetical: filters = filters.sort([Field("title").ascending()]) @@ -98,44 +106,13 @@ class RecipeStore { try collection.addDocument(from: recipe) } - func fetchRecipes(withUserID userID: String? = Auth.auth().currentUser?.uid) async throws { + func fetchRecipes() async throws { let query = activeQuery let snapshot = try await query.execute() self.recipes = try snapshot.results.compactMap { result in - let imageURL = result.data["imageUri"] as? String - - guard let title = result.data["title"] as? String, - let instructions = result.data["instructions"] as? String, - let ingredients = result.data["ingredients"] as? [String], - let authorID = result.data["authorId"] as? String, - let tags = result.data["tags"] as? [String], - let averageRating = result.data["averageRating"] as? Double, - let prepTime = result.data["prepTime"] as? String, - let cookTime = result.data["cookTime"] as? String, - let servings = result.data["servings"] as? String, - let documentID = result.id else { - let errorMessage = "Unable to initialize recipes from data: \(result.data)" - throw RecipeStoreError.recipeDecodingError(errorMessage) - } - - var recipe = Recipe( - title: title, - instructions: instructions, - ingredients: ingredients, - authorId: authorID, - tags: tags, - averageRating: averageRating, - imageUri: imageURL, - prepTime: prepTime, - cookTime: cookTime, - servings: servings - ) - - recipe.id = documentID - return recipe + return try Recipe(from: result) } - } @discardableResult @@ -175,6 +152,22 @@ class RecipeStore { let docRef = db.collection(RecipeStore.recipeCollection).document(id) try docRef.setData(from: recipe, mergeFields: ["isFavorite"]) } + + func averageRating(for recipeID: String) async throws -> Double? { + let collectionPath = + "\(RecipeStore.recipeCollection)/\(recipeID)/\(RecipeStore.reviewsSubcollection)" + + let snapshot = try await db + .pipeline() + .collection(collectionPath) + .aggregate([Field("rating").average().as("averageRating")]) + .execute() + + let average = snapshot.results.first?.data["averageRating"] as? Double + return average.flatMap { + return $0 >= 1 && $0 <= 5 ? $0 : nil + } + } } // Reviews diff --git a/FriendlyMeals/FriendlyMeals/Shared/Services/RemoteConfigService.swift b/FriendlyMeals/FriendlyMeals/Shared/Services/RemoteConfigService.swift index 5385977..0439110 100644 --- a/FriendlyMeals/FriendlyMeals/Shared/Services/RemoteConfigService.swift +++ b/FriendlyMeals/FriendlyMeals/Shared/Services/RemoteConfigService.swift @@ -17,7 +17,7 @@ import Foundation import FirebaseRemoteConfig -import FirebaseAI +import FirebaseAILogic fileprivate enum RemoteConfigKey: String { case maxImagesPerDay = "max_images_per_day" From 29086763a4e25bc83c1ced0bcdca60bae6bf1c7b Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Tue, 14 Apr 2026 18:53:23 -0700 Subject: [PATCH 2/7] fix search --- .../Features/Cookbook/Views/FilterView.swift | 5 +++++ .../Shared/Services/RecipeStore.swift | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/FriendlyMeals/FriendlyMeals/Features/Cookbook/Views/FilterView.swift b/FriendlyMeals/FriendlyMeals/Features/Cookbook/Views/FilterView.swift index 23c186e..8685737 100644 --- a/FriendlyMeals/FriendlyMeals/Features/Cookbook/Views/FilterView.swift +++ b/FriendlyMeals/FriendlyMeals/Features/Cookbook/Views/FilterView.swift @@ -32,6 +32,8 @@ struct FilterConfiguration { var recipeTitle = "" + var recipeInstructions = "" + var minimumRating: Double = 0 var selectedTags: Set = [] @@ -87,6 +89,9 @@ struct FilterView: View { Text("Filter by title") TextField("Scallops", text: $configuration.recipeTitle) + Text("Search recipe text") + TextField("Bake for 30 minutes", text: $configuration.recipeInstructions) + Text("Minimum rating: \(configuration.minimumRating.formatted())") Slider( value: $configuration.minimumRating, diff --git a/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift b/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift index c7d4f32..a1b791c 100644 --- a/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift +++ b/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift @@ -52,6 +52,15 @@ class RecipeStore { to pipeline: Pipeline, currentUserID: String? = Auth.auth().currentUser?.uid) -> Pipeline { var filters = pipeline + if !configuration.recipeInstructions.isEmpty { + filters = pipeline.search( + query: "\(configuration.recipeInstructions)", + addFields: [ + Score().as("searchScore") + ] + ) + } + if let id = currentUserID, configuration.shouldShowOnlyOwnRecipes { filters = filters.where(Field("authorId").equal(id)) } @@ -85,6 +94,10 @@ class RecipeStore { case .popularity: filters = filters.sort([Field("likes").descending()]) case .none: + // If no existing filter, sort by search score if it exists. + if !configuration.recipeInstructions.isEmpty { + filters = filters.sort([Field("searchScore").descending()]) + } break @unknown default: break @@ -96,7 +109,8 @@ class RecipeStore { func applyConfiguration(_ configuration: FilterConfiguration) { filterConfiguration = configuration let output = { (store: Firestore) -> Pipeline in - return self.applyConfiguration(configuration, to: store.pipeline().collection(RecipeStore.recipeCollection)) + let pipeline = store.pipeline().collection(RecipeStore.recipeCollection) + return self.applyConfiguration(configuration, to: pipeline) } activeFilters = output } From 21ad66487183f6936733e9b391d20187f3654cab Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Thu, 16 Apr 2026 15:06:22 -0700 Subject: [PATCH 3/7] remove averageRating in favor of subquery --- .../FriendlyMeals/Shared/Models/Recipe.swift | 6 ------ .../Shared/Services/RecipeStore.swift | 16 ---------------- .../Shared/Views/RecipeDetailsView.swift | 1 - 3 files changed, 23 deletions(-) diff --git a/FriendlyMeals/FriendlyMeals/Shared/Models/Recipe.swift b/FriendlyMeals/FriendlyMeals/Shared/Models/Recipe.swift index 4c34500..32ad1d1 100644 --- a/FriendlyMeals/FriendlyMeals/Shared/Models/Recipe.swift +++ b/FriendlyMeals/FriendlyMeals/Shared/Models/Recipe.swift @@ -30,7 +30,6 @@ struct Recipe: Codable, Identifiable, Hashable, RecipeRepresentable { // These are plain strings var tags: [String] - var averageRating: Double var imageUri: String? // These are display strings @@ -48,7 +47,6 @@ extension Recipe { ingredients: representable.ingredients, authorId: authorID ?? "anonymous", tags: representable.tags, - averageRating: 0, imageUri: representable.imageUri, prepTime: representable.prepTime, cookTime: representable.cookTime, @@ -63,7 +61,6 @@ extension Recipe { let ingredients = result.data["ingredients"] as? [String], let authorID = result.data["authorId"] as? String, let tags = result.data["tags"] as? [String], - let averageRating = result.data["averageRating"] as? Double, let prepTime = result.data["prepTime"] as? String, let cookTime = result.data["cookTime"] as? String, let servings = result.data["servings"] as? String, @@ -72,15 +69,12 @@ extension Recipe { throw DecodingError.missingKeys(errorMessage) } - print("rating: \(result.data["averageRating"])") - self.init( title: title, instructions: instructions, ingredients: ingredients, authorId: authorID, tags: tags, - averageRating: averageRating, imageUri: imageURL, prepTime: prepTime, cookTime: cookTime, diff --git a/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift b/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift index a1b791c..50cb854 100644 --- a/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift +++ b/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift @@ -166,22 +166,6 @@ class RecipeStore { let docRef = db.collection(RecipeStore.recipeCollection).document(id) try docRef.setData(from: recipe, mergeFields: ["isFavorite"]) } - - func averageRating(for recipeID: String) async throws -> Double? { - let collectionPath = - "\(RecipeStore.recipeCollection)/\(recipeID)/\(RecipeStore.reviewsSubcollection)" - - let snapshot = try await db - .pipeline() - .collection(collectionPath) - .aggregate([Field("rating").average().as("averageRating")]) - .execute() - - let average = snapshot.results.first?.data["averageRating"] as? Double - return average.flatMap { - return $0 >= 1 && $0 <= 5 ? $0 : nil - } - } } // Reviews diff --git a/FriendlyMeals/FriendlyMeals/Shared/Views/RecipeDetailsView.swift b/FriendlyMeals/FriendlyMeals/Shared/Views/RecipeDetailsView.swift index 9816080..4d8911b 100644 --- a/FriendlyMeals/FriendlyMeals/Shared/Views/RecipeDetailsView.swift +++ b/FriendlyMeals/FriendlyMeals/Shared/Views/RecipeDetailsView.swift @@ -232,7 +232,6 @@ extension RecipeDetailsView: View { ], authorId: "no author", tags: [], - averageRating: 4.5, imageUri: "https://www.gstatic.com/devrel-devsite/prod/ve08add287a6b4bdf8961ab8a1be50bf551be3816cdd70b7cc934114ff3ad5f10/firebase/images/lockup.svg", prepTime: "30 minutes", cookTime: "10 minutes", From 7227c8f9e36619d82241c6656bcd957b1865fa5f Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Wed, 22 Apr 2026 14:18:10 -0700 Subject: [PATCH 4/7] add likes --- .../Cookbook/Views/RecipeListView.swift | 5 +++ .../FriendlyMeals/Shared/Models/Recipe.swift | 10 ++++-- .../Shared/Services/RecipeStore.swift | 32 ++++++++++++++++--- .../Shared/Views/RecipeDetailsView.swift | 9 ++++++ 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/FriendlyMeals/FriendlyMeals/Features/Cookbook/Views/RecipeListView.swift b/FriendlyMeals/FriendlyMeals/Features/Cookbook/Views/RecipeListView.swift index e05fd79..624f89a 100644 --- a/FriendlyMeals/FriendlyMeals/Features/Cookbook/Views/RecipeListView.swift +++ b/FriendlyMeals/FriendlyMeals/Features/Cookbook/Views/RecipeListView.swift @@ -38,6 +38,11 @@ struct RecipeListView: View { .font(.headline) } Spacer() + if let likes = recipe.likes, likes > 0 { + Text("\(likes)") + .font(.caption) + .foregroundStyle(.secondary) + } if likesStore.isLiked(recipe) { Image(systemName: "heart.fill") .foregroundColor(.pink) diff --git a/FriendlyMeals/FriendlyMeals/Shared/Models/Recipe.swift b/FriendlyMeals/FriendlyMeals/Shared/Models/Recipe.swift index 32ad1d1..0d7ba69 100644 --- a/FriendlyMeals/FriendlyMeals/Shared/Models/Recipe.swift +++ b/FriendlyMeals/FriendlyMeals/Shared/Models/Recipe.swift @@ -37,6 +37,8 @@ struct Recipe: Codable, Identifiable, Hashable, RecipeRepresentable { var cookTime: String var servings: String + var likes: Int? + } extension Recipe { @@ -50,7 +52,8 @@ extension Recipe { imageUri: representable.imageUri, prepTime: representable.prepTime, cookTime: representable.cookTime, - servings: representable.servings + servings: representable.servings, + likes: nil ) } @@ -69,6 +72,8 @@ extension Recipe { throw DecodingError.missingKeys(errorMessage) } + let likes = result.data["likes"] as? Int + self.init( title: title, instructions: instructions, @@ -78,7 +83,8 @@ extension Recipe { imageUri: imageURL, prepTime: prepTime, cookTime: cookTime, - servings: servings + servings: servings, + likes: likes ) self.id = documentID diff --git a/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift b/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift index 50cb854..36aa566 100644 --- a/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift +++ b/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift @@ -39,6 +39,15 @@ class RecipeStore { private static func defaultFilter(_ store: Firestore) -> Pipeline { return store.pipeline().collection(recipeCollection) + .define([Field("__name__").documentId().as("parentRecipeId")]) + .addFields([ + store.pipeline() + .collection("likes") + .where(Field("recipeId").equal(Variable("parentRecipeId"))) + .aggregate([CountAll().as("count")]) + .toScalarExpression() + .as("likes") + ]) } private var activeQuery: Pipeline { @@ -50,10 +59,14 @@ class RecipeStore { private func applyConfiguration(_ configuration: FilterConfiguration, to pipeline: Pipeline, + using db: Firestore, currentUserID: String? = Auth.auth().currentUser?.uid) -> Pipeline { var filters = pipeline + if !configuration.recipeInstructions.isEmpty { - filters = pipeline.search( + // Search must be the first stage in a pipeline, so don't add any + // stages before this. + filters = filters.search( query: "\(configuration.recipeInstructions)", addFields: [ Score().as("searchScore") @@ -77,7 +90,8 @@ class RecipeStore { let parentRestaurantID = "parentRestaurantID" filters = filters.define([Field("__name__").as(parentRestaurantID)]) .addFields([ - Subcollection("reviews") + db.pipeline() + .collection("reviews") .aggregate([Field("rating").average().as("averageRating")]) .toScalarExpression() .as("averageRating") @@ -92,7 +106,15 @@ class RecipeStore { case .rating: filters = filters.sort([Field("averageRating").descending()]) case .popularity: - filters = filters.sort([Field("likes").descending()]) + filters = filters.define([Field("__name__").documentId().as("parentRecipeId")]) + .addFields([ + db.pipeline() + .collection("likes") + .where(Field("recipeId").equal(Variable("parentRecipeId"))) + .aggregate([CountAll().as("count")]) + .toScalarExpression() + .as("likes") + ]).sort([Field("likes").descending()]) case .none: // If no existing filter, sort by search score if it exists. if !configuration.recipeInstructions.isEmpty { @@ -110,7 +132,7 @@ class RecipeStore { filterConfiguration = configuration let output = { (store: Firestore) -> Pipeline in let pipeline = store.pipeline().collection(RecipeStore.recipeCollection) - return self.applyConfiguration(configuration, to: pipeline) + return self.applyConfiguration(configuration, to: pipeline, using: store) } activeFilters = output } @@ -124,6 +146,8 @@ class RecipeStore { let query = activeQuery let snapshot = try await query.execute() + print(snapshot.results) + print(self.recipes) self.recipes = try snapshot.results.compactMap { result in return try Recipe(from: result) } diff --git a/FriendlyMeals/FriendlyMeals/Shared/Views/RecipeDetailsView.swift b/FriendlyMeals/FriendlyMeals/Shared/Views/RecipeDetailsView.swift index 4d8911b..d917e1d 100644 --- a/FriendlyMeals/FriendlyMeals/Shared/Views/RecipeDetailsView.swift +++ b/FriendlyMeals/FriendlyMeals/Shared/Views/RecipeDetailsView.swift @@ -92,6 +92,15 @@ extension RecipeDetailsView: View { if remainder > 0 { partialStar(percentage: remainder) } + + if let likes = recipe.likes { + Spacer() + Image(systemName: "heart.fill") + .foregroundColor(.pink) + Text("\(likes) likes") + .font(.subheadline) + .foregroundStyle(.secondary) + } } .task { do { From dd4c0b680aa27b54da23cd831da6b6b0c11f2442 Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Wed, 22 Apr 2026 14:31:00 -0700 Subject: [PATCH 5/7] Fix rating filter and beautify filters --- .../Features/Cookbook/Views/FilterView.swift | 139 +++++++++++------- .../Shared/Services/RecipeStore.swift | 8 +- 2 files changed, 89 insertions(+), 58 deletions(-) diff --git a/FriendlyMeals/FriendlyMeals/Features/Cookbook/Views/FilterView.swift b/FriendlyMeals/FriendlyMeals/Features/Cookbook/Views/FilterView.swift index 8685737..6150a98 100644 --- a/FriendlyMeals/FriendlyMeals/Features/Cookbook/Views/FilterView.swift +++ b/FriendlyMeals/FriendlyMeals/Features/Cookbook/Views/FilterView.swift @@ -44,6 +44,8 @@ struct FilterConfiguration { struct FilterView: View { + @Environment(\.dismiss) private var dismiss + init( tags: [String], configuration: FilterConfiguration? = FilterConfiguration(), @@ -78,71 +80,102 @@ struct FilterView: View { @State private var configuration: FilterConfiguration var body: some View { - VStack(alignment: .leading) { - Text("Filters") - .font(.largeTitle) + NavigationStack { + Form { + Section { + Toggle("View only my recipes", isOn: $configuration.shouldShowOnlyOwnRecipes) + } header: { + Label("General", systemImage: "slider.horizontal.3") + } - Toggle(isOn: $configuration.shouldShowOnlyOwnRecipes) { - Text("View only my recipes") - } + Section { + LabeledContent("Title") { + TextField("Scallops", text: $configuration.recipeTitle) + .multilineTextAlignment(.trailing) + } + LabeledContent("Instructions") { + TextField("Bake for 30 minutes", text: $configuration.recipeInstructions) + .multilineTextAlignment(.trailing) + } + } header: { + Label("Search", systemImage: "magnifyingglass") + } - Text("Filter by title") - TextField("Scallops", text: $configuration.recipeTitle) - - Text("Search recipe text") - TextField("Bake for 30 minutes", text: $configuration.recipeInstructions) - - Text("Minimum rating: \(configuration.minimumRating.formatted())") - Slider( - value: $configuration.minimumRating, - in: 0...5, - step: 0.25 - ) { - Text("Minimum rating") - } minimumValueLabel: { - Text("0") - } maximumValueLabel: { - Text("5") - } onEditingChanged: { _ in - // do nothing - } + Section { + VStack(alignment: .leading, spacing: 12) { + HStack { + Text(configuration.minimumRating.formatted()) + .font(.headline) + Spacer() + HStack(spacing: 2) { + let rating = configuration.minimumRating + ForEach(0..<5) { index in + let starName = index < Int(rating) ? "star.fill" : (index < Int(rating.rounded(.up)) ? "star.leadinghalf.filled" : "star") + Image(systemName: starName) + .foregroundColor(.yellow) + } + } + } + Slider(value: $configuration.minimumRating, in: 0...5, step: 0.25) + .tint(.blue) + } + .padding(.vertical, 4) + } header: { + Label("Minimum Rating", systemImage: "star.fill") + } - Text("Tags") - ScrollView(.horizontal, showsIndicators: true) { - HStack { - ForEach(0 ..< tags.count, id: \.self) { index in - let tag = tags[index] - let isSelected = tagSelections[index] - Toggle(tag, isOn: $tagSelections[index]) - .toggleStyle(.button) - .tint(isSelected ? .blue : .secondary) + Section { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(0 ..< tags.count, id: \.self) { index in + let tag = tags[index] + let isSelected = tagSelections[index] + Toggle(tag, isOn: $tagSelections[index]) + .toggleStyle(.button) + .tint(isSelected ? .blue : .secondary) + .clipShape(Capsule()) + } + } + .padding(.vertical, 4) } + } header: { + Label("Tags", systemImage: "tag.fill") } - } - Text("Sort by") - Picker("Choose a sort method", selection: $configuration.sortOption) { - ForEach(FilterConfiguration.sortOptions, id: \.self) { option in - Text(option.rawValue).tag(option.rawValue) + Section { + Picker("Sort method", selection: $configuration.sortOption) { + ForEach(FilterConfiguration.sortOptions, id: \.self) { option in + Text(option.rawValue).tag(option) + } + } + .pickerStyle(.menu) + } header: { + Label("Sort By", systemImage: "arrow.up.arrow.down") } } - - HStack { - Button("Remove filters") { - configuration = FilterConfiguration() - tagSelections = tagSelections.map { _ in false } + .navigationTitle("Filters") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Reset") { + configuration = FilterConfiguration() + tagSelections = tagSelections.map { _ in false } + } + .foregroundColor(.red) } - Button("Apply filters") { - let selectedTags = tags.indices - .filter { tagSelections[$0] } - .map { tags[$0] } - configuration.selectedTags = Set(selectedTags) - applyFilters(configuration) + ToolbarItem(placement: .confirmationAction) { + Button("Apply") { + let selectedTags = tags.indices + .filter { tagSelections[$0] } + .map { tags[$0] } + configuration.selectedTags = Set(selectedTags) + applyFilters(configuration) + dismiss() + } + .fontWeight(.bold) } } - Spacer() } - .padding(16) } } diff --git a/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift b/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift index 36aa566..05fbabc 100644 --- a/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift +++ b/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift @@ -87,11 +87,9 @@ class RecipeStore { } if configuration.minimumRating > 0 { - let parentRestaurantID = "parentRestaurantID" - filters = filters.define([Field("__name__").as(parentRestaurantID)]) + filters = filters .addFields([ - db.pipeline() - .collection("reviews") + Subcollection(RecipeStore.reviewsSubcollection) .aggregate([Field("rating").average().as("averageRating")]) .toScalarExpression() .as("averageRating") @@ -195,7 +193,7 @@ class RecipeStore { // Reviews extension RecipeStore { - private static let reviewsSubcollection = "reviews" + fileprivate static let reviewsSubcollection = "reviews" func fetchReview(userID: String, recipeID: String) async throws -> Review? { let compositeID = "\(recipeID)_\(userID)" From 37184979e6578a59d7d325a460eb7d4b020afb23 Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Wed, 22 Apr 2026 15:07:32 -0700 Subject: [PATCH 6/7] code review feedback --- .../FriendlyMeals/Shared/Models/Recipe.swift | 4 ++-- .../Shared/Services/RecipeStore.swift | 14 ++------------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/FriendlyMeals/FriendlyMeals/Shared/Models/Recipe.swift b/FriendlyMeals/FriendlyMeals/Shared/Models/Recipe.swift index 0d7ba69..d229739 100644 --- a/FriendlyMeals/FriendlyMeals/Shared/Models/Recipe.swift +++ b/FriendlyMeals/FriendlyMeals/Shared/Models/Recipe.swift @@ -69,7 +69,7 @@ extension Recipe { let servings = result.data["servings"] as? String, let documentID = result.id else { let errorMessage = "Unable to initialize recipes from data: \(result.data)" - throw DecodingError.missingKeys(errorMessage) + throw RecipeDecodingError.missingKeys(errorMessage) } let likes = result.data["likes"] as? Int @@ -90,7 +90,7 @@ extension Recipe { self.id = documentID } - enum DecodingError: Error { + enum RecipeDecodingError: Error { case missingKeys(String) } } diff --git a/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift b/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift index 05fbabc..3222e3d 100644 --- a/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift +++ b/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift @@ -104,15 +104,7 @@ class RecipeStore { case .rating: filters = filters.sort([Field("averageRating").descending()]) case .popularity: - filters = filters.define([Field("__name__").documentId().as("parentRecipeId")]) - .addFields([ - db.pipeline() - .collection("likes") - .where(Field("recipeId").equal(Variable("parentRecipeId"))) - .aggregate([CountAll().as("count")]) - .toScalarExpression() - .as("likes") - ]).sort([Field("likes").descending()]) + filters = filters.sort([Field("likes").descending()]) case .none: // If no existing filter, sort by search score if it exists. if !configuration.recipeInstructions.isEmpty { @@ -129,7 +121,7 @@ class RecipeStore { func applyConfiguration(_ configuration: FilterConfiguration) { filterConfiguration = configuration let output = { (store: Firestore) -> Pipeline in - let pipeline = store.pipeline().collection(RecipeStore.recipeCollection) + let pipeline = RecipeStore.defaultFilter(store) return self.applyConfiguration(configuration, to: pipeline, using: store) } activeFilters = output @@ -144,8 +136,6 @@ class RecipeStore { let query = activeQuery let snapshot = try await query.execute() - print(snapshot.results) - print(self.recipes) self.recipes = try snapshot.results.compactMap { result in return try Recipe(from: result) } From 881e7f757b68ad9df744ee0af87d12e7bfa471e8 Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Wed, 22 Apr 2026 15:25:04 -0700 Subject: [PATCH 7/7] address review feedback --- .../FriendlyMeals.xcodeproj/project.pbxproj | 57 ++++++++++++++++--- .../Shared/Services/RecipeStore.swift | 34 ++++++++--- 2 files changed, 74 insertions(+), 17 deletions(-) diff --git a/FriendlyMeals/FriendlyMeals.xcodeproj/project.pbxproj b/FriendlyMeals/FriendlyMeals.xcodeproj/project.pbxproj index bf33cc0..7b7e856 100644 --- a/FriendlyMeals/FriendlyMeals.xcodeproj/project.pbxproj +++ b/FriendlyMeals/FriendlyMeals.xcodeproj/project.pbxproj @@ -9,6 +9,11 @@ /* Begin PBXBuildFile section */ 884B2CF22E144BA7007B2E0E /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 884B2CF12E144BA7007B2E0E /* MarkdownUI */; }; 887565682E86CCEE0041DDEF /* ConversationKit in Frameworks */ = {isa = PBXBuildFile; productRef = 887565672E86CCEE0041DDEF /* ConversationKit */; }; + 8D3574952F997F29003F1C27 /* FirebaseAILogic in Frameworks */ = {isa = PBXBuildFile; productRef = 8D3574942F997F29003F1C27 /* FirebaseAILogic */; }; + 8D3574972F997F29003F1C27 /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 8D3574962F997F29003F1C27 /* FirebaseAuth */; }; + 8D3574992F997F29003F1C27 /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 8D3574982F997F29003F1C27 /* FirebaseFirestore */; }; + 8D35749B2F997F29003F1C27 /* FirebaseRemoteConfig in Frameworks */ = {isa = PBXBuildFile; productRef = 8D35749A2F997F29003F1C27 /* FirebaseRemoteConfig */; }; + 8D35749D2F997F29003F1C27 /* FirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 8D35749C2F997F29003F1C27 /* FirebaseStorage */; }; 8D772B902F8039530054270D /* FoundationModels.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8D772B8F2F8039530054270D /* FoundationModels.framework */; }; 8D772B932F803C350054270D /* FirebaseAILogic in Frameworks */ = {isa = PBXBuildFile; productRef = 8D772B922F803C350054270D /* FirebaseAILogic */; }; 8D772B952F803C350054270D /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 8D772B942F803C350054270D /* FirebaseAuth */; }; @@ -44,9 +49,14 @@ 8DBC83FD2F17076D00DAC906 /* ConversationKit in Frameworks */, 8DBC83FC2F17076D00DAC906 /* ConversationKit in Frameworks */, 8D772B972F803C350054270D /* FirebaseFirestore in Frameworks */, + 8D3574972F997F29003F1C27 /* FirebaseAuth in Frameworks */, 8D772B9B2F803C350054270D /* FirebaseStorage in Frameworks */, + 8D35749B2F997F29003F1C27 /* FirebaseRemoteConfig in Frameworks */, 8DBC83FB2F17076D00DAC906 /* ConversationKit in Frameworks */, + 8D3574992F997F29003F1C27 /* FirebaseFirestore in Frameworks */, 8D772B952F803C350054270D /* FirebaseAuth in Frameworks */, + 8D3574952F997F29003F1C27 /* FirebaseAILogic in Frameworks */, + 8D35749D2F997F29003F1C27 /* FirebaseStorage in Frameworks */, 8DBC83FA2F17076D00DAC906 /* ConversationKit in Frameworks */, 887565682E86CCEE0041DDEF /* ConversationKit in Frameworks */, 884B2CF22E144BA7007B2E0E /* MarkdownUI in Frameworks */, @@ -113,6 +123,11 @@ 8D772B962F803C350054270D /* FirebaseFirestore */, 8D772B982F803C350054270D /* FirebaseRemoteConfig */, 8D772B9A2F803C350054270D /* FirebaseStorage */, + 8D3574942F997F29003F1C27 /* FirebaseAILogic */, + 8D3574962F997F29003F1C27 /* FirebaseAuth */, + 8D3574982F997F29003F1C27 /* FirebaseFirestore */, + 8D35749A2F997F29003F1C27 /* FirebaseRemoteConfig */, + 8D35749C2F997F29003F1C27 /* FirebaseStorage */, ); productName = FriendlyMeals; productReference = 88C8FB652E13D879001B86C4 /* FriendlyMeals.app */; @@ -145,7 +160,7 @@ packageReferences = ( 884B2CF02E144BA7007B2E0E /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, 88FF5D522ECF579900646871 /* XCRemoteSwiftPackageReference "ConversationKit" */, - 8D772B912F803C350054270D /* XCLocalSwiftPackageReference "../../firebase-ios-sdk" */, + 8D3574932F997F29003F1C27 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, ); preferredProjectObjectVersion = 77; productRefGroup = 88C8FB662E13D879001B86C4 /* Products */; @@ -386,13 +401,6 @@ }; /* End XCConfigurationList section */ -/* Begin XCLocalSwiftPackageReference section */ - 8D772B912F803C350054270D /* XCLocalSwiftPackageReference "../../firebase-ios-sdk" */ = { - isa = XCLocalSwiftPackageReference; - relativePath = "../../firebase-ios-sdk"; - }; -/* End XCLocalSwiftPackageReference section */ - /* Begin XCRemoteSwiftPackageReference section */ 884B2CF02E144BA7007B2E0E /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = { isa = XCRemoteSwiftPackageReference; @@ -410,6 +418,14 @@ minimumVersion = 0.0.3; }; }; + 8D3574932F997F29003F1C27 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/firebase/firebase-ios-sdk"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 12.12.1; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -439,6 +455,31 @@ package = 88FF5D522ECF579900646871 /* XCRemoteSwiftPackageReference "ConversationKit" */; productName = ConversationKit; }; + 8D3574942F997F29003F1C27 /* FirebaseAILogic */ = { + isa = XCSwiftPackageProductDependency; + package = 8D3574932F997F29003F1C27 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseAILogic; + }; + 8D3574962F997F29003F1C27 /* FirebaseAuth */ = { + isa = XCSwiftPackageProductDependency; + package = 8D3574932F997F29003F1C27 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseAuth; + }; + 8D3574982F997F29003F1C27 /* FirebaseFirestore */ = { + isa = XCSwiftPackageProductDependency; + package = 8D3574932F997F29003F1C27 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseFirestore; + }; + 8D35749A2F997F29003F1C27 /* FirebaseRemoteConfig */ = { + isa = XCSwiftPackageProductDependency; + package = 8D3574932F997F29003F1C27 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseRemoteConfig; + }; + 8D35749C2F997F29003F1C27 /* FirebaseStorage */ = { + isa = XCSwiftPackageProductDependency; + package = 8D3574932F997F29003F1C27 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseStorage; + }; 8D772B922F803C350054270D /* FirebaseAILogic */ = { isa = XCSwiftPackageProductDependency; productName = FirebaseAILogic; diff --git a/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift b/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift index 3222e3d..966ce85 100644 --- a/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift +++ b/FriendlyMeals/FriendlyMeals/Shared/Services/RecipeStore.swift @@ -74,6 +74,9 @@ class RecipeStore { ) } + let shouldAddAverageRating = configuration.minimumRating > 0 || + configuration.sortOption == .rating + if let id = currentUserID, configuration.shouldShowOnlyOwnRecipes { filters = filters.where(Field("authorId").equal(id)) } @@ -86,15 +89,28 @@ class RecipeStore { filters = filters.where(Field("tags").arrayContainsAny(Array(configuration.selectedTags))) } + // Always add likes + filters = filters.define([Field("__name__").documentId().as("parentRecipeId")]).addFields([ + db.pipeline() + .collection("likes") + .where(Field("recipeId").equal(Variable("parentRecipeId"))) + .aggregate([CountAll().as("count")]) + .toScalarExpression() + .as("likes") + ]) + // Add rating sometimes + if shouldAddAverageRating { + filters = filters.addFields([ + Subcollection(RecipeStore.reviewsSubcollection) + .aggregate([Field("rating").average().as("averageRating")]) + .toScalarExpression() + .as("averageRating") + ]) + } + if configuration.minimumRating > 0 { - filters = filters - .addFields([ - Subcollection(RecipeStore.reviewsSubcollection) - .aggregate([Field("rating").average().as("averageRating")]) - .toScalarExpression() - .as("averageRating") - ]) - .where(Field("averageRating").exists() && Field("averageRating").greaterThanOrEqual(configuration.minimumRating) + filters = filters.where( + Field("averageRating").exists() && Field("averageRating").greaterThanOrEqual(configuration.minimumRating) ) } @@ -121,7 +137,7 @@ class RecipeStore { func applyConfiguration(_ configuration: FilterConfiguration) { filterConfiguration = configuration let output = { (store: Firestore) -> Pipeline in - let pipeline = RecipeStore.defaultFilter(store) + let pipeline = store.pipeline().collection(RecipeStore.recipeCollection) return self.applyConfiguration(configuration, to: pipeline, using: store) } activeFilters = output