Вопрос или проблема
Я работаю над приложением для рецептов. На экране я хочу иметь представление с “Рецептами дня”, которое просто показывает рецепты в базе данных (Firestore). Это работает, как и ожидалось, но к базе данных обращаются n-раз, количество рецептов.
import SwiftUI
struct CardView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
NavigationStack {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
ForEach(viewModel.recipes, id: \.id) { recipe in
Card(recipe: recipe)
}
}
.padding(.horizontal, 16)
}
.navigationTitle("Рецепты дня")
.onAppear() {
viewModel.loadRecipes()
}
}
}
}
#Preview {
CardView()
}
А это ViewModel для CardView:
import FirebaseFirestore
import Foundation
import SwiftUI
extension CardView {
class ViewModel: ObservableObject {
@Published var recipes: [Recipe] = []
func loadRecipes() {
let db = Firestore.firestore()
let recipesRef = db.collection("recipes")
recipesRef.getDocuments { (querySnapshot, error) in
if let error = error {
print("Ошибка в запросе к базе данных: \(error.localizedDescription)")
return
} else {
print("Запрос к базе данных успешен")
}
guard let documents = querySnapshot?.documents else { return }
var recipes: [Recipe] = []
for document in documents {
let data = document.data()
let name = data["name"] as? String ?? ""
let rating = data["rating"] as? Double ?? 0
let comments = data["comments"] as? [String] ?? ["Тестовый комментарий"]
let image = data["image"] as? String ?? ""
let preparationTime = data["preparationTime"] as? Int ?? 0
let cookingTime = data["cookingTime"] as? Int ?? 0
let difficulty = data["difficulty"] as? String ?? "Легко"
let instructions = data["instructions"] as? String ?? "Тестовая инструкция"
let ingredients = data["ingredients"] as? [String: [String]] ?? ["": [""]]
_ = ingredients["ingredient"] ?? []
let ingredientsArray = data["ingredients"] as? [[String: Any]] ?? []
var ingredientList: [Ingredient] = []
for ingredientData in ingredientsArray {
let name = ingredientData["name"] as? String ?? ""
let quantity = ingredientData["quantity"] as? Int ?? 0
let unit = ingredientData["unit"] as? String ?? ""
ingredientList.append(Ingredient(name: name, quantity: quantity, unit: Unit(rawValue: unit) ?? .TL))
}
let recipe = Recipe(
name: name,
rating: rating,
comments: comments,
image: UIImage(named: image),
overallTime: preparationTime + cookingTime,
preparationTime: preparationTime,
cookingTime: cookingTime,
difficulty: difficulty,
ingredients: ingredientList,
instructions: instructions
)
print("Имя рецепта: \(recipe.name)")
recipes.append(recipe)
}
self.recipes = recipes
print("\(recipes.count) рецептов загружено")
}
}
}
}
Представление, которое я получаю, правильное, но из терминала я вижу, что оно вызывается несколько раз, что не требуется.
Вывод в консоли:
Запрос к базе данных успешен
Имя рецепта: Дубайский чизкейк
Имя рецепта: Торт Джото
Имя рецепта: Соус с кремом
3 рецепта загружено
Запрос к базе данных успешен
Имя рецепта: Дубайский чизкейк
Имя рецепта: Торт Джото
Имя рецепта: Соус с кремом
3 рецепта загружено
Запрос к базе данных успешен
Имя рецепта: Дубайский чизкейк
Имя рецепта: Торт Джото
Имя рецепта: Соус с кремом
3 рецепта загружено
Я уже пытался работать с флагом, чтобы убедиться, что функция loadRecipes() вызывается только один раз.
struct CardView: View {
@StateObject var viewModel = ViewModel()
@State private var recipesLoaded = false
var body: some View {
// ...
.onAppear {
if !recipesLoaded {
viewModel.loadRecipes()
recipesLoaded = true
}
}
}
}
Это дает мне такой же результат.
Ответ или решение
Вопрос, который вы подняли, — это актуальная проблема, с которой сталкиваются многие разработчики, работающие с SwiftUI и Firebase. Ваша реализация загружает данные из Firestore несколько раз, и это может привести к избыточным запросам к базе данных, задержкам в отображении данных и другим нежелательным последствиям.
Проблема многократных вызовов функций
Ваше текущее решение использует метод onAppear
, который срабатывает каждый раз, когда ваше представление (View) появляется на экране. Ваша реализация добавила проверку флага (recipesLoaded
), что является хорошей первой попыткой, однако при использовании состояния и представлений в SwiftUI все может работать нелинейно. Когда представление обновляется или пересоздается, метод onAppear
будет вызывать ваш код опять.
Решение проблемы
Рассмотрим несколько подходов для решения вашей проблемы с многократными вызовами функции загрузки рецептов.
-
Используйте метод
onAppear
с Guard-условием: Ваша идея с флагомrecipesLoaded
в принципе верна, но можно реализовать её немного иначе. Перемешайте флаг в вашемViewModel
:// ViewModel class ViewModel: ObservableObject { @Published var recipes: [Recipe] = [] private var isLoading: Bool = false // Флаг для предотвращения повторных запросов func loadRecipes() { guard !isLoading else { return } // Проверка, загружается ли уже isLoading = true // Устанавливаем флаг загрузки let db = Firestore.firestore() let recipesRef = db.collection("recipes") recipesRef.getDocuments { (querySnapshot, error) in self.isLoading = false // Сброс флага после завершения загрузки if let error = error { print("Error in DB query: \(error.localizedDescription)") return } else { print("DB query successful") } guard let documents = querySnapshot?.documents else { return } var recipes: [Recipe] = [] // Ваши обработки будут здесь... self.recipes = recipes print("\(recipes.count) recipes loaded") } } }
-
Создание Singleton для управления состоянием: Вы можете реализовать Singleton (единый экземпляр) для вашего ViewModel. Это позволит гарантировать, что данные загружаются только один раз и хранятся в одном месте.
class ViewModel: ObservableObject { static let shared = ViewModel() // Singleton @Published var recipes: [Recipe] = [] private var isLoading: Bool = false private init() {} // Скрываем инициализатор func loadRecipes() { // Ваша реализация... } }
В вашем представлении вы можете использовать
ViewModel.shared
, чтобы убедиться, что у вас всегда один и тот же экземпляр. -
Управление состоянием на уровне представления: Если вы хотите избежать использования ViewModel Singleton, убедитесь, что состояние управления
@StateObject
используется правильно. Например, вы можете перенести логический флагrecipesLoaded
в ViewModel, и будет достаточно управления состоянием внутри него.
Заключение
Made the necessary changes, следуя приведенным выше рекомендациям, и вы сможете устранить проблему с множественными вызовами функций. Это улучшит производительность вашего приложения, снизит количество запросов к Firestore и предоставит пользователям более гладкий опыт работы с приложением.
Если у вас остались вопросы или вам необходима дополнительная информация, не стесняйтесь обращаться!