feat: 更新动态功能,新增我的动态视图及相关状态管理

- 在HomeFeature中添加MeDynamicFeature以管理用户动态状态。
- 在MainFeature中集成MeDynamicFeature,支持动态内容的加载与展示。
- 新增MeDynamicView以展示用户的动态列表,支持下拉刷新和上拉加载更多功能。
- 更新MeView以集成用户动态视图,提升用户体验。
- 在APIEndpoints中新增getMyDynamic端点以支持获取用户动态信息。
- 更新DynamicsModels以适应新的动态数据结构,确保数据解析的准确性。
- 在OptimizedDynamicCardView中优化图片处理逻辑,提升动态展示效果。
- 更新相关视图组件以支持动态内容的展示与交互,增强用户体验。
This commit is contained in:
edwinQQQ
2025-07-23 19:17:49 +08:00
parent 3a68270ca9
commit 8b09653c4c
15 changed files with 1097 additions and 359 deletions

View File

@@ -4,33 +4,38 @@ globs:
alwaysApply: true
---
# CONTEXT
I wish to receive advice using the latest tools and seek step-by-step guidance to fully understand the implementation process.
# OBJECTIVE
I wish to receive advice using the latest tools and seek step-by-step guidance to fully understand the implementation process.
## OBJECTIVE
As an expert AI programming assistant, your task is to provide me with clear and readable SwiftUI code. You should:
- Utilize the latest versions of SwiftUI and Swift, being familiar with the newest features and best practices.
- Provide careful and accurate answers that are well-founded and thoughtfully considered.
- **Explicitly use the Chain-of-Thought (CoT) method in your reasoning and answers, explaining your thought process step by step.**
- Strictly adhere to my requirements and meticulously complete the tasks.
- Begin by outlining your proposed approach with detailed steps or pseudocode.
- Upon confirming the plan, proceed to write the code.
# STYLE
- Keep answers concise and direct, minimizing unnecessary wording.
- Emphasize code readability over performance optimization.
- Maintain a professional and supportive tone, ensuring clarity of content.
- Utilize the latest versions of SwiftUI and Swift, being familiar with the newest features and best practices.
- Provide careful and accurate answers that are well-founded and thoughtfully considered.
- **Explicitly use the Chain-of-Thought (CoT) method in your reasoning and answers, explaining your thought process step by step.**
- Strictly adhere to my requirements and meticulously complete the tasks.
- Begin by outlining your proposed approach with detailed steps or pseudocode.
- Upon confirming the plan, proceed to write the code.
## STYLE
- Keep answers concise and direct, minimizing unnecessary wording.
- Emphasize code readability over performance optimization.
- Maintain a professional and supportive tone, ensuring clarity of content.
# AUDIENCE
## AUDIENCE
The target audience is me, a native Chinese developer eager to learn Swift 6 and Xcode 15.9, seeking guidance and advice on utilizing the latest technologies.
# RESPONSE FORMAT
- **Utilize the Chain-of-Thought (CoT) method to reason and respond, explaining your thought process step by step.**
- The reply should include:
## RESPONSE FORMAT
- **Utilize the Chain-of-Thought (CoT) method to reason and respond, explaining your thought process step by step.**
- The reply should include:
1. **Step-by-Step Plan**: Describe the implementation process with detailed pseudocode or step-by-step explanations, showcasing your thought process.
2. **Code Implementation**: Provide correct, up-to-date, error-free, fully functional, runnable, secure, and efficient code. The code should:
- Include all necessary imports and properly name key components.
- Fully implement all requested features, leaving no to-dos, placeholders, or omissions.
3. **Concise Response**: Minimize unnecessary verbosity, focusing only on essential information.
- If a correct answer may not exist, please point it out. If you do not know the answer, please honestly inform me rather than guessing.
- If a correct answer may not exist, please point it out. If you do not know the answer, please honestly inform me rather than guessing.

View File

