diff --git a/Shared/Models/Post.swift b/Shared/Models/Post.swift index d30f321..6799afc 100644 --- a/Shared/Models/Post.swift +++ b/Shared/Models/Post.swift @@ -37,6 +37,8 @@ enum MediaType: String, Codable { struct MediaItem: Identifiable, Codable { let id: UUID let type: MediaType + let width: Int? + let height: Int? } enum AccessLevel: String, Codable { diff --git a/Shared/Network/PostService.swift b/Shared/Network/PostService.swift index 9a0309a..3622548 100644 --- a/Shared/Network/PostService.swift +++ b/Shared/Network/PostService.swift @@ -13,102 +13,52 @@ class PostService { guard posts.isEmpty else { return } let sampleTitles = [ - "Обзор TikTok UI", - "Мой первый ролик", - "Котик в кадре", - "SwiftUI мастер-класс", - "Анимации и переходы", - "Съёмка с дрона", - "Урок по дизайну", - "Как сделать свайпы", - "Лучший UI 2025", - "Мой первый ролик", - "Котик в кадре", - "SwiftUI мастер-класс", - "Анимации и переходы", - "Съёмка с дрона", - "Урок по дизайну", - "Как сделать свайпы", - "Лучший UI 2025", - "Мой первый ролик", - "Котик в кадре", - "SwiftUI мастер-класс", - "Анимации и переходы", - "Съёмка с дрона", - "Урок по дизайну", - "Как сделать свайпы", - "Лучший UI 2025", - "Мой первый ролик", - "Котик в кадре", - "SwiftUI мастер-класс", - "Анимации и переходы", - "Съёмка с дрона", - "Урок по дизайну", - "Как сделать свайпы", - "Лучший UI 2025", - "Мой первый ролик", - "Котик в кадре", - "SwiftUI мастер-класс", - "Анимации и переходы", - "Съёмка с дрона", - "Урок по дизайну", - "Как сделать свайпы", - "Лучший UI 2025", - "Завершаем проект" + "Обзор TikTok UI", "Мой первый ролик", "Котик в кадре", "SwiftUI мастер-класс", + "Анимации и переходы", "Съёмка с дрона", "Урок по дизайну", "Как сделать свайпы", + "Лучший UI 2025", "Мой первый ролик", "Котик в кадре", "SwiftUI мастер-класс", + "Анимации и переходы", "Съёмка с дрона", "Урок по дизайну", "Как сделать свайпы", + "Лучший UI 2025", "Мой первый ролик", "Котик в кадре", "SwiftUI мастер-класс", + "Анимации и переходы", "Съёмка с дрона", "Урок по дизайну", "Как сделать свайпы", + "Лучший UI 2025", "Мой первый ролик", "Котик в кадре", "SwiftUI мастер-класс", + "Анимации и переходы", "Съёмка с дрона", "Урок по дизайну", "Как сделать свайпы", + "Лучший UI 2025", "Мой первый ролик", "Котик в кадре", "SwiftUI мастер-класс", + "Анимации и переходы", "Съёмка с дрона", "Урок по дизайну", "Как сделать свайпы", + "Лучший UI 2025", "Завершаем проект" ] let sampleDescriptions = [ - "Первый тестовый пост с видео", - "Фейковый контент для ленты", - "Видео с котиком 🐱", - "Интерфейс в стиле TikTok", - "Просто тестирую отображение", - "Код и UI — любовь", - "Видео в реальном времени", - "Интересный UX пример", - "Анимации, переходы, свайпы", - "Фейковый контент для ленты", - "Видео с котиком 🐱", - "Интерфейс в стиле TikTok", - "Просто тестирую отображение", - "Код и UI — любовь", - "Видео в реальном времени", - "Интересный UX пример", - "Анимации, переходы, свайпы", - "Фейковый контент для ленты", - "Видео с котиком 🐱", - "Интерфейс в стиле TikTok", - "Просто тестирую отображение", - "Код и UI — любовь", - "Видео в реальном времени", - "Интересный UX пример", - "Анимации, переходы, свайпы", - "Фейковый контент для ленты", - "Видео с котиком 🐱", - "Интерфейс в стиле TikTok", - "Просто тестирую отображение", - "Код и UI — любовь", - "Видео в реальном времени", - "Интересный UX пример", - "Анимации, переходы, свайпы", + "Первый тестовый пост с видео", "Фейковый контент для ленты", "Видео с котиком 🐱", + "Интерфейс в стиле TikTok", "Просто тестирую отображение", "Код и UI — любовь", + "Видео в реальном времени", "Интересный UX пример", "Анимации, переходы, свайпы", + "Фейковый контент для ленты", "Видео с котиком 🐱", "Интерфейс в стиле TikTok", + "Просто тестирую отображение", "Код и UI — любовь", "Видео в реальном времени", + "Интересный UX пример", "Анимации, переходы, свайпы", "Фейковый контент для ленты", + "Видео с котиком 🐱", "Интерфейс в стиле TikTok", "Просто тестирую отображение", + "Код и UI — любовь", "Видео в реальном времени", "Интересный UX пример", + "Анимации, переходы, свайпы", "Фейковый контент для ленты", "Видео с котиком 🐱", + "Интерфейс в стиле TikTok", "Просто тестирую отображение", "Код и UI — любовь", + "Видео в реальном времени", "Интересный UX пример", "Анимации, переходы, свайпы", "Вот это да, пост работает!" ] for i in 0..<32 { let mediaID = UUID() -// let thumbID = Bool.random() ? UUID() : nil let thumbID = UUID() let postID = UUID() - let authorId = UUID() let authorUserName = "username_\(Int.random(in: 1...5))" -// let authorId = "user_\(Int.random(in: 1...5))" + + // Генерируем случайные размеры для изображения + let width = 1080 // Стандартная ширина + let randomRatio = Double.random(in: 1.0...1.77) // от 1:1 до ~9:16 + let height = Int(Double(width) * randomRatio) + let post = Post( id: postID, title: sampleTitles[i], description: sampleDescriptions[i], media: [ - MediaItem(id: mediaID, type: .video) + MediaItem(id: mediaID, type: .video, width: width, height: height) ], mediaOrder: [mediaID], thumbnailID: thumbID, diff --git a/Shared/ViewModels/NewHomeTabViewModel.swift b/Shared/ViewModels/NewHomeTabViewModel.swift index 5fe5284..3b6b6cb 100644 --- a/Shared/ViewModels/NewHomeTabViewModel.swift +++ b/Shared/ViewModels/NewHomeTabViewModel.swift @@ -1,32 +1,86 @@ import Foundation import Combine +import UIKit // Для доступа к UIScreen class NewHomeTabViewModel: ObservableObject { - @Published var posts: [Post] = [] - @Published var isLoading = true - @Published var isRefreshing = false + @Published var column1Posts: [Post] = [] + @Published var column2Posts: [Post] = [] + @Published var isLoading: Bool = false + @Published var isRefreshing: Bool = false + + private var allPosts: [Post] = [] + private let postService = PostService.shared + + // Рассчитываем ширину колонки один раз + private let columnWidth = (UIScreen.main.bounds.width - 14) / 2 - private var hasLoadedData = false - func fetchDataIfNeeded() { - guard !hasLoadedData else { return } - refreshData() + guard allPosts.isEmpty else { return } + fetchData() } func refreshData() { - DispatchQueue.main.async { - self.isRefreshing = true + isRefreshing = true + fetchData() + } + + private func fetchData() { + if !isRefreshing { + isLoading = true } - PostService.shared.fetchAllPosts { [weak self] fetchedPosts in + postService.fetchAllPosts { [weak self] posts in guard let self = self else { return } DispatchQueue.main.async { - self.posts = fetchedPosts + self.allPosts = posts + self.distributePosts() self.isLoading = false - self.hasLoadedData = true self.isRefreshing = false } } } + + private func distributePosts() { + var tempColumn1Posts: [Post] = [] + var tempColumn2Posts: [Post] = [] + var column1Height: CGFloat = 0 + var column2Height: CGFloat = 0 + + for post in allPosts { + let postHeight = calculatePostHeight(for: post) + + // Добавляем пост в более короткую колонку + if column1Height <= column2Height { + tempColumn1Posts.append(post) + column1Height += postHeight + } else { + tempColumn2Posts.append(post) + column2Height += postHeight + } + } + + self.column1Posts = tempColumn1Posts + self.column2Posts = tempColumn2Posts + } + + private func calculatePostHeight(for post: Post) -> CGFloat { + guard let media = post.media.first, + let width = media.width, + let height = media.height, + width > 0 else { + // Возвращаем стандартную высоту для постов без медиа или с неверными данными + return columnWidth + } + + // Рассчитываем высоту изображения на основе его пропорций + let aspectRatio = CGFloat(height) / CGFloat(width) + let imageHeight = columnWidth * aspectRatio + + // Здесь можно добавить примерную высоту для текста, если нужно + // Например, + 50-70 поинтов для заголовка, автора и кнопок + // Для простоты пока будем ориентироваться только на высоту картинки + return imageHeight + } } + diff --git a/Shared/Views/Tab/NewHomeTab.swift b/Shared/Views/Tab/NewHomeTab.swift index 2f07e2a..a9f66ca 100644 --- a/Shared/Views/Tab/NewHomeTab.swift +++ b/Shared/Views/Tab/NewHomeTab.swift @@ -3,16 +3,7 @@ import SwiftUI struct NewHomeTab: View { @ObservedObject var viewModel: NewHomeTabViewModel - private var column1Posts: [Post] { - viewModel.posts.enumerated().filter { $0.offset % 2 == 0 }.map { $0.element } - } - - private var column2Posts: [Post] { - viewModel.posts.enumerated().filter { $0.offset % 2 != 0 }.map { $0.element } - } - - // Рассчитываем ширину колонки на основе ширины экрана. - // Отступы: 4 слева + 4 справа + 6 между колонками = 14. + // Ширина колонки теперь вычисляется в ViewModel, но нам она нужна и здесь для PostGridItem private let columnWidth = (UIScreen.main.bounds.width - 14) / 2 var body: some View { @@ -25,12 +16,12 @@ struct NewHomeTab: View { }) { HStack(alignment: .top, spacing: 6) { LazyVStack(spacing: 6) { - ForEach(column1Posts) { post in + ForEach(viewModel.column1Posts) { post in PostGridItem(post: post, width: columnWidth) } } LazyVStack(spacing: 6) { - ForEach(column2Posts) { post in + ForEach(viewModel.column2Posts) { post in PostGridItem(post: post, width: columnWidth) } } @@ -42,7 +33,7 @@ struct NewHomeTab: View { .onAppear { viewModel.fetchDataIfNeeded() } - .background(Color(.secondarySystemBackground)) // Фон для всей вкладки +// .background(Color(.secondarySystemBackground)) // Фон для всей вкладки } } @@ -53,16 +44,34 @@ struct PostGridItem: View { // Формируем URL для загрузки изображения private var imageURL: URL? { // Используем picsum.photos для получения уникального изображения для каждого поста - URL(string: "https://picsum.photos/seed/\(post.id.uuidString)/400/400") + // Используем реальные размеры для большей вариативности + if let media = post.media.first, let w = media.width, let h = media.height { + return URL(string: "https://picsum.photos/seed/\(post.id.uuidString)/\(w)/\(h)") + } + return URL(string: "https://picsum.photos/seed/\(post.id.uuidString)/400/400") + } + + // Вычисляем высоту изображения на основе данных из модели + private var imageHeight: CGFloat { + guard let media = post.media.first, + let mediaWidth = media.width, + let mediaHeight = media.height, + mediaWidth > 0 else { + return width // Возвращаем 1:1, если данных нет + } + + let aspectRatio = CGFloat(mediaHeight) / CGFloat(mediaWidth) + return width * aspectRatio } var body: some View { VStack(alignment: .leading, spacing: 0) { // Убираем отступ между картинкой и текстом + // 1. Медиа контент if let url = imageURL { // Создаем контейнер с четкими границами, чтобы избежать перекрытия Color.clear - .frame(width: width, height: width) + .frame(width: width, height: imageHeight) // Используем вычисленную высоту .background( RemoteImageView(url: url) .scaledToFill()