Проблемы с отображением списков в SwiftUI — ForEach не отображает элементы. Почему?

1
9

Я столкнулся с проблемой в своем проекте SwiftUI, где ShoppingListView должен отображать список элементов, загруженных из Firestore. Несмотря на то, что данные, казалось бы, загружаются правильно, списки не отображаются в пользовательском интерфейсе. Ниже приведено подробное объяснение проблемы.

Контекст: Я использую @StateObject для управления ViewModel, которая загружает списки из Firestore. После загрузки списков они назначаются массиву списков, который представляет собой словарь массивов, содержащих ListableItem (перечисление, которое инкапсулирует различные типы элементов, такие как Product и ListTextField).

Проблема: Хотя массив списков, похоже, загружен правильно и содержит ожидаемые данные (что подтверждается операторами печати в консоли), списки не отображаются в пользовательском интерфейсе. Цикл ForEach внутри списка в ShoppingListView, похоже, не перебирает элементы в списках, как следует.

Функция загрузки данных:

Отображение списка:

ViewModel:

Основная функция для отображения списков:

Вывод на консоль:

🔍 Доступный список для отрисовки: 0 - []

🔄 loadLists: Назначены списки. Доступные списки: ["19 agosto", "Premiun 1", "Nueva 1"]

Однако, несмотря на это, представление списка не отображает элементы.

Что я пробовал:

Я подтвердил, что списки содержат ожидаемые данные непосредственно перед отрисовкой. Я упростил структуру представления, удалив сложные конфигурации дизайна, чтобы убедиться в отсутствии визуальных конфликтов. Я убедился, что цикл ForEach перебирает непустой массив. Вопрос: Что может быть причиной того, что элементы в списках не отображаются в представлении? Может ли это быть связано с тем, как настроены ForEach или List, или есть что-то еще, что я мог упустить из виду?

Буду очень признателен за любые предложения или советы.

Заранее спасибо!

private func loadLists() {
    isLoading = true
    print("🔄 loadLists: Starting to load lists.")
    
    guard let uid = Auth.auth().currentUser?.uid else {
        print("❌ loadLists: User UID not found.")
        return
    }

    var combinedLists: [String: [(item: ListableItem, creationDate: Date?)]] = [:]
    
    // Loading data from Firestore...
    // ... (code omitted for brevity)

    self.lists = combinedLists
    self.isLoading = false
    print("🔄 loadLists: Lists assigned. Available lists: \(self.lists.keys)")
}
var body: some View {
    VStack {
        if isLoading {
            ProgressView()
                .progressViewStyle(CircularProgressViewStyle())
                .scaleEffect(2)
        } else {
            List {
                ForEach(Array(lists.keys.sorted()), id: \.self) { listName in
                    NavigationLink(destination: destinationView(for: listName)) {
                        listRow(listName: listName)
                            .background(Color.white)
                            .cornerRadius(10)
                            .shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
                            .padding(.vertical, 5)
                    }
                    .onAppear {
                        print("🔍 Rendering row for \(listName)")
                    }
                }
            }
        }
    }
}
class ListViewModel: ObservableObject {
    @Published var lists: [String: [(item: ListTextField, creationDate: Date?)]] = [:]
    @Published var isLoading: Bool = true
    @Published var errorMessage: ErrorWrapper?

    private var listener: ListenerRegistration?

    init() {
        loadLists()
    }

    func loadLists() {
        guard let uid = Auth.auth().currentUser?.uid else {
            print("❌ loadLists: No se encontró UID del usuario.")
            return
        }

        let userRef = Firestore.firestore().collection("iFoodList").document(uid)
        listener = userRef.addSnapshotListener { documentSnapshot, error in
            self.isLoading = false

            if let error = error {
                self.errorMessage = ErrorWrapper(message: error.localizedDescription)
                print("❌ loadLists: Error al obtener el documento - \(error.localizedDescription)")
                return
            }

            guard let document = documentSnapshot, document.exists, let data = document.data() else {
                print("⚠️ loadLists: El documento no existe o no contiene datos.")
                return
            }

            var newLists: [String: [(item: ListTextField, creationDate: Date?)]] = [:]

            for (listName, listItems) in data {
                if let listData = listItems as? [String: Any] {
                    let list = self.parseListItems(listData)
                    if let creationDate = listData["creationDate"] as? Timestamp {
                        newLists[listName] = list.map { ($0, creationDate.dateValue()) }
                    }
                }
            }

            self.lists = newLists
        }
    }

