Files
e-party-iOS/yana/Views/FeedView.swift
edwinQQQ ba991598be feat: 更新CreateFeed功能及相关视图组件
- 在CreateFeedFeature中新增isPresented依赖,确保在适当的上下文中执行视图关闭操作。
- 在FeedFeature中优化状态管理,简化CreateFeedView的呈现逻辑。
- 新增FeedListFeature和MainFeature,整合FeedListView和底部导航功能,提升用户体验。
- 更新HomeView和SplashView以集成MainView,确保应用结构一致性。
- 在多个视图中调整状态管理和导航逻辑,增强可维护性和用户体验。
2025-07-21 19:10:31 +08:00

624 lines
24 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
import ComposableArchitecture
struct FeedTopBarView: View {
let store: StoreOf<FeedFeature>
let onShowCreateFeed: () -> Void
var body: some View {
WithPerceptionTracking {
HStack {
Spacer()
Text("Enjoy your Life Time")
.font(.system(size: 22, weight: .semibold))
.foregroundColor(.white)
Spacer()
Button(action: {
onShowCreateFeed() //
}) {
Image("add icon")
.frame(width: 36, height: 36)
}
}
.padding(.horizontal, 20)
}
}
}
struct FeedMomentsListView: View {
let store: StoreOf<FeedFeature>
var body: some View {
WithPerceptionTracking {
LazyVStack(spacing: 16) {
if store.moments.isEmpty {
VStack(spacing: 12) {
Image(systemName: "heart.text.square")
.font(.system(size: 40))
.foregroundColor(.white.opacity(0.6))
Text("暂无动态内容")
.font(.system(size: 16))
.foregroundColor(.white.opacity(0.8))
if let error = store.error {
Text("错误: \(error)")
.font(.system(size: 12))
.foregroundColor(.red.opacity(0.8))
.multilineTextAlignment(.center)
.padding(.horizontal, 20)
}
//
if store.error != nil {
Button(action: {
store.send(.retryLoad)
}) {
Text("重试")
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 8)
.background(Color.blue.opacity(0.8))
.cornerRadius(8)
}
.padding(.top, 8)
}
}
.padding(.top, 40)
} else {
ForEach(Array(store.moments.enumerated()), id: \.element.dynamicId) { index, moment in
WithPerceptionTracking {
Text(moment.avatar)
// OptimizedDynamicCardView(
// moment: moment,
// allMoments: store.moments,
// currentIndex: index
// )
.onAppear {
//
if index == store.moments.count - 1 && store.hasMoreData && !store.isLoading {
store.send(.loadMoreMoments)
}
}
}
}
//
if store.isLoading && !store.moments.isEmpty {
HStack {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
Text("加载更多...")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.8))
}
.padding(.top, 20)
}
}
}
.padding(.horizontal, 16)
.padding(.top, 20) //
}
}
}
struct FeedView: View {
let store: StoreOf<FeedFeature>
let onShowCreateFeed: () -> Void
var body: some View {
WithPerceptionTracking {
GeometryReader { geometry in
ZStack {
// - HomeView
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.clipped()
.ignoresSafeArea(.all)
//
VStack(spacing: 0) {
//
VStack(spacing: 20) {
FeedTopBarView(store: store, onShowCreateFeed: onShowCreateFeed)
Image(systemName: "heart.fill")
.font(.system(size: 60))
.foregroundColor(.red)
.padding(.top, 40)
Text("The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable.")
.font(.system(size: 16))
.multilineTextAlignment(.center)
.foregroundColor(.white.opacity(0.9))
.padding(.horizontal, 30)
.padding(.bottom, 30)
}
// .padding(.top, 60) //
// -
ScrollView {
FeedMomentsListView(store: store)
.padding(.bottom, 20) //
}
.refreshable {
//
await withCheckedContinuation { continuation in
store.send(.refresh)
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
continuation.resume()
}
}
}
}
}
}
.onAppear {
store.send(.onAppear)
}
}
}
}
// MARK: -
struct OptimizedDynamicCardView: View {
let moment: MomentsInfo
let allMoments: [MomentsInfo]
let currentIndex: Int
var body: some View {
WithPerceptionTracking{
VStack(alignment: .leading, spacing: 12) {
//
HStack {
// 使
CachedAsyncImage(url: moment.avatar) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Circle()
.fill(Color.gray.opacity(0.3))
.overlay(
Text(String(moment.nick.prefix(1)))
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
)
}
.frame(width: 40, height: 40)
.clipShape(Circle())
VStack(alignment: .leading, spacing: 2) {
Text(moment.nick)
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
Text(formatTime(moment.publishTime))
.font(.system(size: 12))
.foregroundColor(.white.opacity(0.6))
}
Spacer()
// VIP
if let vipInfo = moment.userVipInfoVO, let vipLevel = vipInfo.vipLevel {
Text("VIP\(vipLevel)")
.font(.system(size: 10, weight: .bold))
.foregroundColor(.yellow)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.yellow.opacity(0.2))
.cornerRadius(4)
}
}
//
if !moment.content.isEmpty {
Text(moment.content)
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.9))
.multilineTextAlignment(.leading)
}
//
if let images = moment.dynamicResList, !images.isEmpty {
OptimizedImageGrid(images: images)
}
//
HStack(spacing: 20) {
Button(action: {}) {
HStack(spacing: 4) {
Image(systemName: "message")
.font(.system(size: 16))
Text("\(moment.commentCount)")
.font(.system(size: 14))
}
.foregroundColor(.white.opacity(0.8))
}
Button(action: {}) {
HStack(spacing: 4) {
Image(systemName: moment.isLike ? "heart.fill" : "heart")
.font(.system(size: 16))
Text("\(moment.likeCount)")
.font(.system(size: 14))
}
.foregroundColor(moment.isLike ? .red : .white.opacity(0.8))
}
Spacer()
}
.padding(.top, 8)
}
}
.padding(16)
.background(
Color.white.opacity(0.1)
.cornerRadius(12)
)
.onAppear {
//
preloadNearbyImages()
}
}
private func formatTime(_ timestamp: Int) -> String {
let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0)
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "zh_CN")
let now = Date()
let interval = now.timeIntervalSince(date)
if interval < 60 {
return "刚刚"
} else if interval < 3600 {
return "\(Int(interval / 60))分钟前"
} else if interval < 86400 {
return "\(Int(interval / 3600))小时前"
} else {
formatter.dateFormat = "MM-dd HH:mm"
return formatter.string(from: date)
}
}
private func preloadNearbyImages() {
var urlsToPreload: [String] = []
// 2
let preloadRange = max(0, currentIndex - 2)...min(allMoments.count - 1, currentIndex + 2)
for index in preloadRange {
let moment = allMoments[index]
//
urlsToPreload.append(moment.avatar)
//
if let images = moment.dynamicResList {
urlsToPreload.append(contentsOf: images.map { $0.resUrl })
}
}
//
ImageCacheManager.shared.preloadImages(urls: urlsToPreload)
}
}
// MARK: -
struct OptimizedImageGrid: View {
let images: [MomentsPicture]
var body: some View {
GeometryReader { geometry in
let availableWidth = max(geometry.size.width, 1) // 0
let spacing: CGFloat = 8
// availableWidth
if availableWidth < 10 {
Color.clear.frame(height: 1)
} else {
switch images.count {
case 1:
//
let imageSize: CGFloat = min(availableWidth * 0.6, 200)
HStack {
Spacer()
SquareImageView(image: images[0], size: imageSize)
Spacer()
}
case 2:
//
let imageSize: CGFloat = (availableWidth - spacing) / 2
HStack(spacing: spacing) {
SquareImageView(image: images[0], size: imageSize)
SquareImageView(image: images[1], size: imageSize)
}
case 3:
//
let imageSize: CGFloat = (availableWidth - spacing * 2) / 3
HStack(spacing: spacing) {
ForEach(images.prefix(3), id: \.id) { image in
SquareImageView(image: image, size: imageSize)
}
}
default:
// 9
let imageSize: CGFloat = (availableWidth - spacing * 2) / 3
let columns = Array(repeating: GridItem(.fixed(imageSize), spacing: spacing), count: 3)
LazyVGrid(columns: columns, spacing: spacing) {
ForEach(images.prefix(9), id: \.id) { image in
SquareImageView(image: image, size: imageSize)
}
}
}
}
}
.frame(height: calculateGridHeight())
}
private func calculateGridHeight() -> CGFloat {
switch images.count {
case 1:
return 200 //
case 2:
return 120 //
case 3:
return 100 //
case 4...6:
return 216 // 2 ( * 2 + )
default:
return 340 // 3 ( * 3 + + )
}
}
}
// MARK: -
struct SquareImageView: View {
let image: MomentsPicture
let size: CGFloat
var body: some View {
let safeSize = size.isFinite && size > 0 ? size : 100 //
CachedAsyncImage(url: image.resUrl) { imageView in
imageView
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Rectangle()
.fill(Color.gray.opacity(0.3))
.overlay(
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6)))
.scaleEffect(0.8)
)
}
.frame(width: safeSize, height: safeSize)
.clipped()
.cornerRadius(8)
}
}
// MARK: -
struct RealDynamicCardView: View {
let moment: MomentsInfo
var body: some View {
VStack(alignment: .leading, spacing: 12) {
//
HStack {
AsyncImage(url: URL(string: moment.avatar)) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Circle()
.fill(Color.gray.opacity(0.3))
.overlay(
Text(String(moment.nick.prefix(1)))
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
)
}
.frame(width: 40, height: 40)
.clipShape(Circle())
VStack(alignment: .leading, spacing: 2) {
Text(moment.nick)
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
Text(formatTime(moment.publishTime))
.font(.system(size: 12))
.foregroundColor(.white.opacity(0.6))
}
Spacer()
// VIP
if let vipInfo = moment.userVipInfoVO, let vipLevel = vipInfo.vipLevel {
Text("VIP\(vipLevel)")
.font(.system(size: 10, weight: .bold))
.foregroundColor(.yellow)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.yellow.opacity(0.2))
.cornerRadius(4)
}
}
//
if !moment.content.isEmpty {
Text(moment.content)
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.9))
.multilineTextAlignment(.leading)
}
//
if let images = moment.dynamicResList, !images.isEmpty {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: min(images.count, 3)), spacing: 8) {
ForEach(images.prefix(9), id: \.id) { image in
AsyncImage(url: URL(string: image.resUrl)) { imageView in
imageView
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Rectangle()
.fill(Color.gray.opacity(0.3))
.overlay(
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6)))
)
}
.frame(height: 100)
.clipped()
.cornerRadius(8)
}
}
}
//
HStack(spacing: 20) {
Button(action: {}) {
HStack(spacing: 4) {
Image(systemName: "message")
.font(.system(size: 16))
Text("\(moment.commentCount)")
.font(.system(size: 14))
}
.foregroundColor(.white.opacity(0.8))
}
Button(action: {}) {
HStack(spacing: 4) {
Image(systemName: moment.isLike ? "heart.fill" : "heart")
.font(.system(size: 16))
Text("\(moment.likeCount)")
.font(.system(size: 14))
}
.foregroundColor(moment.isLike ? .red : .white.opacity(0.8))
}
Spacer()
}
.padding(.top, 8)
}
.padding(16)
.background(
Color.white.opacity(0.1)
.cornerRadius(12)
)
}
private func formatTime(_ timestamp: Int) -> String {
let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0)
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "zh_CN")
let now = Date()
let interval = now.timeIntervalSince(date)
if interval < 60 {
return "刚刚"
} else if interval < 3600 {
return "\(Int(interval / 60))分钟前"
} else if interval < 86400 {
return "\(Int(interval / 3600))小时前"
} else {
formatter.dateFormat = "MM-dd HH:mm"
return formatter.string(from: date)
}
}
}
// MARK: -
struct DynamicCardView: View {
let index: Int
var body: some View {
VStack(alignment: .leading, spacing: 12) {
//
HStack {
Circle()
.fill(Color.gray.opacity(0.3))
.frame(width: 40, height: 40)
.overlay(
Text("U\(index + 1)")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
)
VStack(alignment: .leading, spacing: 2) {
Text("用户\(index + 1)")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
Text("2小时前")
.font(.system(size: 12))
.foregroundColor(.white.opacity(0.6))
}
Spacer()
}
//
Text("今天是美好的一天,分享一些生活中的小确幸。希望大家都能珍惜每一个当下的时刻。")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.9))
.multilineTextAlignment(.leading)
//
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3), spacing: 8) {
ForEach(0..<3) { imageIndex in
Rectangle()
.fill(Color.gray.opacity(0.3))
.aspectRatio(1, contentMode: .fit)
.overlay(
Image(systemName: "photo")
.foregroundColor(.white.opacity(0.6))
)
}
}
//
HStack(spacing: 20) {
Button(action: {}) {
HStack(spacing: 4) {
Image(systemName: "message")
.font(.system(size: 16))
Text("354")
.font(.system(size: 14))
}
.foregroundColor(.white.opacity(0.8))
}
Button(action: {}) {
HStack(spacing: 4) {
Image(systemName: "heart")
.font(.system(size: 16))
Text("354")
.font(.system(size: 14))
}
.foregroundColor(.white.opacity(0.8))
}
Spacer()
}
.padding(.top, 8)
}
.padding(16)
.background(
Color.white.opacity(0.1)
.cornerRadius(12)
)
}
}
//#Preview {
// FeedView(
// store: Store(initialState: FeedFeature.State()) {
// FeedFeature()
// }
// )
//}