Я столкнулся с проблемой в своем проекте 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)
}
}