我正在开发一个聊天应用程序,使用 SwiftUI 作为前端,使用 Firebase Firestore 和 Firebase Storage 作为后端服务。我的应用程序有一个功能,用户可以在聊天中共享图像和视频,并且这些媒体项目应显示在单独的图库视图中(
ChatGalleryView
)。但是,我遇到了一个问题,即聊天中共享的图像和视频不会在图库视图中自动更新。
这里简要概述了数据的结构以及我的代码的相关部分:
数据结构:
聊天消息存储在 Firestore 中名为
chats
的集合中,每个聊天都有一个子集合 messages
。
每条消息可以包含文本、图像或视频。图像 URL 和视频 URL 存储在
messages
字段下的 content
文档中,这是一个包含 imageUrl
和 videoUrl
等字段的地图。
尽管在
ChatViewModel
中设置侦听器以在将新图像或视频添加到聊天时获取并更新 imageUrls
和 videoUrls
数组,但 ChatGalleryView
不会反映这些更新自动。
我确保监听器已连接并正确获取数据,因为当共享新图像/视频时,我可以看到 URL 被打印到控制台。然而,这些数据似乎并没有传播到
ChatGalleryView
。
有人遇到过类似的问题或者能发现我可能做错了什么吗?如何确保在聊天中添加新媒体时
ChatGalleryView
自动更新?
´´´
class ChatViewModel: ObservableObject {
private var db = Firestore.firestore()
private var messagesListener: ListenerRegistration?
private var storage = Storage.storage()
private let locationManager = CLLocationManager()
@Published var messages: [ChatMessage] = []
@Published var showingPermissionAlert: Bool = false
@Published var isImagePickerPresented = false
@Published var chats: [ChatModel] = []
@Published var userNickname: String?
@Published var isContactPickerPresented: Bool = false
@Published var replyingToMessage: ChatMessage? = nil
@Published var scrollToMessageId: String?
@Published var imageUrls: [String] = []
@Published var videoUrls: [String] = []
func setupMessagesListener(chatId: String) {
let messagesRef = db.collection("chats").document(chatId).collection("messages")
messagesListener = messagesRef.order(by: "sentAt", descending: false).addSnapshotListener { [weak self] (querySnapshot, error) in
guard let self = self, let snapshot = querySnapshot else {
print("Error listening for message updates: \(error?.localizedDescription ?? "No error")")
return
}
snapshot.documentChanges.forEach { change in
let data = change.document.data()
// Asegúrate de acceder al mapa 'content' para obtener 'imageUrl' y 'videoUrl'
if let content = data["content"] as? [String: Any] {
if let imageUrl = content["imageUrl"] as? String, !imageUrl.isEmpty {
print("Image URL found: \(imageUrl)")
self.updateImageUrls(with: imageUrl, messageId: change.document.documentID, added: change.type == .added)
}
if let videoUrl = content["videoUrl"] as? String, !videoUrl.isEmpty {
print("Video URL found: \(videoUrl)")
self.updateVideoUrls(with: videoUrl, messageId: change.document.documentID, added: change.type == .added)
}
}
}
}
}
private func updateImageUrls(with imageUrl: String, messageId: String, added: Bool) {
DispatchQueue.main.async {
if added, !self.imageUrls.contains(imageUrl) {
print("Adding image URL: \(imageUrl) for message ID: \(messageId)")
self.imageUrls.append(imageUrl)
} else if !added {
print("Removing image URL: \(imageUrl) for message ID: \(messageId)")
self.imageUrls.removeAll { $0 == imageUrl }
}
}
}
private func updateVideoUrls(with videoUrl: String, messageId: String, added: Bool) {
DispatchQueue.main.async {
if added, !self.videoUrls.contains(videoUrl) {
print("Adding video URL: \(videoUrl) for message ID: \(messageId)")
self.videoUrls.append(videoUrl)
} else if !added {
print("Removing video URL: \(videoUrl) for message ID: \(messageId)")
self.videoUrls.removeAll { $0 == videoUrl }
}
}
}
导入 SwiftUI 导入AVKit
结构 ChatGalleryView:查看 {
var chatId: String
@ObservedObject var viewModel: ChatViewModel
enum GalleryTab {
case images, videos
}
@State private var selectedTab: GalleryTab = .images
var body: some View {
VStack {
Picker("Select", selection: $selectedTab) {
Text("Images").tag(GalleryTab.images)
Text("Videos").tag(GalleryTab.videos)
}
.pickerStyle(SegmentedPickerStyle())
.padding()
switch selectedTab {
case .images:
GalleryImagesView(imageUrls: viewModel.imageUrls, viewModel: viewModel, chatId: chatId)
case .videos:
GalleryVideosView(videoUrls: viewModel.videoUrls, viewModel: viewModel, chatId: chatId)
}
Spacer()
}
.background(LinearGradient(gradient: Gradient(colors: [Color.white, Color.blue.opacity(0.3)]), startPoint: .top, endPoint: .bottom))
.navigationBarTitle("Gallery", displayMode: .inline)
.onAppear {
print("Cargando galería con \(viewModel.imageUrls.count) imágenes y \(viewModel.videoUrls.count) vídeos")
print("URLs de imágenes: \(viewModel.imageUrls)")
print("URLs de vídeos: \(viewModel.videoUrls)")
}
}
}
结构GalleryImagesView:视图{ 让 imageUrls: [字符串] @ObservedObject var viewModel: ChatViewModel var chatId:字符串
var body: some View {
ScrollView {
LazyVStack {
ForEach(imageUrls, id: \.self) { imageUrl in
AsyncImage(url: URL(string: imageUrl)) { phase in
switch phase {
case .success(let image):
image.resizable()
.aspectRatio(contentMode: .fit)
default:
ProgressView()
}
}
.contextMenu { // Uso correcto de contextMenu
Button(action: {
viewModel.downloadImage(imageUrl, chatId: chatId)
}) {
Text(NSLocalizedString("Download", comment: "Download action"))
Image(systemName: "arrow.down.circle")
}
Button(action: {
viewModel.deleteImageFromChat(chatId: chatId, imageUrl: imageUrl)
}) {
Text(NSLocalizedString("Delete from Chat", comment: "Delete action"))
Image(systemName: "trash")
}
}
}
}
}
}
}
结构GalleryVideosView:查看{ 让 videoUrls: [字符串] @ObservedObject var viewModel: ChatViewModel var chatId:字符串
var body: some View {
ScrollView {
LazyVStack {
ForEach(videoUrls, id: \.self) { videoUrl in
if let url = URL(string: videoUrl) {
VideoPlayer(player: AVPlayer(url: url))
.frame(height: 200)
.contextMenu { // Uso correcto de contextMenu
Button(action: {
viewModel.downloadVideo(videoUrl, chatId: chatId)
}) {
Text(NSLocalizedString("Download", comment: "Download action"))
Image(systemName: "arrow.down.circle")
}
Button(action: {
viewModel.deleteVideoFromChat(chatId: chatId, videoUrl: videoUrl)
}) {
Text(NSLocalizedString("Delete from Chat", comment: "Delete action"))
Image(systemName: "trash")
}
}
}
}
}
}
}
}
New pictures uodated: [enter image description here][1] [enter image description here][2] [enter image description here][3] [enter image description here][4] [enter image description here][5] [enter image description here][6]
[1]: https://i.stack.imgur.com/2U25n.png
[2]: https://i.stack.imgur.com/f12tF.png
[3]: https://i.stack.imgur.com/4jGj6.png
[4]: https://i.stack.imgur.com/XWChv.png
[5]: https://i.stack.imgur.com/UJajJ.png
[6]: https://i.stack.imgur.com/iCWNq.png
Here you are the view where the StateObject remains:
´´´
import SwiftUI
import UIKit
import FirebaseAuth
import FirebaseFirestore
import AVKit
import PhotosUI
class MessageInputState: ObservableObject {
@Published var messageText: String = ""
@Published var isUploadingVideo: Bool = false
}
struct ChatView: View {
@StateObject var viewModel: ChatViewModel
@EnvironmentObject var userSession: UserSession
var chatId: String
@ObservedObject var messageState = MessageInputState()
@State private var pickerResult: [UIImage] = []
@State private var isImagePickerPresented: Bool = false
@State private var showMoreOptions = false
@State private var videoUrl: URL?
@State private var isVideoPickerPresented: Bool = false
@State private var userName: String = ""
@State private var showAlert: Bool = false
@State private var isAliasModalPresented: Bool = false
@State private var showOptionsMenu = false
@State private var nicknameToAdd: String = ""
@State private var isNicknameDialogPresented: Bool = false
@State private var userNickname: String?
@State private var isAddParticipantViewPresented: Bool = false
@State private var isGalleryViewPresented: Bool = false
@State private var showOptionsView = false
@State private var showingChatOptions = false
var chatGroupName: String = "Chat"
@State var connectedContacts: [String]?
init(chatId: String, userSession: UserSession) {
self.chatId = chatId
self._viewModel = StateObject(wrappedValue: ChatViewModel(userSession: userSession))
self.userNickname = userSession.nickname
if let nickname = userSession.nickname {
self.userName = nickname
} else {
self.userName = NSLocalizedString("unknown", comment: "Default username when unknown")
print(NSLocalizedString("ChatView iniciado con chatId: ", comment: "Log message") + "\(chatId)")
print(NSLocalizedString("Chats disponibles en viewModel: ", comment: "Log message") + "\(viewModel.chats)")
}
}
var body: some View {
NavigationView {
VStack {
ScrollView {
ForEach(viewModel.messages) { message in
Group {
if message.state == .deleted {
Text("Message deleted / Mensaje borrado")
.italic()
.foregroundColor(.gray)
.padding(.horizontal, 10)
.padding(.vertical, 5)
} else {
ChatMessageView(message: message, chatId: chatId)
.padding(.horizontal, 10)
.padding(.vertical, 5)
.environmentObject(viewModel)
.contextMenu {
if message.state != .deleted {
Button(action: {
viewModel.startReplying(to: message)
}) {
Label("Reply", systemImage: "arrowshape.turn.up.left")
}
}
Button(role: .destructive, action: {
viewModel.deleteMessage(chatId: chatId, messageId: message.id ?? "")
}) {
Label("Delete", systemImage: "trash")
}
}
}
}
}
}
replyIndicator.transition(.slide).animation(.easeInOut, value: viewModel.replyingToMessage != nil)
messageInputSection.padding(.top, 5)
}
.navigationTitle(NSLocalizedString("Chat", comment: "Navigation title for chat view"))
.navigationBarTitleDisplayMode(.inline)
.actionSheet(isPresented: $showOptionsMenu) {
ActionSheet(
title: Text(NSLocalizedString("Options", comment: "Options")),
buttons: [
.default(Text(NSLocalizedString("Chat Options", comment: "Chat Options"))) {
// Aquí puedes cambiar el estado para mostrar la vista de opciones del chat
showingChatOptions = true
},
.default(Text(NSLocalizedString("Add Photo", comment: "Add Photo"))) {
self.isImagePickerPresented = true
},
.default(Text(NSLocalizedString("Add Video", comment: "Add Video"))) {
self.isVideoPickerPresented = true
},
.destructive(Text(NSLocalizedString("Leave Chat", comment: "Leave Chat"))) {
showAlert = true
},
.cancel()
]
)
}
.sheet(isPresented: $showingChatOptions) {
ChatOptionsView(chatId: chatId, viewModel: viewModel)
}
.alert(isPresented: $showAlert) {
Alert(
title: Text(NSLocalizedString("Confirm Leave", comment: "Confirm Leave")),
message: Text(NSLocalizedString("Are you sure you want to leave the chat?", comment: "Are you sure you want to leave the chat?")),
primaryButton: .destructive(Text(NSLocalizedString("Leave", comment: "Leave"))) {
// Implementar lógica para salir del chat aquí
},
secondaryButton: .cancel()
)
}
.onAppear {
self.userName = userSession.nickname ?? NSLocalizedString("unknown", comment: "Fallback username when unknown")
viewModel.setupMessagesListener(chatId: chatId)
viewModel.fetchMessages(chatId: chatId)
viewModel.fetchChatParticipants(chatId: chatId) { participants in
self.connectedContacts = participants
}
viewModel.openChat(chatId: chatId)
}
.sheet(isPresented: $showOptionsView) {
ChatOptionsView(chatId: chatId, viewModel: viewModel)
}
.sheet(isPresented: $isImagePickerPresented, onDismiss: handleImageSelection) {
PhotoPicker(pickerResult: $pickerResult, isPresented: $isImagePickerPresented)
}
.sheet(isPresented: $isVideoPickerPresented, onDismiss: handleVideoSelection) {
VideoPicker(videoUrl: $videoUrl, isPresented: $isVideoPickerPresented)
}
.sheet(isPresented: $isAddParticipantViewPresented) {
AddParticipantView(viewModel: viewModel, chatId: chatId)
}
.sheet(isPresented: $isGalleryViewPresented) {
ChatGalleryView(chatId: chatId, viewModel: viewModel)
}
}
}
private func sendMessage() {
print("ChatView - sendMessage - userName: \(userName)")
if userName.isEmpty {
showAlert = true
return
}
let messageText = messageState.messageText.trimmingCharacters(in: .whitespacesAndNewlines)
if !messageText.isEmpty {
let textMessageContent = MessageContent(text: messageText, imageUrl: nil, videoUrl: nil)
// Imprime el valor de replyToMessageId antes de enviar
let replyToMessageId = viewModel.replyingToMessage?.id // Actualizado para usar replyingToMessage
print("Intentando enviar mensaje con replyToMessageId: \(String(describing: replyToMessageId))")
viewModel.sendMessage(chatId: chatId, messageContent: textMessageContent, senderId: Auth.auth().currentUser?.uid ?? "unknown", senderName: userName) { success, error in
if success {
// Imprime para confirmar que el mensaje se envió correctamente
print("Message sent successfully with replyToMessageId: \(String(describing: replyToMessageId))")
// Asegúrate de que el indicador de respuesta en la interfaz de usuario se resetee
self.viewModel.cancelReplying()
// Limpia el campo de texto después de enviar
self.messageState.messageText = ""
// Añadir un mensaje de confirmación aquí si es necesario
print("replyToMessageId se ha reseteado y el mensaje se ha enviado correctamente.")
} else {
print("Error sending message: \(error?.localizedDescription ?? "Error desconocido")")
}
}
} else {
print("No hay texto para enviar.")
}
// Manejar automáticamente la selección de imágenes o vídeos
handleMediaSelection()
}
private func sendUpdatedMessage(messageContent: MessageContent) {
// Utiliza la propiedad replyingToMessage.id si está presente
let replyToMessageId = viewModel.replyingToMessage?.id
viewModel.sendMessage(chatId: chatId, messageContent: messageContent, senderId: Auth.auth().currentUser?.uid ?? "unknown", senderName: userName) { success, error in
if success {
print("Mensaje enviado con éxito con replyToMessageId: \(String(describing: replyToMessageId))")
self.viewModel.cancelReplying() // Resetear el estado de respuesta
} else {
print("Error al enviar mensaje: \(error?.localizedDescription ?? "Error desconocido")")
}
}
}
// Esta función ahora llama directamente a `sendUpdatedMessage`
private func finalizeMessageSending(messageContent: MessageContent) {
sendUpdatedMessage(messageContent: messageContent)
}
func uploadAndSendImages(images: [UIImage]) {
var imageUrls = [String]()
let group = DispatchGroup()
for image in images {
group.enter()
viewModel.uploadImage(image) { uploadedImageUrl in
imageUrls.append(uploadedImageUrl)
group.leave()
}
}
group.notify(queue: .main) {
// Envío de un único mensaje con todas las URLs de las imágenes
let messageContent = MessageContent(text: nil, imageUrl: imageUrls, videoUrl: nil, contact: nil)
self.sendUpdatedMessage(messageContent: messageContent)
}
}
func handleMediaSelection() {
// Manejar la subida y envío de imágenes
if !pickerResult.isEmpty {
uploadAndSendImages(images: pickerResult)
self.pickerResult.removeAll() // Limpiar después de enviar
}
// Manejar la subida y envío de vídeo
if let videoUrl = videoUrl {
uploadVideoAndSendMessage(videoUrl: videoUrl)
}
}
// Maneja la selección de imagen
func handleImageSelection() {
if !pickerResult.isEmpty {
uploadAndSendImages(images: pickerResult)
self.pickerResult.removeAll() // Limpiar después de enviar
}
}
// Maneja la selección de vídeo
func handleVideoSelection() {
guard let videoUrl = videoUrl else { return }
messageState.isUploadingVideo = true // Indicar que la carga ha comenzado
viewModel.uploadVideoAfterConversion(videoUrl) { [self] result in
messageState.isUploadingVideo = false // Resetear el estado de carga independientemente del resultado
switch result {
case .success(let uploadedVideoUrl):
print("Video cargado y disponible en: \(uploadedVideoUrl)")
let videoMessageContent = MessageContent(text: nil, imageUrl: nil, videoUrl: [uploadedVideoUrl])
sendUpdatedMessage(messageContent: videoMessageContent)
self.videoUrl = nil // Limpiar después de enviar
case .failure(let error):
print("Error al cargar el video: \(error.localizedDescription)")
// Manejar el error, por ejemplo, mostrando un mensaje al usuario.
}
}
}
private func uploadImages(images: [UIImage], completion: @escaping ([String]) -> Void) {
var uploadedUrls = [String]()
let uploadGroup = DispatchGroup()
for image in images {
uploadGroup.enter()
viewModel.uploadImage(image) { imageUrl in
print("Imagen subida con URL: \(imageUrl)")
uploadedUrls.append(imageUrl)
uploadGroup.leave()
}
}
uploadGroup.notify(queue: .main) {
completion(uploadedUrls)
}
}
private func uploadVideoAndSendMessage(videoUrl: URL) {
viewModel.uploadVideo(videoUrl) { result in
switch result {
case .success(let uploadedVideoUrl):
print("Video subido con URL: \(uploadedVideoUrl)")
let videoMessageContent = MessageContent(text: nil, imageUrl: nil, videoUrl: [uploadedVideoUrl])
self.sendUpdatedMessage(messageContent: videoMessageContent)
self.videoUrl = nil // Limpiar después de enviar
case .failure(let error):
print("Error al subir el video: \(error.localizedDescription)")
// Manejar el error, por ejemplo, mostrando un mensaje al usuario
}
}
}
private var messageInputSection: some View {
HStack {
Button(action: { showOptionsMenu.toggle() }) {
Image(systemName: "plus.circle.fill")
.resizable()
.frame(width: 30, height: 30)
.foregroundColor(.blue)
}
TextField(NSLocalizedString("Message...", comment: "Placeholder text for message input field"), text: $messageState.messageText)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(10)
.background(Color.white)
.cornerRadius(10)
.shadow(color: Color.cyan.opacity(0.5), radius: 3, x: 0, y: 3)
if messageState.isUploadingVideo {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.padding(10)
} else {
Button(action: sendMessage) {
Image(systemName: "paperplane.fill")
.foregroundColor(.blue)
.padding(10)
}
.disabled(messageState.messageText.isEmpty && !messageState.isUploadingVideo) // Deshabilita el botón si no hay texto o un video se está cargando
}
}
.padding(.horizontal)
}
}
struct ChatMessageView: View {
@EnvironmentObject var viewModel: ChatViewModel
@State private var showingDeleteConfirmation = false
@State private var messageIdToDelete: String?
@State private var selectedVideoUrl: URL?
@State private var showVideoPlayer = false
@State private var selectedImageUrl: URL?
@State private var showImageViewer: Bool = false
let message: ChatMessage
let chatId: String
private var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .short
return formatter
}
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
if message.senderId == Auth.auth().currentUser?.uid {
Spacer()
}
VStack(alignment: .leading, spacing: 4) {
// Aquí, directamente verificas si el mensaje actual es una respuesta usando la lógica adaptada
if let replyingToMessage = viewModel.replyingToMessage, message.id == replyingToMessage.id {
QuoteView(message: replyingToMessage)
.background(Color(red: 0.95, green: 0.95, blue: 0.95)) // Un gris muy suave personalizado
.cornerRadius(8)
.onAppear {
print("Replying to message: \(replyingToMessage.content.text ?? "")")
}
} else {
EmptyView()
.onAppear {
print("This message is not a reply or replyToMessageId is empty.")
}
}
// Nombre del emisor del mensaje actual
Text(message.senderName)
.font(.caption)
.foregroundColor(.secondary)
// Contenido del mensaje actual
messageContentView(message: message)
// Hora de envío del mensaje actual
Text("\(message.sentAt, formatter: dateFormatter)")
.font(.caption2)
.foregroundColor(.gray)
}
.padding(.all, 10)
.background(Color.white.opacity(0.7))
.cornerRadius(10)
.shadow(radius: 1)
if message.senderId != Auth.auth().currentUser?.uid {
Spacer()
}
}
}
.contextMenu {
Button(NSLocalizedString("Reply", comment: "Botón para responder a un mensaje")) {
viewModel.startReplying(to: message)
}
Button(NSLocalizedString("Delete", comment: "Botón para eliminar un mensaje"), role: .destructive) {
messageIdToDelete = message.id
showingDeleteConfirmation = true
}
}
.alert(NSLocalizedString("Are you sure you want to delete this message?", comment: "Confirmación de eliminación de mensaje"), isPresented: $showingDeleteConfirmation) {
Button(NSLocalizedString("Delete", comment: "Confirmar eliminación"), role: .destructive) {
if let messageIdToDelete = messageIdToDelete {
viewModel.deleteMessage(chatId: chatId, messageId: messageIdToDelete)
}
}
Button(NSLocalizedString("Cancel", comment: "Cancelar eliminación"), role: .cancel) { }
}
.sheet(isPresented: $showVideoPlayer) {
if let videoUrl = selectedVideoUrl {
VideoPlayer(player: AVPlayer(url: videoUrl))
.edgesIgnoringSafeArea(.all)
.onDisappear {
AVPlayer(url: videoUrl).pause()
}
}
}
.sheet(isPresented: $showImageViewer) {
if let selectedImageUrl = selectedImageUrl {
AsyncImage(url: selectedImageUrl) { phase in
switch phase {
case .success(let image):
image.resizable()
.aspectRatio(contentMode: .fit)
.edgesIgnoringSafeArea(.all)
default:
ProgressView()
}
}
}
}
}
private func messageContentView(message: ChatMessage) -> some View {
if message.isDeleted {
// Mensaje borrado
return AnyView(
Text("Mensaje borrado / Message deleted")
.foregroundColor(.gray)
.font(.footnote).italic()
.padding(10)
.background(Color.red.opacity(0.3))
.cornerRadius(15)
)
} else {
// Contenido del mensaje no borrado
return AnyView(
VStack(alignment: .leading, spacing: 4) {
// Verificar si el mensaje es una respuesta
if let replyToMessageId = message.replyToMessageId, !replyToMessageId.isEmpty,
let originalMessage = viewModel.messages.first(where: { $0.id == replyToMessageId }) {
QuoteView(message: originalMessage)
.padding(.bottom, 4)
.background(Color(red: 0.95, green: 0.95, blue: 0.95))
.cornerRadius(8)
.padding(.leading, 10)
}
// Contenido del mensaje
messageContentDetails(message: message)
}
)
}
}
@ViewBuilder
private func messageContentDetails(message: ChatMessage) -> some View {
// Texto del mensaje
if let text = message.content.text, !text.isEmpty {
Text(text)
.padding(10)
.background(message.senderId == Auth.auth().currentUser?.uid ? Color.blue : Color.gray)
.foregroundColor(.white)
.cornerRadius(15)
}
// Imágenes del mensaje
if let imageUrls = message.content.imageUrl, !imageUrls.isEmpty {
imageCarousel(imageUrls: imageUrls)
}
// Videos del mensaje
if let videoUrls = message.content.videoUrl, !videoUrls.isEmpty {
videoCarousel(videoUrls: videoUrls)
}
}
@ViewBuilder
private func imageCarousel(imageUrls: [String]) -> some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(imageUrls, id: \.self) { imageUrlString in
if let url = URL(string: imageUrlString) {
Button(action: {
self.selectedImageUrl = url
self.showImageViewer = true
}) {
AsyncImage(url: url) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200, height: 200)
.cornerRadius(15)
case .failure:
Image(systemName: "photo")
@unknown default:
EmptyView()
}
}
}
}
}
}
}
.frame(height: 200) // Altura fija para el carrusel
}
@ViewBuilder
private func videoCarousel(videoUrls: [String]) -> some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(videoUrls, id: \.self) { videoUrlString in
if let url = URL(string: videoUrlString) {
Button(action: {
self.selectedVideoUrl = url
self.showVideoPlayer = true
}) {
VideoThumbnailView(videoUrl: url)
.frame(width: 200, height: 200)
.cornerRadius(15)
}
}
}
}
}
.frame(height: 200) // Altura fija para el carrusel de videos
}
}
´´´