    private func parseListItems(_ listData: [String: Any]) -> [ListTextField] {
        guard let items = listData["items"] as? [String: [String: Any]] else { return [] }
        var list: [ListTextField] = []

        for (id, itemData) in items {
            let title = itemData["title"] as? String ?? ""
            let subtitle = itemData["subtitle"] as? String ?? ""
            let isChecked = itemData["isChecked"] as? Bool ?? false
            list.append(ListTextField(id: id, title: title, subtitle: subtitle, isChecked: isChecked))
        }

        return list
    }

    func deleteList(at offsets: IndexSet) {
        for index in offsets {
            let listName = Array(lists.keys)[index]
            print("🗑 deleteList: Intentando eliminar la lista '\(listName)'.")
            
            guard let uid = Auth.auth().currentUser?.uid else {
                print("❌ deleteList: No se encontró UID del usuario.")
                return
            }

            let userRef = Firestore.firestore().collection("iFoodList").document(uid)
            userRef.updateData([listName: FieldValue.delete()]) { error in
                if let error = error {
                    self.errorMessage = ErrorWrapper(message: error.localizedDescription)
                    print("❌ deleteList: Error al eliminar la lista '\(listName)' en Firestore - \(error.localizedDescription)")
                } else {
                    self.lists.removeValue(forKey: listName)
                    print("✅ deleteList: Lista '\(listName)' eliminada correctamente.")
                }
            }
        }
    }

    func formattedDate(_ date: Date?) -> String? {
        guard let date = date else { return nil }
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        return formatter.string(from: date)
    }

    func sortedListNames() -> [String] {
        return Array(lists.keys).sorted()
    }

    func items(for listName: String) -> [ListTextField] {
        return lists[listName]?.map { $0.item } ?? []
    }

