Множественные вызовы функций в SwiftUI

Вопрос или проблема

Я работаю над приложением для рецептов. На экране я хочу иметь представление с “Рецептами дня”, которое просто показывает рецепты в базе данных (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 будет вызывать ваш код опять.

Решение проблемы

Рассмотрим несколько подходов для решения вашей проблемы с многократными вызовами функции загрузки рецептов.

  1. Используйте метод 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")
           }
       }
    }
  2. Создание 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, чтобы убедиться, что у вас всегда один и тот же экземпляр.

  3. Управление состоянием на уровне представления: Если вы хотите избежать использования ViewModel Singleton, убедитесь, что состояние управления @StateObject используется правильно. Например, вы можете перенести логический флаг recipesLoaded в ViewModel, и будет достаточно управления состоянием внутри него.

Заключение

Made the necessary changes, следуя приведенным выше рекомендациям, и вы сможете устранить проблему с множественными вызовами функций. Это улучшит производительность вашего приложения, снизит количество запросов к Firestore и предоставит пользователям более гладкий опыт работы с приложением.

Если у вас остались вопросы или вам необходима дополнительная информация, не стесняйтесь обращаться!

Оцените материал
Добавить комментарий

Капча загружается...