1.0.4: Smarter auto-assign, quick dish entry, calendar fix, frictionless onboarding

- AutocompleteEngine: no-repeat rule now applies to all dishes (not just tagless ones); fallback still allows repeats when no alternative exists
- CalendarService: update existing events instead of delete+create to prevent duplicates; fix weekStartDate using plan date instead of currentWeekStart
- DishFormView: form stays open after saving in create mode for quick multi-dish entry; Done button to close; saved feedback banner
- OnboardingViewModel/FirstDishesStepView: dishes are now optional during onboarding; Finish button always enabled
- AnalyticsService: new events — onboarding_step_viewed(step) and auto_assign_used(filled, unfilled)
- Localization: dish_done and first_dishes_optional_hint added in 6 languages; release notes updated in all 6 languages
- Version bump: 1.0.3 → 1.0.4 (build 19)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
alexandrev-tibco
2026-04-19 11:35:50 +02:00
parent c66132f494
commit 6c7e12b41f
25 changed files with 659 additions and 49 deletions
+26 -2
View File
@@ -10,6 +10,7 @@ struct DishFormView: View {
@StateObject private var viewModel = DishViewModel()
@State private var showPremium = false
@State private var showDishLimitUpsell = false
@State private var showSavedFeedback = false
var dish: Dish?
@@ -30,6 +31,20 @@ struct DishFormView: View {
ScrollView {
VStack(spacing: 20) {
if showSavedFeedback {
HStack(spacing: 8) {
Image(systemName: "checkmark.circle.fill")
Text("toast_dish_saved")
}
.font(.mealMoodSmall)
.foregroundColor(.mealMoodSuccess)
.padding(.vertical, 10)
.frame(maxWidth: .infinity)
.background(Color.mealMoodSuccess.opacity(0.1))
.cornerRadius(10)
.transition(.opacity.combined(with: .move(edge: .top)))
}
// Name field
VStack(alignment: .leading, spacing: 8) {
Text("dish_name_label")
@@ -150,7 +165,7 @@ struct DishFormView: View {
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("dish_cancel") { dismiss() }
Button(viewModel.isEditing ? "dish_cancel" : "dish_done") { dismiss() }
.foregroundColor(.mealMoodTextSecondary)
}
ToolbarItem(placement: .navigationBarTrailing) {
@@ -163,8 +178,17 @@ struct DishFormView: View {
HapticManager.shared.notification(type: .warning)
return
}
let wasEditing = viewModel.isEditing
viewModel.save(context: context)
dismiss()
if wasEditing {
dismiss()
} else {
withAnimation { showSavedFeedback = true }
HapticManager.shared.notification(type: .success)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
withAnimation { showSavedFeedback = false }
}
}
} label: {
Image(systemName: "checkmark")
.fontWeight(.semibold)
@@ -51,6 +51,13 @@ struct FirstDishesStepView: View {
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 24)
Text("first_dishes_optional_hint")
.font(.mealMoodCaption)
.foregroundColor(.mealMoodCoral.opacity(0.8))
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 24)
.padding(.top, -16)
// New dish form
VStack(spacing: 12) {
TextField("first_dishes_name_placeholder", text: $viewModel.newDishName)
@@ -232,7 +239,7 @@ struct FirstDishesStepView: View {
HapticManager.shared.notification(type: .success)
onFinish()
},
isEnabled: viewModel.addedDishes.count >= 2
isEnabled: true
)
.padding(.horizontal, 24)
.padding(.top, 12)
@@ -79,6 +79,10 @@ struct OnboardingView: View {
.onAppear {
ensureDefaultTagsIfNeeded()
Task { await restoreFromICloudAndFinishIfNeeded() }
AnalyticsService.logOnboardingStepViewed(step: 0)
}
.onChange(of: viewModel.currentStep) { _, newStep in
AnalyticsService.logOnboardingStepViewed(step: newStep)
}
}