@@ -583,14 +583,16 @@
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = EKM7RAGNA6;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stupidmonkey.yanaAPITests;
PRODUCT_BUNDLE_IDENTIFIER = com.junpeiqi.eparty;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
@@ -610,7 +612,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = EKM7RAGNA6;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stupidmonkey.yanaAPITests;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -23,6 +23,7 @@ enum APIEndpoint: String, CaseIterable {
case tcToken = "/tencent/cos/getToken" // COS Token
case publishFeed = "/dynamic/square/publish" //
case getUserInfo = "/user/get" //
case getMyDynamic = "/dynamic/getMyDynamic"
// Web
case userAgreement = "/modules/rule/protocol.html"

View File

@@ -23,7 +23,6 @@ public struct MomentsInfo: Codable, Equatable, Sendable {
let uid: Int
let nick: String
let avatar: String
let gender: Int
let type: Int
let content: String
let likeCount: Int
@@ -31,35 +30,30 @@ public struct MomentsInfo: Codable, Equatable, Sendable {
let commentCount: Int
let publishTime: Int
let worldId: Int
let squareTop: Int
let topicTop: Int
let newUser: Bool
let defUser: Int
let status: Int
let scene: String
// data.md
let playCount: Int?
let dynamicResList: [MomentsPicture]?
//
let gender: Int?
let squareTop: Int?
let topicTop: Int?
let newUser: Bool?
let defUser: Int?
let scene: String?
let userVipInfoVO: UserVipInfo?
// -
let headwearPic: String?
let headwearEffect: String?
let headwearType: Int?
let headwearName: String?
let headwearId: Int?
// -
let experLevelPic: String?
let charmLevelPic: String?
//
let isCustomWord: Bool?
let labelList: [String]?
// IntBool
var isSquareTop: Bool { squareTop != 0 }
var isTopicTop: Bool { topicTop != 0 }
//
//
var isSquareTop: Bool { (squareTop ?? 0) != 0 }
var isTopicTop: Bool { (topicTop ?? 0) != 0 }
var formattedPublishTime: Date {
Date(timeIntervalSince1970: TimeInterval(publishTime) / 1000.0)
}
@@ -67,11 +61,11 @@ public struct MomentsInfo: Codable, Equatable, Sendable {
///
struct MomentsPicture: Codable, Equatable, Sendable {
let id: Int
let resUrl: String
let format: String
let width: Int
let height: Int
let id: Int?
let resUrl: String?
let format: String?
let width: Int?
let height: Int?
let resDuration: Int? //
}
@@ -244,3 +238,46 @@ struct PublishFeedResponse: Codable, Equatable {
struct PublishFeedData: Codable, Equatable {
let dynamicId: Int?
}
// MARK: - API
///
struct MyMomentsResponse: Codable, Equatable, Sendable {
let code: Int
let message: String
let data: [MomentsInfo]?
let timestamp: Int?
}
struct GetMyDynamicRequest: APIRequestProtocol {
typealias Response = MyMomentsResponse
let endpoint: String = APIEndpoint.getMyDynamic.path
let method: HTTPMethod = .POST
let fromUid: Int
let uid: Int
let page: Int
let pageSize: Int
init(fromUid: Int, uid: Int, page: Int = 1, pageSize: Int = 20) {
self.fromUid = fromUid
self.uid = uid
self.page = page
self.pageSize = pageSize
}
var queryParameters: [String: String]? {
[
"fromUid": String(fromUid),
"uid": String(uid),
"page": String(page),
"pageSize": String(pageSize)
]
}
var bodyParameters: [String: Any]? { nil }
var includeBaseParameters: Bool { true }
var shouldShowLoading: Bool { true }
var shouldShowError: Bool { true }
}

View File

@@ -146,29 +146,209 @@ struct IDLoginData: Codable, Equatable {
// MARK: - User Info Model
struct UserInfo: Codable, Equatable {
let userId: String?
let username: String?
let nickname: String?
let uid: Int?
let userId: String? //
let nick: String?
let nickname: String? //
let avatar: String?
let email: String?
let phone: String?
let status: String?
let createTime: String?
let updateTime: String?
let region: String?
let regionDesc: String?
let gender: Int?
let birth: Int64?
let userDesc: String?
let userLevelVo: UserLevelVo?
let userVipInfoVO: UserVipInfoVO?
let medalsPic: [MedalsPic]?
let userHeadwear: UserHeadwear?
let privatePhoto: [PrivatePhoto]?
let createTime: Int64?
let phoneAreaCode: String?
let erbanNo: Int?
let isCertified: Bool?
let isBindPhone: Bool?
let isBindApple: Bool?
let isBindPasswd: Bool?
let isBindPaymentPwd: Bool?
let banAccount: Bool?
let visitNum: Int?
let fansNum: Int?
let followNum: Int?
let visitHide: Bool?
let visitListView: Bool?
let newUser: Bool?
let defUser: Int?
let platformRole: Int?
let bindType: Int?
let showLimitCharge: Bool?
let uploadGifAvatarPrice: Int?
let hasRegPacket: Bool?
let hasPrettyErbanNo: Bool?
let hasSuperRole: Bool?
let isRechargeUser: Bool?
let isFirstCharge: Bool?
let fromSayHelloChannel: Bool?
let partitionId: Int?
let useStatus: Int?
let micNickColor: String?
let micCircle: String?
let audioCard: AudioCard?
let userInfoSkillVo: UserInfoSkillVo?
let userInfoCardPic: String?
let iosBubbleUrl: String?
let androidBubbleUrl: String?
let status: String? //
let username: String? //
let email: String? //
let phone: String? //
let updateTime: String? //
enum CodingKeys: String, CodingKey {
case uid
case userId = "user_id"
case username
case nick
case nickname
case avatar
case region
case regionDesc
case gender
case birth
case userDesc
case userLevelVo
case userVipInfoVO
case medalsPic
case userHeadwear
case privatePhoto
case createTime
case phoneAreaCode
case erbanNo
case isCertified
case isBindPhone
case isBindApple
case isBindPasswd
case isBindPaymentPwd
case banAccount
case visitNum
case fansNum
case followNum
case visitHide
case visitListView
case newUser
case defUser
case platformRole
case bindType
case showLimitCharge
case uploadGifAvatarPrice
case hasRegPacket
case hasPrettyErbanNo
case hasSuperRole
case isRechargeUser
case isFirstCharge
case fromSayHelloChannel
case partitionId
case useStatus
case micNickColor
case micCircle
case audioCard
case userInfoSkillVo
case userInfoCardPic
case iosBubbleUrl
case androidBubbleUrl
case status
case username
case email
case phone
case status
case createTime = "create_time"
case updateTime = "update_time"
case updateTime
}
}
// MARK: -
struct UserLevelVo: Codable, Equatable {
let experUrl: String?
let charmLevelSeq: Int?
let experLevelName: String?
let charmLevelName: String?
let charmAmount: Int?
let experLevelGrp: String?
let charmUrl: String?
let experLevelSeq: Int?
let experAmount: Int?
let charmLevelGrp: String?
}
struct UserVipInfoVO: Codable, Equatable {
let vipIcon: String?
let nameplateId: Int?
let vipLogo: String?
let userCardBG: String?
let preventKick: Bool?
let preventTrace: Bool?
let preventFollow: Bool?
let micNickColour: String?
let micCircle: String?
let enterRoomEffects: String?
let medalSeat: Int?
let friendNickColour: String?
let visitHide: Bool?
let visitListView: Bool?
let privateChatLimit: Bool?
let nameplateUrl: String?
let roomPicScreen: Bool?
let uploadGifAvatar: Bool?
let expireTime: Int64?
let enterHide: Bool?
let vipLevel: Int?
let vipName: String?
}
struct MedalsPic: Codable, Equatable {
let picUrl: String?
let mp4Url: String?
}
struct UserHeadwear: Codable, Equatable {
let expireTime: Int64?
let renewPrice: Int?
let uid: Int?
let comeFrom: Int?
let labelType: Int?
let limitDesc: String?
let redirectLink: String?
let headwearId: Int?
let buyTime: Int64?
let pic: String?
let used: Bool?
let price: Int?
let originalPrice: Int?
let type: Int?
let days: Int?
let headwearName: String?
let effect: String?
let expireDays: Int?
let status: Int?
}
struct PrivatePhoto: Codable, Equatable {
let seqNo: Int?
let photoUrl: String?
let createTime: Int64?
let review: Bool?
let pid: Int?
}
struct AudioCard: Codable, Equatable {
let uid: Int?
let status: Int?
}
struct UserInfoSkillVo: Codable, Equatable {
let liveTag: Bool?
let liveSkillVoList: [LiveSkillVo]?
}
struct LiveSkillVo: Codable, Equatable {
// API
}
// MARK: - Login Helper
struct LoginHelper {

View File

@@ -1,92 +1,330 @@
## 📝 给继任者的详细工作交接说明
亲爱的继任者,我刚刚为这个 **yana iOS 项目**完成了一个完整的图片缓存优化工作。以下是关键信息:
### 🎯 已完成的核心工作
1. **解决了重大性能问题**
- **问题**FeedView 中图片每次滚动都重新加载,用户体验极差
- **原因**AsyncImage 缓存不足没有预加载机制cell 重用时图片丢失
2. **创建了企业级图片缓存系统**
- **文件**`yana/Utils/ImageCacheManager.swift`
- **功能**:三层缓存(内存+磁盘+网络)+ 智能预加载 + 任务去重
3. **优化了 FeedView 架构**
- **文件**`yana/Views/FeedView.swift`
- **改进**:使用 `CachedAsyncImage` 替代 `AsyncImage`,添加预加载机制
### ✅ 技术架构详情
#### **ImageCacheManager 核心特性**
- **内存缓存**NSCache50MB 限制100张图片
- **磁盘缓存**Documents/ImageCache100MB 限制SHA256 文件名
- **预加载**当前位置前后2个动态的所有图片
- **任务去重**:同一图片多次请求共享下载任务
#### **CachedAsyncImage 组件**
- **缓存优先级**:内存 → 磁盘 → 网络
- **异步加载**:不阻塞主线程
- **SwiftUI 兼容**:完全兼容现有 AsyncImage 语法
#### **FeedView 优化**
- **OptimizedDynamicCardView**:使用缓存图片组件
- **OptimizedImageGrid**:优化的图片网格
- **智能预加载**onAppear 时触发相邻内容预加载
### 🔧 重要的技术细节
1. **哈希冲突解决**
- 项目中已有 `String+MD5.swift` 文件
- 使用现有的 `sha256()``md5()` 方法,避免重复声明
2. **兼容性处理**
- iOS 13+:使用 CryptoKit 的 SHA256
- iOS 13以下使用 CommonCrypto 的 MD5
3. **Bridging Header 配置**
- 已添加 `#import <CommonCrypto/CommonCrypto.h>`
### 🚀 性能提升效果
| 优化前 | 优化后 |
|--------|--------|
| ❌ 每次滚动重新加载图片 | ✅ 缓存图片瞬间显示 |
| ❌ 频繁网络请求 | ✅ 大幅减少网络请求 |
| ❌ 用户体验差 | ✅ 流畅滚动体验 |
### 📋 项目上下文回顾
1. **API 功能已完成**
- 动态内容 API 集成完毕DynamicsModels.swift + FeedFeature.swift
- 数据解析问题已解决(类型匹配修复)
- TCA 架构状态管理正常工作
2. **当前状态**
- ✅ 编译成功
- ✅ API 数据正常显示
- ✅ 图片缓存系统就绪
- ✅ 性能优化完成
### 🔍 可能的后续工作
用户可能需要:
1. **功能扩展**:点赞、评论、分享等交互功能
2. **UI 优化**:更丰富的动画效果、主题切换
3. **性能监控**:添加缓存命中率统计、内存使用监控
4. **错误处理**:网络异常时的重试机制优化
### 💡 重要提醒
- **用户是中文开发者**:需要中文回复,使用 Chain-of-Thought 思考过程
- **项目基于 iOS 15.6**:注意兼容性要求
- **TCA 架构**:遵循项目现有的 TCA 模式
- **图片缓存系统**:已经是生产就绪的企业级方案,无需重构
### 🎉 工作成果
这次优化彻底解决了图片重复加载的性能问题,用户现在可以享受流畅的滚动体验。缓存系统设计完善,支持大规模图片内容,为后续功能扩展奠定了坚实基础。
**继任者,你接手的是一个功能完整、性能优秀的动态内容展示系统!** 🚀
祝你工作顺利!
📦 Response Data:
{
"code" : 200,
"message" : "success",
"data" : [
{
"isLike" : false,
"uid" : 3184,
"playCount" : 0,
"worldId" : -1,
"likeCount" : 0,
"type" : 0,
"dynamicId" : 267,
"nick" : "hansome",
"avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg",
"commentCount" : 0,
"publishTime" : 1753182147000,
"status" : 0,
"content" : "我"
},
{
"isLike" : false,
"uid" : 3184,
"playCount" : 0,
"dynamicResList" : [
{
"height" : 3024,
"id" : 443,
"width" : 4032,
"resUrl" : "https:\/\/image.molistar.xyz\/images\/C32EB0F8-CBF5-4F4B-8114-C3C7E1AF192F.jpg",
"format" : "jpeg"
}
],
"worldId" : -1,
"likeCount" : 0,
"type" : 2,
"dynamicId" : 266,
"nick" : "hansome",
"avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg",
"commentCount" : 0,
"publishTime" : 1753181890000,
"status" : 0,
"content" : ""
},
{
"isLike" : false,
"uid" : 3184,
"playCount" : 0,
"dynamicResList" : [
{
"height" : 828,
"id" : 442,
"width" : 828,
"resUrl" : "https:\/\/image.molistar.xyz\/images\/1E8FE811-1989-4337-BDEE-63554F92A686.jpg",
"format" : "jpeg"
}
],
"worldId" : -1,
"likeCount" : 0,
"type" : 2,
"dynamicId" : 265,
"nick" : "hansome",
"avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg",
"commentCount" : 0,
"publishTime" : 1753181143000,
"status" : 0,
"content" : "大"
},
{
"isLike" : false,
"uid" : 3184,
"playCount" : 0,
"dynamicResList" : [
{
"height" : 3024,
"id" : 440,
"width" : 4032,
"resUrl" : "https:\/\/https:\/\/image.molistar.xyz\/images\/DF8E655B-2F63-4B34-90B3-13C8A812245C.jpg",
"format" : "jpeg"
},
{
"height" : 1792,
"id" : 441,
"width" : 828,
"resUrl" : "https:\/\/https:\/\/image.molistar.xyz\/images\/D869C761-59CC-4E6B-BB2A-74F87D4A4979.jpg",
"format" : "jpeg"
}
],
"worldId" : -1,
"likeCount" : 0,
"type" : 2,
"dynamicId" : 264,
"nick" : "hansome",
"avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg",
"commentCount" : 0,
"publishTime" : 1753180835000,
"status" : 0,
"content" : "好"
},
{
"isLike" : false,
"uid" : 3184,
"playCount" : 0,
"dynamicResList" : [
{
"height" : 1792,
"id" : 438,
"width" : 828,
"resUrl" : "https:\/\/image.molistar.xyz\/image\/9d5c8e10eb0d228a26ec4e8d58b41c38.jpeg",
"format" : "jpeg"
},
{
"height" : 1792,
"id" : 439,
"width" : 828,
"resUrl" : "https:\/\/image.molistar.xyz\/image\/9ab8dff9f5ffbb4d65998822dd126794.jpeg",
"format" : "jpeg"
}
],
"worldId" : -1,
"likeCount" : 0,
"type" : 2,
"dynamicId" : 263,
"nick" : "hansome",
"avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg",
"commentCount" : 0,
"publishTime" : 1753180130000,
"status" : 0,
"content" : "猜猜猜"
},
{
"isLike" : false,
"uid" : 3184,
"playCount" : 0,
"worldId" : -1,
"likeCount" : 0,
"type" : 0,
"dynamicId" : 262,
"nick" : "hansome",
"avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg",
"commentCount" : 0,
"publishTime" : 1753168392000,
"status" : 0,
"content" : "他哥哥哥哥哥哥"
},
{
"isLike" : false,
"uid" : 3184,
"playCount" : 0,
"worldId" : -1,
"likeCount" : 0,
"type" : 0,
"dynamicId" : 261,
"nick" : "hansome",
"avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg",
"commentCount" : 0,
"publishTime" : 1753168329000,
"status" : 0,
"content" : "一直以为自己是真的"
},
{
"isLike" : false,
"uid" : 3184,
"playCount" : 0,
"worldId" : -1,
"likeCount" : 0,
"type" : 0,
"dynamicId" : 260,
"nick" : "hansome",
"avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg",
"commentCount" : 0,
"publishTime" : 1753167661000,
"status" : 0,
"content" : "在意那些是自己一"
},
{
"isLike" : false,
"uid" : 3184,
"playCount" : 0,
"worldId" : -1,
"likeCount" : 0,
"type" : 0,
"dynamicId" : 259,
"nick" : "hansome",
"avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg",
"commentCount" : 0,
"publishTime" : 1753166596000,
"status" : 0,
"content" : "哈哈我觉得这个世界"
},
{
"isLike" : false,
"uid" : 3184,
"playCount" : 0,
"worldId" : -1,
"likeCount" : 0,
"type" : 0,
"dynamicId" : 258,
"nick" : "hansome",
"avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg",
"commentCount" : 0,
"publishTime" : 1753166592000,
"status" : 0,
"content" : "哈哈我觉得这个世界"
},
{
"isLike" : false,
"uid" : 3184,
"playCount" : 0,
"worldId" : -1,
"likeCount" : 0,
"type" : 0,
"dynamicId" : 257,
"nick" : "hansome",
"avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg",
"commentCount" : 0,
"publishTime" : 1753166298000,
"status" : 0,
"content" : "哈哈哈哈更"
},
{
"isLike" : false,
"uid" : 3184,
"playCount" : 0,
"worldId" : -1,
"likeCount" : 0,
"type" : 0,
"dynamicId" : 256,
"nick" : "hansome",
"avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg",
"commentCount" : 0,
"publishTime" : 1753165531000,
"status" : 0,
"content" : "不不不不不"
},
{
"isLike" : false,
"uid" : 3184,
"playCount" : 0,
"worldId" : -1,
"likeCount" : 0,
"type" : 0,
"dynamicId" : 255,
"nick" : "hansome",
"avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg",
"commentCount" : 0,
"publishTime" : 1753156105000,
"status" : 0,
"content" : "你有什么"
},
{
"isLike" : false,
"uid" : 3184,
"playCount" : 0,
"worldId" : -1,
"likeCount" : 0,
"type" : 0,
"dynamicId" : 254,
"nick" : "hansome",
"avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg",
"commentCount" : 0,
"publishTime" : 1752650142000,
"status" : 0,
"content" : "igvigciycoyvcoyvyovoy突袭陶瓷陶瓷陶瓷陶瓷陶瓷陶瓷陶瓷"
},
{
"isLike" : false,
"uid" : 3184,
"playCount" : 0,
"worldId" : -1,
"likeCount" : 0,
"type" : 0,
"dynamicId" : 247,
"nick" : "hansome",
"avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg",
"commentCount" : 0,
"publishTime" : 1742801936000,
"status" : 0,
"content" : "vicigiigohvhveerr让你表弟姐姐接你吧多半都不\n\n\n\n代表脯肉吧多半日品牌狠批人品很频频频频噢……在一起的时候就是一次又来了就可以😌我们一起加油呀我要好好学习📑你好开心🥳、在一起的时候就像一只手握在一起\n"
},
{
"isLike" : false,
"uid" : 3184,
"playCount" : 0,
"worldId" : -1,
"likeCount" : 0,
"type" : 0,
"dynamicId" : 206,
"nick" : "hansome",
"avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg",
"commentCount" : 0,
"publishTime" : 1726834519000,
"status" : 1,
"content" : "爸爸不会后悔就"
},
{
"isLike" : true,
"uid" : 3184,
"playCount" : 0,
"dynamicResList" : [
{
"height" : 500,
"id" : 355,
"width" : 500,
"resUrl" : "https:\/\/image.pekolive.com\/image\/0c091078d01305f3144ab3352a9fe21a.jpeg",
"format" : "jpeg"
},
{
"height" : 328,
"id" : 356,
"width" : 440,
"resUrl" : "https:\/\/image.pekolive.com\/image\/8cdbbab3a0e6df7389f2d2671ee48bc3.jpeg",
"format" : "jpeg"
}
],
"worldId" : -1,
"likeCount" : 1,
"type" : 2,
"dynamicId" : 205,
"nick" : "hansome",
"avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg",
"commentCount" : 0,
"publishTime" : 1726834050000,
"status" : 1,
"content" : "寄件人、一直以为自己是什么地方玩不"
}
],
"timestamp" : 1753256129947
}
=====================================

View File

@@ -1,33 +1,24 @@
import Foundation
import ComposableArchitecture
@Reducer
struct HomeFeature {
struct HomeFeature: Reducer {
enum Route: Equatable {
case createFeed
}
@ObservableState
struct State: Equatable, Sendable {
struct State: Equatable {
var isInitialized = false
var userInfo: UserInfo?
var accountModel: AccountModel?
var error: String?
//
var isSettingPresented = false
var settingState = SettingFeature.State()
// Feed
var feedState = FeedFeature.State()
//
var meDynamic = MeDynamicFeature.State(uid: 0)
var isLoggedOut = false
//
var route: Route? = nil
}
@CasePathable
enum Action: Equatable {
case onAppear
@@ -37,91 +28,77 @@ struct HomeFeature {
case accountModelLoaded(AccountModel?)
case logoutTapped
case logout
// actions
case settingDismissed
case setting(SettingFeature.Action)
// Feed actions
case feed(FeedFeature.Action)
//
case meDynamic(MeDynamicFeature.Action)
case logoutCompleted
// actions
case showCreateFeed
case createFeedDismissed
}
var body: some Reducer<State, Action> {
// Reducer<State, Action>.combine([
// Reducer { state, action in
// switch action {
// case .onAppear:
// guard !state.isInitialized else {
// return Effect.none
// }
// state.isInitialized = true
// return .concatenate(
// .send(.loadUserInfo),
// .send(.loadAccountModel)
// )
// case .loadUserInfo:
// return .run { send in
// let userInfo = await UserInfoManager.getUserInfo()
// await send(.userInfoLoaded(userInfo))
// }
// case let .userInfoLoaded(userInfo):
// state.userInfo = userInfo
// return Effect.none
// case .loadAccountModel:
// return .run { send in
// let accountModel = await UserInfoManager.getAccountModel()
// await send(.accountModelLoaded(accountModel))
// }
// case let .accountModelLoaded(accountModel):
// state.accountModel = accountModel
// return Effect.none
// case .logoutTapped:
// return .send(.logout)
// case .logout:
// return .run { send in
// await UserInfoManager.clearAllAuthenticationData()
// await send(.logoutCompleted)
// }
// case .logoutCompleted:
// state.isLoggedOut = true
// return Effect.none
// case .settingDismissed:
// state.isSettingPresented = false
// return Effect.none
// case .setting:
// return Effect.none
// case .showCreateFeed:
// state.route = .createFeed
// return Effect.none
// case .createFeedDismissed:
// state.route = nil
// return Effect.none
// case .feed:
// return Effect.none
// }
// },
// Scope(
// state: \State.settingState,
// action: /Action.setting,
// child: SettingFeature()
// ),
// Scope(
// state: \State.feedState,
// action: /Action.feed,
// child: FeedFeature()
// )
// ])
var body: some ReducerOf<Self> {
Scope(state: \.settingState, action: \.setting) {
SettingFeature()
}
Scope(state: \.feedState, action: \.feed) {
FeedFeature()
}
Scope(state: \.meDynamic, action: \.meDynamic) {
MeDynamicFeature()
}
Reduce { state, action in
switch action {
case .onAppear:
guard !state.isInitialized else { return .none }
state.isInitialized = true
return .concatenate(
.send(.loadUserInfo),
.send(.loadAccountModel)
)
case .loadUserInfo:
return .run { send in
let userInfo = await UserInfoManager.getUserInfo()
await send(.userInfoLoaded(userInfo))
}
case let .userInfoLoaded(userInfo):
state.userInfo = userInfo
state.meDynamic.uid = userInfo?.uid ?? 0
return .none
case .loadAccountModel:
return .run { send in
let accountModel = await UserInfoManager.getAccountModel()
await send(.accountModelLoaded(accountModel))
}
case let .accountModelLoaded(accountModel):
state.accountModel = accountModel
return .none
case .logoutTapped:
return .send(.logout)
case .logout:
return .run { send in
await UserInfoManager.clearAllAuthenticationData()
await send(.logoutCompleted)
}
case .logoutCompleted:
state.isLoggedOut = true
return .none
case .settingDismissed:
state.isSettingPresented = false
return .none
case .setting:
return .none
case .showCreateFeed:
state.route = .createFeed
return .none
case .createFeedDismissed:
state.route = nil
return .none
case .feed:
return .none
case .meDynamic:
return .none
}
}
}
}
// 使
// extension Notification.Name {
// static let homeLogout = Notification.Name("homeLogout")
// }

View File

@@ -10,25 +10,51 @@ struct MainFeature: Reducer {
struct State: Equatable {
var selectedTab: Tab = .feed
var feedList: FeedListFeature.State = .init()
var meDynamic: MeDynamicFeature.State = .init(uid: 0)
var accountModel: AccountModel? = nil
}
@CasePathable
enum Action: Equatable {
case onAppear
case selectTab(Tab)
case feedList(FeedListFeature.Action)
case meDynamic(MeDynamicFeature.Action)
case accountModelLoaded(AccountModel?)
}
var body: some ReducerOf<Self> {
Scope(state: \.feedList, action: \.feedList) {
FeedListFeature()
}
Scope(state: \.meDynamic, action: \.meDynamic) {
MeDynamicFeature()
}
Reduce { state, action in
switch action {
case .onAppear:
return .run { send in
let accountModel = await UserInfoManager.getAccountModel()
await send(.accountModelLoaded(accountModel))
}
case .selectTab(let tab):
state.selectedTab = tab
if tab == .other, let uidStr = state.accountModel?.uid, let uid = Int(uidStr), uid > 0 {
state.meDynamic = MeDynamicFeature.State(uid: uid)
return .send(.meDynamic(.onAppear))
}
return .none
case .feedList:
return .none
case let .accountModelLoaded(accountModel):
state.accountModel = accountModel
if let uidStr = accountModel?.uid, let uid = Int(uidStr), uid > 0 {
state.meDynamic = MeDynamicFeature.State(uid: uid)
return .send(.meDynamic(.onAppear))
}
return .none
default:
return .none
}
}
}

View File

@@ -0,0 +1,91 @@
import Foundation
import ComposableArchitecture
@Reducer
struct MeDynamicFeature: Reducer {
struct State: Equatable {
var uid: Int
var dynamics: [MomentsInfo] = []
var page: Int = 1
var pageSize: Int = 20
var isLoading: Bool = false
var isRefreshing: Bool = false
var isLoadingMore: Bool = false
var hasMore: Bool = true
var error: String?
var isInitialized: Bool = false //
}
enum Action: Equatable {
case onAppear
case refresh
case loadMore
case fetchResponse(Result<MyMomentsResponse, APIError>)
}
@Dependency(\.apiService) var apiService
func reduce(into state: inout State, action: Action) async -> Effect<Action> {
switch action {
case .onAppear:
guard !state.isInitialized else { return .none }
state.isInitialized = true
state.page = 1
state.dynamics = []
state.hasMore = true
state.isLoading = true
state.error = nil
return fetchDynamics(uid: state.uid, page: 1, pageSize: state.pageSize)
case .refresh:
state.page = 1
state.hasMore = true
state.isRefreshing = true
state.error = nil
state.isInitialized = false //
return fetchDynamics(
uid: state.uid,
page: 1,
pageSize: state.pageSize
)
case .loadMore:
guard !state.isLoadingMore, state.hasMore else { return .none }
state.isLoadingMore = true
return fetchDynamics(uid: state.uid, page: state.page + 1, pageSize: state.pageSize)
case let .fetchResponse(result):
state.isLoading = false
state.isRefreshing = false
state.isLoadingMore = false
switch result {
case let .success(resp):
let newDynamics = resp.data ?? []
if state.page == 1 {
state.dynamics = newDynamics
} else {
state.dynamics += newDynamics
}
state.hasMore = newDynamics.count == state.pageSize
if state.hasMore { state.page += 1 }
state.error = nil
case let .failure(error):
state.error = error.localizedDescription
}
return .none
}
}
private func fetchDynamics(uid: Int, page: Int, pageSize: Int) -> Effect<Action> {
let apiService = self.apiService
return .run { send in
do {
let req = GetMyDynamicRequest(fromUid: uid, uid: uid, page: page, pageSize: pageSize)
let resp = try await apiService.request(req)
await send(.fetchResponse(.success(resp)))
} catch {
await send(.fetchResponse(.failure(error as? APIError ?? .unknown(error.localizedDescription))))
}
}
}
}

View File

@@ -71,7 +71,7 @@ struct OptimizedDynamicCardView: View {
//
if let images = moment.dynamicResList, !images.isEmpty {
OptimizedImageGrid(images: images) { tappedIndex in
previewImageUrls = images.map { $0.resUrl }
previewImageUrls = images.map { $0.resUrl ?? "" }
previewIndex = tappedIndex
showPreview = true
}
@@ -160,7 +160,7 @@ struct OptimizedDynamicCardView: View {
let moment = allMoments[index]
urlsToPreload.append(moment.avatar)
if let images = moment.dynamicResList {
urlsToPreload.append(contentsOf: images.map { $0.resUrl })
urlsToPreload.append(contentsOf: images.compactMap { $0.resUrl })
}
}
ImageCacheManager.shared.preloadImages(urls: urlsToPreload)
@@ -277,7 +277,7 @@ struct SquareImageView: View {
}
private var imageContent: some View {
CachedAsyncImage(url: image.resUrl) { imageView in
CachedAsyncImage(url: image.resUrl ?? "") { imageView in
imageView
.resizable()
.aspectRatio(contentMode: .fill)

View File

@@ -34,8 +34,15 @@ struct HomeView: View {
)
.transition(.opacity)
case .me:
MeView(onLogout: onLogout)
.transition(.opacity)
Spacer()
// MeView(
// meDynamicStore: store.scope(
// state: \.meDynamic,
// action: \.meDynamic
// ),
// onLogout: onLogout
// )
// .transition(.opacity)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)

View File

@@ -1,6 +1,5 @@
import SwiftUI
import ComposableArchitecture
//import Components // BottomTabView Components
struct MainView: View {
let store: StoreOf<MainFeature>
@@ -27,9 +26,17 @@ struct MainView: View {
))
.transition(.opacity)
case .other:
MeView(onLogout: {}) //
if let accountModel = viewStore.accountModel {
MeView(
meDynamicStore: store.scope(
state: \.meDynamic,
action: \.meDynamic
),
accountModel: accountModel,
onLogout: {}
)
.transition(.opacity)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -45,6 +52,9 @@ struct MainView: View {
}
}
}
.onAppear {
viewStore.send(.onAppear)
}
}
}
}

View File

@@ -0,0 +1,75 @@
import SwiftUI
import ComposableArchitecture
struct MeDynamicView: View {
let store: StoreOf<MeDynamicFeature>
// uid
@State private var showDeleteAlert = false
@State private var selectedMoment: MomentsInfo?
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
Group {
if viewStore.isLoading && viewStore.dynamics.isEmpty {
ProgressView("加载中...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if let error = viewStore.error {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 40))
.foregroundColor(.yellow)
Text(error)
.foregroundColor(.red)
Button("重试") {
viewStore.send(.onAppear)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if viewStore.dynamics.isEmpty {
VStack(spacing: 16) {
Image(systemName: "tray")
.font(.system(size: 40))
.foregroundColor(.gray)
Text("暂无动态")
.foregroundColor(.white.opacity(0.8))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
ScrollView {
LazyVStack(spacing: 12) {
ForEach(Array(viewStore.dynamics.enumerated()), id: \.element.dynamicId) { index, moment in
OptimizedDynamicCardView(moment: moment, allMoments: viewStore.dynamics, currentIndex: index)
.padding(.horizontal, 12)
.onLongPressGesture {
if viewStore.uid == moment.uid {
selectedMoment = moment
showDeleteAlert = true
}
}
}
if viewStore.hasMore {
ProgressView()
.onAppear {
viewStore.send(.loadMore)
}
}
}
.padding(.top, 8)
}
.refreshable {
viewStore.send(.refresh)
}
}
}
.alert("确认删除该动态?", isPresented: $showDeleteAlert, presenting: selectedMoment) { moment in
Button("删除", role: .destructive) {
// TODO: Action viewStore.send(.delete(moment.dynamicId))
}
Button("取消", role: .cancel) {}
} message: { _ in
Text("此操作不可恢复")
}
}
}
}

View File

@@ -2,90 +2,48 @@ import SwiftUI
import ComposableArchitecture
struct MeView: View {
let meDynamicStore: StoreOf<MeDynamicFeature>
let accountModel: AccountModel
@State private var showLogoutConfirmation = false
let onLogout: () -> Void //
@State private var showSetting = false // SettingView
@State private var showSetting = false
@State private var userInfo: UserInfo?
@State private var isLoadingUserInfo = true
@State private var errorMessage: String?
@State private var hasLoaded = false
let onLogout: () -> Void
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack(spacing: 20) {
//
HStack {
Spacer()
Text("我的")
.font(.system(size: 22, weight: .semibold))
.foregroundColor(.white)
Spacer()
Button(action: { showSetting = true }) {
Image(systemName: "gearshape")
.font(.system(size: 22, weight: .regular))
.foregroundColor(.white)
}
.padding(.trailing, 8)
}
.padding(.top, geometry.safeAreaInsets.top + 20)
//
VStack(spacing: 16) {
Circle()
.fill(Color.white.opacity(0.2))
.frame(width: 80, height: 80)
.overlay(
Image(systemName: "person.fill")
.font(.system(size: 40))
.foregroundColor(.white)
)
Text("用户昵称")
.font(.system(size: 18, weight: .medium))
.foregroundColor(.white)
Text("ID: 123456789")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.7))
}
.padding(.top, 30)
//
VStack(spacing: 12) {
MenuItemView(icon: "gearshape", title: "设置", action: {})
MenuItemView(icon: "person.circle", title: "个人信息", action: {})
MenuItemView(icon: "heart", title: "我的收藏", action: {})
MenuItemView(icon: "clock", title: "浏览历史", action: {})
MenuItemView(icon: "questionmark.circle", title: "帮助与反馈", action: {})
}
.padding(.horizontal, 20)
.padding(.top, 40)
// 退
Button(action: {
showLogoutConfirmation = true
}) {
HStack {
Image(systemName: "rectangle.portrait.and.arrow.right")
.font(.system(size: 16))
Text("退出登录")
.font(.system(size: 16, weight: .medium))
}
.foregroundColor(.red)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(
Color.white.opacity(0.1)
.cornerRadius(12)
)
}
.padding(.horizontal, 20)
.padding(.top, 30)
// -
Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100)
ZStack {
// - 使"bg"
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.clipped()
.ignoresSafeArea(.all)
VStack(spacing: 0) {
// -
UserProfileSection(
userInfo: userInfo,
isLoading: isLoadingUserInfo,
errorMessage: errorMessage,
onSettingTapped: { showSetting = true }
)
//
MeDynamicView(store: meDynamicStore)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.padding(.top, 100)
}
}
.ignoresSafeArea(.container, edges: .top)
.onAppear {
if !hasLoaded {
loadUserInfo()
hasLoaded = true
}
}
.alert("确认退出", isPresented: $showLogoutConfirmation) {
Button("取消", role: .cancel) { }
Button("退出", role: .destructive) {
@@ -95,53 +53,178 @@ struct MeView: View {
Text("确定要退出登录吗?")
}
.sheet(isPresented: $showSetting) {
// storestore
SettingView(store: Store(initialState: SettingFeature.State()) { SettingFeature() })
}
}
// MARK: -
private func loadUserInfo() {
Task {
isLoadingUserInfo = true
errorMessage = nil
debugInfoSync("📱 MeView: 开始加载用户信息")
// ID
guard let currentUserId = await UserInfoManager.getCurrentUserId() else {
debugErrorSync("❌ MeView: 无法获取当前用户ID")
await MainActor.run {
errorMessage = "用户未登录"
isLoadingUserInfo = false
}
return
}
debugInfoSync("📱 MeView: 当前用户ID: \(currentUserId)")
// APIService
let apiService: APIServiceProtocol = LiveAPIService()
//
if let cachedUserInfo = await UserInfoManager.getUserInfo() {
debugInfoSync("📱 MeView: 使用本地缓存的用户信息")
await MainActor.run {
self.userInfo = cachedUserInfo
self.isLoadingUserInfo = false
}
}
//
debugInfoSync("🌐 MeView: 从服务器获取最新用户信息")
let freshUserInfo = await UserInfoManager.fetchUserInfoFromServer(
uid: currentUserId,
apiService: apiService
)
await MainActor.run {
if let freshUserInfo = freshUserInfo {
debugInfoSync("✅ MeView: 成功获取最新用户信息")
debugInfoSync(" 用户名: \(freshUserInfo.nick ?? freshUserInfo.nick ?? "未知")")
debugInfoSync(" 用户ID: \(String(freshUserInfo.uid ?? 0))")
self.userInfo = freshUserInfo
self.errorMessage = nil
} else {
debugErrorSync("❌ MeView: 无法从服务器获取用户信息")
if self.userInfo == nil {
self.errorMessage = "无法获取用户信息"
}
}
self.isLoadingUserInfo = false
}
}
}
// MARK: - 退
private func performLogout() async {
debugInfoSync("🔓 开始执行退出登录...")
// keychain
await UserInfoManager.clearAllAuthenticationData()
//
onLogout()
debugInfoSync("✅ 退出登录完成")
}
// MARK: -
private func onSettingTapped() {
showSetting = true
}
// MARK: - ID
private func copyUserId() {
if let userId = userInfo?.userId {
UIPasteboard.general.string = userId
debugInfoSync("📋 MeView: 用户ID已复制到剪贴板: \(userId)")
}
}
}
// MARK: -
struct MenuItemView: View {
let icon: String
let title: String
let action: () -> Void
// MARK: -
struct UserProfileSection: View {
let userInfo: UserInfo?
let isLoading: Bool
let errorMessage: String?
let onSettingTapped: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 16) {
Image(systemName: icon)
.font(.system(size: 20))
.foregroundColor(.white)
.frame(width: 24)
Text(title)
.font(.system(size: 16))
.foregroundColor(.white)
.frame(maxWidth: .infinity, alignment: .leading)
Image(systemName: "chevron.right")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.6))
VStack(spacing: 16) {
//
HStack {
Spacer()
Button(action: onSettingTapped) {
Image(systemName: "gearshape")
.font(.system(size: 22, weight: .regular))
.foregroundColor(.white)
}
.padding(.trailing, 20)
}
//
if isLoading {
Circle()
.fill(Color.white.opacity(0.2))
.frame(width: 130, height: 130)
.overlay(
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
)
} else {
Circle()
.fill(Color.white.opacity(0.2))
.frame(width: 130, height: 130)
.overlay(
Group {
if let avatarUrl = userInfo?.avatar, !avatarUrl.isEmpty {
AsyncImage(url: URL(string: avatarUrl)) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
.clipShape(Circle())
} placeholder: {
Image(systemName: "person.fill")
.font(.system(size: 40))
.foregroundColor(.white)
}
} else {
Image(systemName: "person.fill")
.font(.system(size: 40))
.foregroundColor(.white)
}
}
)
}
//
if let errorMessage = errorMessage {
Text(errorMessage)
.font(.system(size: 14))
.foregroundColor(.red)
.multilineTextAlignment(.center)
} else {
Text(userInfo?.nick ?? userInfo?.nick ?? "用户昵称")
.font(.system(size: 18, weight: .medium))
.foregroundColor(.white)
}
// ID
HStack(spacing: 8) {
Text("ID: \(userInfo?.uid ?? 0)")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.7))
if userInfo?.userId != nil {
Button(action: {
// ID
if let userId = userInfo?.userId {
UIPasteboard.general.string = userId
}
}) {
Image(systemName: "doc.on.doc")
.font(.system(size: 12))
.foregroundColor(.white.opacity(0.6))
}
}
}
.padding(.horizontal, 20)
.frame(height: 56)
.background(
Color.white.opacity(0.1)
.cornerRadius(12)
)
}
.buttonStyle(PlainButtonStyle())
.padding(.horizontal, 20)
.padding(.bottom, 20)
}
}

View File

@@ -175,11 +175,13 @@ xcodebuild -workspace yana.xcworkspace -scheme yana -configuration Debug build
```
### 关键代码修改
1. **HomeFeature.swift**: 添加设置相关状态管理
2. **HomeView.swift**: 修复 TCA store 绑定语法
3. **SettingFeature.swift**: 确保 Action 完整性
### 构建结果
**编译成功**: Exit code 0
⚠️ **警告信息**: 仅 Swift 6 兼容性警告,不影响运行
@@ -188,18 +190,21 @@ xcodebuild -workspace yana.xcworkspace -scheme yana -configuration Debug build
## 预防措施
### 开发规范
1. **统一包管理**: 优先使用一种包管理工具
2. **定期清理**: 定期清理 DerivedData 避免缓存问题
3. **代码审查**: 确保 TCA Feature 结构完整
4. **版本控制**: 及时提交关键配置文件
### 监控指标
- [ ] 项目编译时间 < 30s
- [ ] 无编译错误
- [ ] 依赖解析正常
- [ ] TCA 结构完整
### 工具使用
```bash
# 项目健康检查脚本
check_project() {
@@ -255,6 +260,7 @@ pod install --clean-install
## 总结
本次问题解决涉及以下关键技术点
1. **Xcode 项目配置管理**
2. **Swift Package Manager 与 CocoaPods 共存**
3. **TCA (The Composable Architecture) 最佳实践**
@@ -265,5 +271,5 @@ pod install --clean-install
---
**文档更新时间**: 2025-07-10
**适用版本**: iOS 15.6+, Swift 6, TCA 1.20.2+
**维护者**: AI Assistant & 开发团队
**适用版本**: iOS 16+, Swift 6, TCA 1.20.2+
**维护者**: AI Assistant & 开发团队