Files
e-party-iOS/yana/Views/FeedView.swift
edwinQQQ c8ff40cac1 feat: 更新动态相关数据模型及视图组件
- 在DynamicsModels.swift中为动态响应结构和列表数据添加Sendable协议,提升并发安全性。
- 在FeedListFeature.swift中实现动态内容的加载逻辑,集成API请求以获取最新动态。
- 在FeedListView.swift中新增动态内容列表展示,优化用户交互体验。
- 在MeView.swift中添加设置按钮,支持弹出设置视图。
- 在SettingView.swift中新增COS上传测试功能,允许用户测试图片上传至腾讯云COS。
- 在OptimizedDynamicCardView.swift中实现优化的动态卡片组件,提升动态展示效果。
2025-07-22 17:17:21 +08:00

637 lines
25 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(NSLocalizedString("feed.title", comment: "Enjoy your Life Time"))
.font(.system(size: 22, weight: .semibold))
.foregroundColor(.white)
Spacer()
Button(action: {
// showEditFeed = true //
}) {
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(NSLocalizedString("feed.empty", comment: "No moments yet"))
.font(.system(size: 16))
.foregroundColor(.white.opacity(0.8))
if let error = store.error {
Text(String(format: NSLocalizedString("feed.error", comment: "Error: %@"), 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(NSLocalizedString("feed.retry", comment: "Retry"))
.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(NSLocalizedString("feed.loadingMore", comment: "Loading more..."))
.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
@State private var showEditFeed = false
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)
}
.sheet(isPresented: $showEditFeed) {
EditFeedView(
onDismiss: {
showEditFeed = false
},
store: Store(
initialState: EditFeedFeature.State()
) {
EditFeedFeature()
}
)
}
}
}
}
// 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()
// }
// )
//}