Compare commits
No commits in common. "3c1f23f44726476eb686ed6816c5aebb0295341a" and "f7c8c1c0a098e8c7a2e7cef76cd9d5c942b32929" have entirely different histories.
3c1f23f447
...
f7c8c1c0a0
@ -1,70 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
// 1. Класс для загрузки изображения с поддержкой кеширования
|
|
||||||
class ImageLoader: ObservableObject {
|
|
||||||
@Published var image: UIImage?
|
|
||||||
|
|
||||||
private var cancellable: AnyCancellable?
|
|
||||||
private let url: URL
|
|
||||||
private let cache = ImageCacheManager.shared
|
|
||||||
|
|
||||||
init(url: URL) {
|
|
||||||
self.url = url
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
cancellable?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
func load() {
|
|
||||||
// Проверяем, есть ли изображение в кеше
|
|
||||||
if let cachedImage = cache.get(forKey: url.absoluteString) {
|
|
||||||
self.image = cachedImage
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если в кеше нет, загружаем из сети
|
|
||||||
cancellable = URLSession.shared.dataTaskPublisher(for: url)
|
|
||||||
.map { UIImage(data: $0.data) }
|
|
||||||
.replaceError(with: nil)
|
|
||||||
.handleEvents(receiveOutput: { [weak self] image in
|
|
||||||
// Сохраняем загруженное изображение в кеш
|
|
||||||
if let image = image, let key = self?.url.absoluteString {
|
|
||||||
self?.cache.set(image, forKey: key)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.assign(to: \.image, on: self)
|
|
||||||
}
|
|
||||||
|
|
||||||
func cancel() {
|
|
||||||
cancellable?.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. View для отображения удаленного изображения
|
|
||||||
struct RemoteImageView: View {
|
|
||||||
@StateObject private var loader: ImageLoader
|
|
||||||
|
|
||||||
init(url: URL) {
|
|
||||||
_loader = StateObject(wrappedValue: ImageLoader(url: url))
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
content
|
|
||||||
.onAppear(perform: loader.load)
|
|
||||||
.onDisappear(perform: loader.cancel)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var content: some View {
|
|
||||||
Group {
|
|
||||||
if let image = loader.image {
|
|
||||||
Image(uiImage: image)
|
|
||||||
.resizable()
|
|
||||||
} else {
|
|
||||||
ProgressView() // Показываем индикатор загрузки
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"data" : [
|
||||||
|
{
|
||||||
|
"filename" : "placeholderVideo.mp4",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
BIN
Shared/Media/placeholderPhoto.jpg
Normal file
BIN
Shared/Media/placeholderPhoto.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
BIN
Shared/Media/placeholderPhotoSquare.png
Normal file
BIN
Shared/Media/placeholderPhotoSquare.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
BIN
Shared/Media/placeholderVideo.mp4
Normal file
BIN
Shared/Media/placeholderVideo.mp4
Normal file
Binary file not shown.
@ -37,8 +37,6 @@ enum MediaType: String, Codable {
|
|||||||
struct MediaItem: Identifiable, Codable {
|
struct MediaItem: Identifiable, Codable {
|
||||||
let id: UUID
|
let id: UUID
|
||||||
let type: MediaType
|
let type: MediaType
|
||||||
let width: Int?
|
|
||||||
let height: Int?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AccessLevel: String, Codable {
|
enum AccessLevel: String, Codable {
|
||||||
|
|||||||
@ -13,52 +13,102 @@ class PostService {
|
|||||||
guard posts.isEmpty else { return }
|
guard posts.isEmpty else { return }
|
||||||
|
|
||||||
let sampleTitles = [
|
let sampleTitles = [
|
||||||
"Обзор TikTok UI", "Мой первый ролик", "Котик в кадре", "SwiftUI мастер-класс",
|
"Обзор TikTok UI",
|
||||||
"Анимации и переходы", "Съёмка с дрона", "Урок по дизайну", "Как сделать свайпы",
|
"Мой первый ролик",
|
||||||
"Лучший UI 2025", "Мой первый ролик", "Котик в кадре", "SwiftUI мастер-класс",
|
"Котик в кадре",
|
||||||
"Анимации и переходы", "Съёмка с дрона", "Урок по дизайну", "Как сделать свайпы",
|
"SwiftUI мастер-класс",
|
||||||
"Лучший UI 2025", "Мой первый ролик", "Котик в кадре", "SwiftUI мастер-класс",
|
"Анимации и переходы",
|
||||||
"Анимации и переходы", "Съёмка с дрона", "Урок по дизайну", "Как сделать свайпы",
|
"Съёмка с дрона",
|
||||||
"Лучший UI 2025", "Мой первый ролик", "Котик в кадре", "SwiftUI мастер-класс",
|
"Урок по дизайну",
|
||||||
"Анимации и переходы", "Съёмка с дрона", "Урок по дизайну", "Как сделать свайпы",
|
"Как сделать свайпы",
|
||||||
"Лучший UI 2025", "Мой первый ролик", "Котик в кадре", "SwiftUI мастер-класс",
|
"Лучший UI 2025",
|
||||||
"Анимации и переходы", "Съёмка с дрона", "Урок по дизайну", "Как сделать свайпы",
|
"Мой первый ролик",
|
||||||
"Лучший UI 2025", "Завершаем проект"
|
"Котик в кадре",
|
||||||
|
"SwiftUI мастер-класс",
|
||||||
|
"Анимации и переходы",
|
||||||
|
"Съёмка с дрона",
|
||||||
|
"Урок по дизайну",
|
||||||
|
"Как сделать свайпы",
|
||||||
|
"Лучший UI 2025",
|
||||||
|
"Мой первый ролик",
|
||||||
|
"Котик в кадре",
|
||||||
|
"SwiftUI мастер-класс",
|
||||||
|
"Анимации и переходы",
|
||||||
|
"Съёмка с дрона",
|
||||||
|
"Урок по дизайну",
|
||||||
|
"Как сделать свайпы",
|
||||||
|
"Лучший UI 2025",
|
||||||
|
"Мой первый ролик",
|
||||||
|
"Котик в кадре",
|
||||||
|
"SwiftUI мастер-класс",
|
||||||
|
"Анимации и переходы",
|
||||||
|
"Съёмка с дрона",
|
||||||
|
"Урок по дизайну",
|
||||||
|
"Как сделать свайпы",
|
||||||
|
"Лучший UI 2025",
|
||||||
|
"Мой первый ролик",
|
||||||
|
"Котик в кадре",
|
||||||
|
"SwiftUI мастер-класс",
|
||||||
|
"Анимации и переходы",
|
||||||
|
"Съёмка с дрона",
|
||||||
|
"Урок по дизайну",
|
||||||
|
"Как сделать свайпы",
|
||||||
|
"Лучший UI 2025",
|
||||||
|
"Завершаем проект"
|
||||||
]
|
]
|
||||||
|
|
||||||
let sampleDescriptions = [
|
let sampleDescriptions = [
|
||||||
"Первый тестовый пост с видео", "Фейковый контент для ленты", "Видео с котиком 🐱",
|
"Первый тестовый пост с видео",
|
||||||
"Интерфейс в стиле TikTok", "Просто тестирую отображение", "Код и UI — любовь",
|
"Фейковый контент для ленты",
|
||||||
"Видео в реальном времени", "Интересный UX пример", "Анимации, переходы, свайпы",
|
"Видео с котиком 🐱",
|
||||||
"Фейковый контент для ленты", "Видео с котиком 🐱", "Интерфейс в стиле TikTok",
|
"Интерфейс в стиле TikTok",
|
||||||
"Просто тестирую отображение", "Код и UI — любовь", "Видео в реальном времени",
|
"Просто тестирую отображение",
|
||||||
"Интересный UX пример", "Анимации, переходы, свайпы", "Фейковый контент для ленты",
|
"Код и UI — любовь",
|
||||||
"Видео с котиком 🐱", "Интерфейс в стиле TikTok", "Просто тестирую отображение",
|
"Видео в реальном времени",
|
||||||
"Код и UI — любовь", "Видео в реальном времени", "Интересный UX пример",
|
"Интересный UX пример",
|
||||||
"Анимации, переходы, свайпы", "Фейковый контент для ленты", "Видео с котиком 🐱",
|
"Анимации, переходы, свайпы",
|
||||||
"Интерфейс в стиле TikTok", "Просто тестирую отображение", "Код и UI — любовь",
|
"Фейковый контент для ленты",
|
||||||
"Видео в реальном времени", "Интересный UX пример", "Анимации, переходы, свайпы",
|
"Видео с котиком 🐱",
|
||||||
|
"Интерфейс в стиле TikTok",
|
||||||
|
"Просто тестирую отображение",
|
||||||
|
"Код и UI — любовь",
|
||||||
|
"Видео в реальном времени",
|
||||||
|
"Интересный UX пример",
|
||||||
|
"Анимации, переходы, свайпы",
|
||||||
|
"Фейковый контент для ленты",
|
||||||
|
"Видео с котиком 🐱",
|
||||||
|
"Интерфейс в стиле TikTok",
|
||||||
|
"Просто тестирую отображение",
|
||||||
|
"Код и UI — любовь",
|
||||||
|
"Видео в реальном времени",
|
||||||
|
"Интересный UX пример",
|
||||||
|
"Анимации, переходы, свайпы",
|
||||||
|
"Фейковый контент для ленты",
|
||||||
|
"Видео с котиком 🐱",
|
||||||
|
"Интерфейс в стиле TikTok",
|
||||||
|
"Просто тестирую отображение",
|
||||||
|
"Код и UI — любовь",
|
||||||
|
"Видео в реальном времени",
|
||||||
|
"Интересный UX пример",
|
||||||
|
"Анимации, переходы, свайпы",
|
||||||
"Вот это да, пост работает!"
|
"Вот это да, пост работает!"
|
||||||
]
|
]
|
||||||
|
|
||||||
for i in 0..<32 {
|
for i in 0..<32 {
|
||||||
let mediaID = UUID()
|
let mediaID = UUID()
|
||||||
|
// let thumbID = Bool.random() ? UUID() : nil
|
||||||
let thumbID = UUID()
|
let thumbID = UUID()
|
||||||
let postID = UUID()
|
let postID = UUID()
|
||||||
|
|
||||||
let authorId = UUID()
|
let authorId = UUID()
|
||||||
let authorUserName = "username_\(Int.random(in: 1...5))"
|
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(
|
let post = Post(
|
||||||
id: postID,
|
id: postID,
|
||||||
title: sampleTitles[i],
|
title: sampleTitles[i],
|
||||||
description: sampleDescriptions[i],
|
description: sampleDescriptions[i],
|
||||||
media: [
|
media: [
|
||||||
MediaItem(id: mediaID, type: .video, width: width, height: height)
|
MediaItem(id: mediaID, type: .video)
|
||||||
],
|
],
|
||||||
mediaOrder: [mediaID],
|
mediaOrder: [mediaID],
|
||||||
thumbnailID: thumbID,
|
thumbnailID: thumbID,
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
import UIKit
|
|
||||||
|
|
||||||
// Менеджер для кеширования изображений в памяти
|
|
||||||
class ImageCacheManager {
|
|
||||||
static let shared = ImageCacheManager()
|
|
||||||
|
|
||||||
// NSCache будет хранить UIImage по ключу NSString (URL)
|
|
||||||
// Он автоматически удаляет объекты при нехватке памяти
|
|
||||||
private let cache = NSCache<NSString, UIImage>()
|
|
||||||
|
|
||||||
private init() {}
|
|
||||||
|
|
||||||
// Добавить изображение в кеш
|
|
||||||
func set(_ image: UIImage, forKey key: String) {
|
|
||||||
cache.setObject(image, forKey: key as NSString)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получить изображение из кеша
|
|
||||||
func get(forKey key: String) -> UIImage? {
|
|
||||||
return cache.object(forKey: key as NSString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,86 +1,32 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import UIKit // Для доступа к UIScreen
|
|
||||||
|
|
||||||
class NewHomeTabViewModel: ObservableObject {
|
class NewHomeTabViewModel: ObservableObject {
|
||||||
@Published var column1Posts: [Post] = []
|
@Published var posts: [Post] = []
|
||||||
@Published var column2Posts: [Post] = []
|
@Published var isLoading = true
|
||||||
@Published var isLoading: Bool = false
|
@Published var isRefreshing = 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() {
|
func fetchDataIfNeeded() {
|
||||||
guard allPosts.isEmpty else { return }
|
guard !hasLoadedData else { return }
|
||||||
fetchData()
|
refreshData()
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshData() {
|
func refreshData() {
|
||||||
isRefreshing = true
|
DispatchQueue.main.async {
|
||||||
fetchData()
|
self.isRefreshing = true
|
||||||
}
|
|
||||||
|
|
||||||
private func fetchData() {
|
|
||||||
if !isRefreshing {
|
|
||||||
isLoading = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
postService.fetchAllPosts { [weak self] posts in
|
PostService.shared.fetchAllPosts { [weak self] fetchedPosts in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.allPosts = posts
|
self.posts = fetchedPosts
|
||||||
self.distributePosts()
|
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
|
self.hasLoadedData = true
|
||||||
self.isRefreshing = false
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,13 @@ import SwiftUI
|
|||||||
struct NewHomeTab: View {
|
struct NewHomeTab: View {
|
||||||
@ObservedObject var viewModel: NewHomeTabViewModel
|
@ObservedObject var viewModel: NewHomeTabViewModel
|
||||||
|
|
||||||
// Ширина колонки теперь вычисляется в ViewModel, но нам она нужна и здесь для PostGridItem
|
private var column1Posts: [Post] {
|
||||||
private let columnWidth = (UIScreen.main.bounds.width - 14) / 2
|
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 }
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
@ -16,13 +21,13 @@ struct NewHomeTab: View {
|
|||||||
}) {
|
}) {
|
||||||
HStack(alignment: .top, spacing: 6) {
|
HStack(alignment: .top, spacing: 6) {
|
||||||
LazyVStack(spacing: 6) {
|
LazyVStack(spacing: 6) {
|
||||||
ForEach(viewModel.column1Posts) { post in
|
ForEach(column1Posts) { post in
|
||||||
PostGridItem(post: post, width: columnWidth)
|
PostGridItem(post: post)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LazyVStack(spacing: 6) {
|
LazyVStack(spacing: 6) {
|
||||||
ForEach(viewModel.column2Posts) { post in
|
ForEach(column2Posts) { post in
|
||||||
PostGridItem(post: post, width: columnWidth)
|
PostGridItem(post: post)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -33,107 +38,77 @@ struct NewHomeTab: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.fetchDataIfNeeded()
|
viewModel.fetchDataIfNeeded()
|
||||||
}
|
}
|
||||||
// .background(Color(.secondarySystemBackground)) // Фон для всей вкладки
|
.background(Color(.secondarySystemBackground)) // Фон для всей вкладки
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PostGridItem: View {
|
struct PostGridItem: View {
|
||||||
let post: Post
|
let post: Post
|
||||||
let width: CGFloat // Ширина элемента
|
|
||||||
|
|
||||||
// Формируем URL для загрузки изображения
|
private var randomHeight: CGFloat {
|
||||||
private var imageURL: URL? {
|
CGFloat.random(in: 150...300)
|
||||||
// Используем picsum.photos для получения уникального изображения для каждого поста
|
|
||||||
// Используем реальные размеры для большей вариативности
|
|
||||||
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 {
|
var body: some View {
|
||||||
NavigationLink(destination: PostDetailView(post: post)) {
|
VStack(alignment: .leading, spacing: 0) { // Убираем отступ между картинкой и текстом
|
||||||
VStack(alignment: .leading, spacing: 0) { // Убираем отступ между картинкой и текстом
|
// 1. Медиа контент
|
||||||
|
if let _ = post.media.first {
|
||||||
// 1. Медиа контент
|
Image("placeholderPhoto")
|
||||||
if let url = imageURL {
|
.resizable()
|
||||||
// Создаем контейнер с четкими границами, чтобы избежать перекрытия
|
.aspectRatio(contentMode: .fill)
|
||||||
Color.clear
|
.frame(height: randomHeight)
|
||||||
.frame(width: width, height: imageHeight) // Используем вычисленную высоту
|
}
|
||||||
.background(
|
|
||||||
RemoteImageView(url: url)
|
// Контейнер для текста, который создает эффект "расширения"
|
||||||
.scaledToFill()
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
)
|
// 2. Название поста
|
||||||
.clipped()
|
if let title = post.title, !title.isEmpty {
|
||||||
|
Text(title)
|
||||||
|
.font(.subheadline)
|
||||||
|
.lineLimit(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Контейнер для текста, который создает эффект "расширения"
|
// 3. Информация об авторе и лайки
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
HStack {
|
||||||
// 2. Название поста
|
|
||||||
if let title = post.title, !title.isEmpty {
|
|
||||||
Text(title)
|
|
||||||
.font(.subheadline)
|
|
||||||
.lineLimit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Информация об авторе и лайки
|
Button(action: {
|
||||||
HStack {
|
// пока ничего не делаем
|
||||||
|
}) {
|
||||||
Button(action: {
|
HStack(spacing: 4) {
|
||||||
print("account \(post.id)")
|
Image(systemName: "person.circle.fill")
|
||||||
// пока ничего не делаем
|
.resizable()
|
||||||
}) {
|
.frame(width: 20, height: 20)
|
||||||
HStack(spacing: 4) {
|
.foregroundColor(.gray)
|
||||||
Image(systemName: "person.circle.fill")
|
Text(post.authorUsername)
|
||||||
.resizable()
|
.font(.footnote)
|
||||||
.frame(width: 20, height: 20)
|
.lineLimit(1)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.primary)
|
||||||
Text(post.authorUsername)
|
|
||||||
.font(.footnote)
|
|
||||||
.lineLimit(1)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
print("like \(post.id)")
|
|
||||||
// пока ничего не делаем
|
|
||||||
}) {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Image(systemName: post.isLikedByCurrentUser ? "heart.fill" : "heart")
|
|
||||||
.foregroundColor(post.isLikedByCurrentUser ? .red : .primary)
|
|
||||||
Text("\(post.likes)")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
// пока ничего не делаем
|
||||||
|
}) {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: post.isLikedByCurrentUser ? "heart.fill" : "heart")
|
||||||
|
.foregroundColor(post.isLikedByCurrentUser ? .red : .primary)
|
||||||
|
Text("\(post.likes)")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
|
||||||
}
|
}
|
||||||
.padding(8)
|
|
||||||
.background(Color(UIColor.systemBackground)) // Фон только для текстовой части
|
|
||||||
}
|
}
|
||||||
.cornerRadius(6) // Закругляем всю карточку
|
.padding(8)
|
||||||
.clipped() // Обрезаем дочерние вью по закругленной форме родителя
|
.background(Color(UIColor.systemBackground)) // Фон только для текстовой части
|
||||||
.shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 2)
|
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle()) // Убираем стандартный стиль кнопки, чтобы не влиять на UI
|
.cornerRadius(6) // Закругляем всю карточку
|
||||||
|
.clipped() // Обрезаем дочерние вью по закругленной форме родителя
|
||||||
|
.shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,12 +30,13 @@
|
|||||||
1A7940F02DF7B7A3002569DA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1A7940F22DF7B7A3002569DA /* Localizable.strings */; };
|
1A7940F02DF7B7A3002569DA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1A7940F22DF7B7A3002569DA /* Localizable.strings */; };
|
||||||
1A79410C2DF7C81D002569DA /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A79410B2DF7C81D002569DA /* RegistrationView.swift */; };
|
1A79410C2DF7C81D002569DA /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A79410B2DF7C81D002569DA /* RegistrationView.swift */; };
|
||||||
1A9B014B2E4BF3CD00887E0B /* NewHomeTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A9B014A2E4BF3CD00887E0B /* NewHomeTab.swift */; };
|
1A9B014B2E4BF3CD00887E0B /* NewHomeTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A9B014A2E4BF3CD00887E0B /* NewHomeTab.swift */; };
|
||||||
|
1A9B01582E4BF50D00887E0B /* placeholderVideo.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 1A9B01572E4BF50D00887E0B /* placeholderVideo.mp4 */; };
|
||||||
|
1A9B015F2E4BF9C000887E0B /* placeholderPhotoSquare.png in Resources */ = {isa = PBXBuildFile; fileRef = 1A9B015D2E4BF9C000887E0B /* placeholderPhotoSquare.png */; };
|
||||||
|
1A9B01602E4BF9C000887E0B /* placeholderPhoto.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 1A9B015E2E4BF9C000887E0B /* placeholderPhoto.jpg */; };
|
||||||
1A9B01662E4BFA3600887E0B /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1A9B01652E4BFA3600887E0B /* Media.xcassets */; };
|
1A9B01662E4BFA3600887E0B /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1A9B01652E4BFA3600887E0B /* Media.xcassets */; };
|
||||||
1A9B016E2E4BFB9000887E0B /* NewHomeTabViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A9B016D2E4BFB9000887E0B /* NewHomeTabViewModel.swift */; };
|
1A9B016E2E4BFB9000887E0B /* NewHomeTabViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A9B016D2E4BFB9000887E0B /* NewHomeTabViewModel.swift */; };
|
||||||
1A9B017C2E4C087F00887E0B /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A9B017B2E4C087F00887E0B /* SideMenuView.swift */; };
|
1A9B017C2E4C087F00887E0B /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A9B017B2E4C087F00887E0B /* SideMenuView.swift */; };
|
||||||
1A9E4FB32E4D6A67002249D6 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A9E4FB22E4D6A67002249D6 /* ThemeManager.swift */; };
|
1A9E4FB32E4D6A67002249D6 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A9E4FB22E4D6A67002249D6 /* ThemeManager.swift */; };
|
||||||
1A9E4FD72E4E47EF002249D6 /* RemoteImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A9E4FD62E4E47EF002249D6 /* RemoteImageView.swift */; };
|
|
||||||
1A9E4FDF2E4E4AA1002249D6 /* ImageCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A9E4FDE2E4E4AA1002249D6 /* ImageCacheManager.swift */; };
|
|
||||||
1AB4F8CD2E22E341002B6E40 /* AccountShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB4F8CC2E22E341002B6E40 /* AccountShareSheet.swift */; };
|
1AB4F8CD2E22E341002B6E40 /* AccountShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB4F8CC2E22E341002B6E40 /* AccountShareSheet.swift */; };
|
||||||
1AB4F8F32E22EC9F002B6E40 /* FollowersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB4F8F22E22EC9F002B6E40 /* FollowersView.swift */; };
|
1AB4F8F32E22EC9F002B6E40 /* FollowersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB4F8F22E22EC9F002B6E40 /* FollowersView.swift */; };
|
||||||
1AB4F8F72E22ECAC002B6E40 /* FollowingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB4F8F62E22ECAC002B6E40 /* FollowingView.swift */; };
|
1AB4F8F72E22ECAC002B6E40 /* FollowingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB4F8F62E22ECAC002B6E40 /* FollowingView.swift */; };
|
||||||
@ -84,12 +85,13 @@
|
|||||||
1A7940F72DF7B7EC002569DA /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
|
1A7940F72DF7B7EC002569DA /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
1A79410B2DF7C81D002569DA /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = "<group>"; };
|
1A79410B2DF7C81D002569DA /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = "<group>"; };
|
||||||
1A9B014A2E4BF3CD00887E0B /* NewHomeTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewHomeTab.swift; sourceTree = "<group>"; };
|
1A9B014A2E4BF3CD00887E0B /* NewHomeTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewHomeTab.swift; sourceTree = "<group>"; };
|
||||||
|
1A9B01572E4BF50D00887E0B /* placeholderVideo.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; name = placeholderVideo.mp4; path = ../../../../Volumes/Untitled/xcode/volnahub/Shared/Media/placeholderVideo.mp4; sourceTree = DEVELOPER_DIR; };
|
||||||
|
1A9B015D2E4BF9C000887E0B /* placeholderPhotoSquare.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = placeholderPhotoSquare.png; path = ../../../../Volumes/Untitled/xcode/volnahub/Shared/Media/placeholderPhotoSquare.png; sourceTree = DEVELOPER_DIR; };
|
||||||
|
1A9B015E2E4BF9C000887E0B /* placeholderPhoto.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = placeholderPhoto.jpg; path = ../../../../Volumes/Untitled/xcode/volnahub/Shared/Media/placeholderPhoto.jpg; sourceTree = DEVELOPER_DIR; };
|
||||||
1A9B01652E4BFA3600887E0B /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = "<group>"; };
|
1A9B01652E4BFA3600887E0B /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = "<group>"; };
|
||||||
1A9B016D2E4BFB9000887E0B /* NewHomeTabViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewHomeTabViewModel.swift; sourceTree = "<group>"; };
|
1A9B016D2E4BFB9000887E0B /* NewHomeTabViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewHomeTabViewModel.swift; sourceTree = "<group>"; };
|
||||||
1A9B017B2E4C087F00887E0B /* SideMenuView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
|
1A9B017B2E4C087F00887E0B /* SideMenuView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
|
||||||
1A9E4FB22E4D6A67002249D6 /* ThemeManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
|
1A9E4FB22E4D6A67002249D6 /* ThemeManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
|
||||||
1A9E4FD62E4E47EF002249D6 /* RemoteImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteImageView.swift; sourceTree = "<group>"; };
|
|
||||||
1A9E4FDE2E4E4AA1002249D6 /* ImageCacheManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCacheManager.swift; sourceTree = "<group>"; };
|
|
||||||
1AB4F8CC2E22E341002B6E40 /* AccountShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountShareSheet.swift; sourceTree = "<group>"; };
|
1AB4F8CC2E22E341002B6E40 /* AccountShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountShareSheet.swift; sourceTree = "<group>"; };
|
||||||
1AB4F8F22E22EC9F002B6E40 /* FollowersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersView.swift; sourceTree = "<group>"; };
|
1AB4F8F22E22EC9F002B6E40 /* FollowersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersView.swift; sourceTree = "<group>"; };
|
||||||
1AB4F8F62E22ECAC002B6E40 /* FollowingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingView.swift; sourceTree = "<group>"; };
|
1AB4F8F62E22ECAC002B6E40 /* FollowingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingView.swift; sourceTree = "<group>"; };
|
||||||
@ -227,7 +229,6 @@
|
|||||||
1A7940E52DF7B341002569DA /* Services */ = {
|
1A7940E52DF7B341002569DA /* Services */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
1A9E4FDE2E4E4AA1002249D6 /* ImageCacheManager.swift */,
|
|
||||||
1A9E4FB22E4D6A67002249D6 /* ThemeManager.swift */,
|
1A9E4FB22E4D6A67002249D6 /* ThemeManager.swift */,
|
||||||
1A7940E12DF7B1C5002569DA /* KeychainService.swift */,
|
1A7940E12DF7B1C5002569DA /* KeychainService.swift */,
|
||||||
);
|
);
|
||||||
@ -246,6 +247,9 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
1A9B01652E4BFA3600887E0B /* Media.xcassets */,
|
1A9B01652E4BFA3600887E0B /* Media.xcassets */,
|
||||||
|
1A9B015E2E4BF9C000887E0B /* placeholderPhoto.jpg */,
|
||||||
|
1A9B015D2E4BF9C000887E0B /* placeholderPhotoSquare.png */,
|
||||||
|
1A9B01572E4BF50D00887E0B /* placeholderVideo.mp4 */,
|
||||||
);
|
);
|
||||||
path = Media;
|
path = Media;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -253,7 +257,6 @@
|
|||||||
1AB7F5132E32EBF1003756F3 /* Components */ = {
|
1AB7F5132E32EBF1003756F3 /* Components */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
1A9E4FD62E4E47EF002249D6 /* RemoteImageView.swift */,
|
|
||||||
1AB7F5142E32EC1C003756F3 /* RefreshableScrollView.swift */,
|
1AB7F5142E32EC1C003756F3 /* RefreshableScrollView.swift */,
|
||||||
1AB7F5152E32EC1C003756F3 /* TopBarView.swift */,
|
1AB7F5152E32EC1C003756F3 /* TopBarView.swift */,
|
||||||
);
|
);
|
||||||
@ -399,9 +402,12 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
1A9B01582E4BF50D00887E0B /* placeholderVideo.mp4 in Resources */,
|
||||||
1A7940912DF77BC3002569DA /* Assets.xcassets in Resources */,
|
1A7940912DF77BC3002569DA /* Assets.xcassets in Resources */,
|
||||||
1A7940F02DF7B7A3002569DA /* Localizable.strings in Resources */,
|
1A7940F02DF7B7A3002569DA /* Localizable.strings in Resources */,
|
||||||
|
1A9B01602E4BF9C000887E0B /* placeholderPhoto.jpg in Resources */,
|
||||||
1A9B01662E4BFA3600887E0B /* Media.xcassets in Resources */,
|
1A9B01662E4BFA3600887E0B /* Media.xcassets in Resources */,
|
||||||
|
1A9B015F2E4BF9C000887E0B /* placeholderPhotoSquare.png in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -423,12 +429,10 @@
|
|||||||
1AEE5EAB2E21A83200A3DCA3 /* HomeTab.swift in Sources */,
|
1AEE5EAB2E21A83200A3DCA3 /* HomeTab.swift in Sources */,
|
||||||
1ACE60F82E22F3DC00B37AC5 /* SettingsView.swift in Sources */,
|
1ACE60F82E22F3DC00B37AC5 /* SettingsView.swift in Sources */,
|
||||||
1AD757CD2E27608C0069C1FD /* PostFeedView.swift in Sources */,
|
1AD757CD2E27608C0069C1FD /* PostFeedView.swift in Sources */,
|
||||||
1A9E4FD72E4E47EF002249D6 /* RemoteImageView.swift in Sources */,
|
|
||||||
1A9B017C2E4C087F00887E0B /* SideMenuView.swift in Sources */,
|
1A9B017C2E4C087F00887E0B /* SideMenuView.swift in Sources */,
|
||||||
1A7940C62DF7A98E002569DA /* ContactsTab.swift in Sources */,
|
1A7940C62DF7A98E002569DA /* ContactsTab.swift in Sources */,
|
||||||
1ACE61092E22F57100B37AC5 /* AppPreferencesView.swift in Sources */,
|
1ACE61092E22F57100B37AC5 /* AppPreferencesView.swift in Sources */,
|
||||||
1ACE61052E22F56800B37AC5 /* SecuritySettingsView.swift in Sources */,
|
1ACE61052E22F56800B37AC5 /* SecuritySettingsView.swift in Sources */,
|
||||||
1A9E4FDF2E4E4AA1002249D6 /* ImageCacheManager.swift in Sources */,
|
|
||||||
1A9B016E2E4BFB9000887E0B /* NewHomeTabViewModel.swift in Sources */,
|
1A9B016E2E4BFB9000887E0B /* NewHomeTabViewModel.swift in Sources */,
|
||||||
1A7940E72DF7B5E5002569DA /* SplashScreenView.swift in Sources */,
|
1A7940E72DF7B5E5002569DA /* SplashScreenView.swift in Sources */,
|
||||||
1A7940B02DF77E26002569DA /* LoginView.swift in Sources */,
|
1A7940B02DF77E26002569DA /* LoginView.swift in Sources */,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user