    func creationDate(for listName: String) -> Date? {
        return lists[listName]?.first?.creationDate
    }
}
   // Vista de fila de lista
    private func listRow(listName: String) -> some View {
        VStack(alignment: .leading) {
            HStack {
                Image(systemName: "list.bullet")
                    .foregroundColor(.blue)
                Text(listName)
                    .font(.headline)
                    .foregroundColor(.black)
                Spacer()
            }
            if let creationDate = lists[listName]?.first?.creationDate {
                Text("Creado el \(formattedDate(creationDate))")
                    .font(.subheadline)
                    .foregroundColor(.gray)
            }
        }
        .padding()
        .onAppear {
            print("🔍 listRow: Renderizando fila para \(listName)")
        }
    }


    private func loadLists() {
        isLoading = true
        print("🔄 loadLists: Inicio de la carga de listas.")
        
        guard let uid = Auth.auth().currentUser?.uid else {
            print("❌ loadLists: No se encontró UID del usuario.")
            return
        }

        let manualRef = Firestore.firestore().collection("iFoodList").document(uid)
        let premiumRef = Firestore.firestore().collection("PaidShoppingLists").document(uid)

        let group = DispatchGroup()

        var combinedLists: [String: [(item: ListableItem, creationDate: Date?)]] = [:]

        group.enter()
        manualRef.getDocument { document, error in
            defer { group.leave() }
            if let error = error {
                self.errorMessage = ErrorWrapper(message: error.localizedDescription)
                print("❌ loadLists: Error al obtener el documento de iFoodList - \(error.localizedDescription)")
                return
            }

            if let document = document, document.exists, let data = document.data() {
                combinedLists.merge(self.parseListData(data, listType: .manual)) { current, _ in current }
                print("🟢 loadLists: Listas manuales cargadas. \(combinedLists.keys)")
            } else {
                print("⚠️ loadLists: El documento de iFoodList no existe o no contiene datos.")
            }
        }

        group.enter()
        premiumRef.getDocument { document, error in
            defer { group.leave() }
            if let error = error {
                self.errorMessage = ErrorWrapper(message: error.localizedDescription)
                print("❌ loadLists: Error al obtener el documento de PaidShoppingLists - \(error.localizedDescription)")
                return
            }

            if let document = document, document.exists, let data = document.data() {
                combinedLists.merge(self.parseListData(data, listType: .premium)) { current, _ in current }
                print("🟢 loadLists: Listas premium cargadas. \(combinedLists.keys)")
            } else {
                print("⚠️ loadLists: El documento de PaidShoppingLists no existe o no contiene datos.")
            }
        }

        group.notify(queue: .main) {
            print("🔄 loadLists: Procesando listas cargadas...")
            self.lists = combinedLists
            self.isLoading = false
            print("🔄 loadLists: Listas asignadas. Listas disponibles: \(self.lists.keys)")

            if self.lists.isEmpty {
                print("⚠️ loadLists: No se cargaron listas.")
            } else {
                print("🔄 loadLists: Listas cargadas exitosamente. Listas disponibles: \(self.lists.keys)")
            }
        }
    }


    private func parseListData(_ data: [String: Any], listType: ListType) -> [String: [(item: ListableItem, creationDate: Date?)]] {
        var parsedLists: [String: [(item: ListableItem, creationDate: Date?)]] = [:]

        for (listName, listItems) in data {
            if let listData = listItems as? [String: Any],
               let creationDate = listData["creationDate"] as? Timestamp {
                var itemsArray: [[String: Any]] = []

                if let items = listData["items"] as? [[String: Any]], !items.isEmpty {
                    itemsArray = items
                } else if let items = listData["items"] as? [String: [String: Any]], !items.isEmpty {
                    itemsArray = Array(items.values)
                }

                let list = itemsArray.compactMap { itemData -> (ListableItem, Date?)? in
                    if listType == .manual {
                        if let title = itemData["title"] as? String {
                            let listItem = ListTextField(
                                id: itemData["id"] as? String ?? UUID().uuidString,
                                title: title,
                                subtitle: itemData["subtitle"] as? String ?? "",
                                isChecked: itemData["isChecked"] as? Bool ?? false
                            )
                            print("🟢 parseListData: Item manual cargado: \(title)")
                            return (.listTextField(listItem), creationDate.dateValue())
                        } else {
                            print("❌ Error: Datos inesperados en 'itemData' para la lista '\(listName)'. Datos: \(itemData)")
                            return nil
                        }
                    } else if listType == .premium {
                        if let productItem = Product(
                            id: itemData["id"] as? String ?? UUID().uuidString,
                            data: itemData
                        ) {
                            print("🟢 parseListData: Producto premium cargado: \(productItem.name_es) / \(productItem.name_en)")
                            return (.product(productItem), creationDate.dateValue())
                        } else {
                            print("❌ Error: Datos inesperados en 'itemData' para la lista '\(listName)'. Datos: \(itemData)")
                            return nil
                        }
                    }
                    return nil
                }

                parsedLists[listName] = list
                print("🟢 parseListData: Lista '\(listName)' cargada exitosamente.")
            } else {
                print("❌ Error: Datos inesperados para la lista '\(listName)'. Datos: \(listItems)")
            }
        }

        return parsedLists
    }


    // Crear nueva lista
    private func createList(_ newListName: String) {
        guard !newListName.isEmpty else {
            print("⚠️ createList: El nombre de la nueva lista está vacío.")
            return
        }
        if lists.keys.contains(newListName) {
            print("⚠️ createList: La lista '\(newListName)' ya existe.")
            return
        }
        let creationDate = Date()
        lists[newListName] = []
        print("✅ createList: Lista '\(newListName)' creada localmente.")

        guard let uid = Auth.auth().currentUser?.uid else {
            print("❌ createList: No se encontró UID del usuario.")
            return
        }

        let userRef = Firestore.firestore().collection("iFoodList").document(uid)

        // Cambiar items a un array vacío en lugar de un diccionario
        userRef.setData([newListName: ["items": [], "creationDate": creationDate]], merge: true) { error in
            if let error = error {
                self.errorMessage = ErrorWrapper(message: error.localizedDescription)
                print("❌ createList: Error al guardar la lista en Firestore - \(error.localizedDescription)")
            } else {
                print("✅ createList: Lista '\(newListName)' guardada en Firestore.")
                self.loadLists()  // Recargar listas después de guardar una nueva
            }
        }
    }

    func deleteList(at offsets: IndexSet) {
        for index in offsets {
            let listName = Array(lists.keys)[index]
            print("🗑 deleteList: Intentando eliminar la lista '\(listName)'.")

            guard let uid = Auth.auth().currentUser?.uid else {
                print("❌ deleteList: No se encontró UID del usuario.")
                return
            }

            let userRef = Firestore.firestore().collection("iFoodList").document(uid)
            userRef.updateData([listName: FieldValue.delete()]) { error in
                if let error = error {
                    self.errorMessage = ErrorWrapper(message: error.localizedDescription)
                    print("❌ deleteList: Error al eliminar la lista '\(listName)' en Firestore - \(error.localizedDescription)")
                } else {
                    self.lists.removeValue(forKey: listName)
                    print("✅ deleteList: Lista '\(listName)' eliminada correctamente.")
                }
            }
        }
    }

    private func destinationView(for listName: String) -> AnyView {
        if let firstItem = lists[listName]?.first?.item {
            switch firstItem {
            case .product(let product):
                print("🔄 destinationView: Lista '\(listName)' es de tipo 'Product'")
                return AnyView(
                    ListThirdCategoriesView(selectedProducts: lists[listName]?.compactMap { item in
                        if case let .product(product) = item.item { return product }
                        return nil
                    } ?? [])
                )
            case .listTextField(let listTextField):
                print("🔄 destinationView: Lista '\(listName)' es de tipo 'ListTextField'")
                return AnyView(
                    NewListView(listName: listName, items: lists[listName]?.compactMap { item in
                        if case let .listTextField(listTextField) = item.item { return listTextField }
                        return nil
                    } ?? [])
                )
            default:
                print("❌ destinationView: Lista '\(listName)' no soportada. Mostrando mensaje de error.")
                return AnyView(Text("Lista no soportada"))
            }
        } else {
            print("⚠️ destinationView: Lista '\(listName)' está vacía. Determinando vista.")
            return AnyView(Text("Lista vacía"))
        }
    }



    // Formato de fecha
    private func formattedDate(_ date: Date) -> String {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        return formatter.string(from: date)
    }
}

//MARK: Otras estructuras

struct FloatingButton: View {
    var action: () -> Void

    var body: some View {
        Button(action: action) {
            Image(systemName: "plus")
                .font(.system(size: 36))
                .foregroundColor(.white)
                .frame(width: 60, height: 60)
                .background(
                    Circle()
                        .fill(Color.blue)
                        .shadow(radius: 10)
                )
        }
        .buttonStyle(PlainButtonStyle())
        .scaleEffect(0.9)
        .animation(.spring())
        .padding(.bottom, 20)
    }
}

struct RefreshableScrollView<Content: View>: View {
    let content: () -> Content
    let onRefresh: () -> Void

    var body: some View {
        ScrollView {
            PullToRefresh(coordinateSpaceName: "pullToRefresh") {
                onRefresh()
            }
            content()
        }
        .coordinateSpace(name: "pullToRefresh")
    }
}

struct PullToRefresh: View {
    let coordinateSpaceName: String
    let onRefresh: () -> Void

    @State private var needRefresh = false

    var body: some View {
        GeometryReader { geo in
            if geo.frame(in: .named(coordinateSpaceName)).midY > 50 {
                Spacer()
                    .onAppear {
                        needRefresh = true
                    }
            } else if geo.frame(in: .named(coordinateSpaceName)).maxY < 10 {
                Spacer()
                    .onAppear {
                        if needRefresh {
                            needRefresh = false
                            onRefresh()
                        }
                    }
            }
            HStack {
                Spacer()
                if needRefresh {
                    ProgressView()
                }
                Spacer()
            }
        }
        .padding(.top, -50)
    }
}


Никандр
Вопрос задан16 марта 2024 г.

1 Ответ

2
Иларион
Ответ получен14 сентября 2024 г.

Ваш ответ

Загрузить файл.