feat: 修复MainView Tab切换问题并优化MeView逻辑
- 新增MainView Tab切换问题分析文档,详细描述问题原因及解决方案。 - 优化BottomTabView的绑定逻辑,简化状态管理,确保Tab切换时状态正确更新。 - 在MeView中实现用户信息加载逻辑调整,确保动态列表仅在首次进入时加载,并添加错误处理视图。 - 创建EmptyStateView组件,提供统一的空状态展示和重试功能。 - 增强调试信息输出,便于后续问题排查和用户体验提升。
This commit is contained in:
82
issues/MainView Tab切换问题修复.md
Normal file
82
issues/MainView Tab切换问题修复.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# MainView Tab切换问题修复
|
||||
|
||||
## 问题描述
|
||||
|
||||
点击me tab时,页面没有切换到MeView,而是停留在FeedListView并显示"no moments yet",但触发了2次MeFeature onAppear事件。
|
||||
|
||||
## 问题分析
|
||||
|
||||
### 1. 根本原因:MainFeature被重新初始化
|
||||
从debug日志发现:
|
||||
```
|
||||
📱 MainContentView selectedTab: other
|
||||
🏗️ MainFeature 初始化 ← MainFeature被重新创建!
|
||||
📱 MainContentView selectedTab: feed
|
||||
```
|
||||
|
||||
**问题**:AppRootView中每次渲染都重新创建MainFeature的store,导致状态丢失。
|
||||
|
||||
### 2. Tab枚举不匹配问题
|
||||
- **MainFeature.Tab**: `feed(0), other(1)`
|
||||
- **BottomTabView.Tab**: `feed(0), me(1)`
|
||||
|
||||
虽然rawValue相同,但类型不同,导致类型转换问题。
|
||||
|
||||
### 3. MainView中的绑定逻辑问题
|
||||
```swift
|
||||
// 原来的错误代码
|
||||
BottomTabView(selectedTab: Binding(
|
||||
get: { Tab(rawValue: store.selectedTab.rawValue) ?? .feed }, // Tab类型不匹配
|
||||
set: { newTab in
|
||||
store.send(.selectTab(MainFeature.Tab(rawValue: newTab.rawValue) ?? .feed))
|
||||
}
|
||||
))
|
||||
```
|
||||
|
||||
### 4. MainContentView缺少状态追踪
|
||||
MainContentView没有使用`WithPerceptionTracking`,可能导致状态更新时视图不刷新。
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 1. 简化BottomTabView绑定逻辑
|
||||
- 添加详细的调试信息追踪Tab转换过程
|
||||
- 避免复杂的switch语句,使用三元运算符
|
||||
- 确保绑定逻辑的清晰性和可追踪性
|
||||
|
||||
### 2. 优化MainFeature的selectTab处理
|
||||
- 添加重复设置检查,避免重复状态变化
|
||||
- 增加详细的调试信息
|
||||
- 确保状态变化的唯一性
|
||||
|
||||
### 3. 添加状态一致性检查
|
||||
- 在MainView加载时检查selectedTab状态
|
||||
- 在MainContentView中验证状态一致性
|
||||
- 添加详细的调试信息追踪状态变化
|
||||
|
||||
### 4. 优化AppRootView的store管理
|
||||
- 修复store创建和缓存的逻辑
|
||||
- 确保store的稳定性
|
||||
- 添加store生命周期调试信息
|
||||
|
||||
### 5. 添加全面的调试信息
|
||||
- BottomTabView的get/set操作追踪
|
||||
- MainFeature的selectTab处理追踪
|
||||
- MainView和MainContentView的状态检查
|
||||
- AppRootView的store管理追踪
|
||||
|
||||
## 修复状态
|
||||
|
||||
- ✅ 简化BottomTabView绑定逻辑
|
||||
- ✅ 优化MainFeature的selectTab处理
|
||||
- ✅ 添加状态一致性检查
|
||||
- ✅ 优化AppRootView的store管理
|
||||
- ✅ 添加全面的调试信息
|
||||
- ✅ 更新问题分析文档
|
||||
|
||||
## 测试要点
|
||||
|
||||
1. 点击feed tab时正确显示FeedListView
|
||||
2. 点击me tab时正确显示MeView
|
||||
3. Tab切换时状态正确更新
|
||||
4. 调试信息正确输出
|
||||
5. 不再出现重复的onAppear事件
|
53
issues/MeView逻辑调整.md
Normal file
53
issues/MeView逻辑调整.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# MeView逻辑调整计划
|
||||
|
||||
## 需求分析
|
||||
|
||||
1. **用户信息获取逻辑**:每次显示都重新获取用户信息
|
||||
2. **动态列表获取逻辑**:只在首次进入时获取动态列表
|
||||
3. **错误处理逻辑**:动态列表API失败时显示错误视图组件
|
||||
4. **下拉刷新**:用户可以下拉刷新获取最新数据
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 1. 创建EmptyStateView组件
|
||||
- 位置:`Views/Components/EmptyStateView.swift`
|
||||
- 功能:显示"暂无数据"文案和"重试"按钮
|
||||
- 高度:100,与列表视图对齐
|
||||
- 接受重试回调函数
|
||||
|
||||
### 2. 修改MeFeature.State
|
||||
- 添加 `isUserInfoFirstLoad: Bool = true`
|
||||
- 添加 `showErrorView: Bool = false`
|
||||
- 添加 `momentsFirstLoadFailed: Bool = false`
|
||||
|
||||
### 3. 修改MeFeature.Action
|
||||
- 添加 `loadUserInfo`:专门用于获取用户信息
|
||||
- 添加 `retryMoments`:用于重试动态列表加载
|
||||
|
||||
### 4. 修改MeFeature.reducer逻辑
|
||||
- `onAppear`:每次显示都获取用户信息,只在首次进入时获取动态列表
|
||||
- `refresh`:同时获取用户信息和动态列表(下拉刷新)
|
||||
- `retryMoments`:重新加载动态列表第一页
|
||||
- `momentsResponse`:处理错误状态,第一页失败时显示错误视图
|
||||
|
||||
### 5. 修改MeView
|
||||
- 根据 `showErrorView` 状态显示错误视图或动态列表
|
||||
- 保持下拉刷新功能
|
||||
- 添加调试信息
|
||||
|
||||
## 实现状态
|
||||
|
||||
- ✅ 创建EmptyStateView组件
|
||||
- ✅ 修改MeFeature.State
|
||||
- ✅ 修改MeFeature.Action
|
||||
- ✅ 修改MeFeature.reducer逻辑
|
||||
- ✅ 修改MeView显示逻辑
|
||||
|
||||
## 测试要点
|
||||
|
||||
1. 每次进入页面都获取最新用户信息
|
||||
2. 动态列表只在首次进入时加载
|
||||
3. 动态列表API失败时显示错误视图
|
||||
4. 点击重试按钮重新加载动态列表
|
||||
5. 下拉刷新功能正常工作
|
||||
6. 用户信息加载失败时的错误处理
|
@@ -50,8 +50,6 @@
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
4C4C8FBE2DE5AF9200384527 /* yanaAPITests */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = yanaAPITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -258,10 +256,14 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources.sh\"\n";
|
||||
@@ -275,10 +277,14 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks.sh\"\n";
|
||||
|
@@ -42,8 +42,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-collections",
|
||||
"state" : {
|
||||
"revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0",
|
||||
"version" : "1.2.0"
|
||||
"revision" : "8c0c0a8b49e080e54e5e328cc552821ff07cd341",
|
||||
"version" : "1.2.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -51,8 +51,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-composable-architecture",
|
||||
"state" : {
|
||||
"revision" : "6574de2396319a58e86e2178577268cb4aeccc30",
|
||||
"version" : "1.20.2"
|
||||
"revision" : "4c47829a080789cf20d82c64d8c27291352391d4",
|
||||
"version" : "1.21.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -78,8 +78,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-dependencies",
|
||||
"state" : {
|
||||
"revision" : "4c90d6b2b9bf0911af87b103bb40f41771891596",
|
||||
"version" : "1.9.2"
|
||||
"revision" : "eefcdaa88d2e2fd82e3405dfb6eb45872011a0b5",
|
||||
"version" : "1.9.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -96,8 +96,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-navigation",
|
||||
"state" : {
|
||||
"revision" : "db6bc9dbfed001f21e6728fd36413d9342c235b4",
|
||||
"version" : "2.3.0"
|
||||
"revision" : "4e89284c1966538109dc783497405bc680e9bc96",
|
||||
"version" : "2.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -105,8 +105,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-perception",
|
||||
"state" : {
|
||||
"revision" : "d924c62a70fca5f43872f286dbd7cef0957f1c01",
|
||||
"version" : "1.6.0"
|
||||
"revision" : "328a0b49e2690135c4c2660661f0ed83f16853e3",
|
||||
"version" : "2.0.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -114,8 +114,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-sharing",
|
||||
"state" : {
|
||||
"revision" : "75e846ee3159dc75b3a29bfc24b6ce5a557ddca9",
|
||||
"version" : "2.5.2"
|
||||
"revision" : "5d87dda90ed048f216826efbad404110141161bb",
|
||||
"version" : "2.6.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -132,8 +132,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
|
||||
"state" : {
|
||||
"revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4",
|
||||
"version" : "1.5.2"
|
||||
"revision" : "23e3442166b5122f73f9e3e622cd1e4bafeab3b7",
|
||||
"version" : "1.6.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@@ -21,7 +21,7 @@ struct ConfigView: View {
|
||||
} else if let configData = store.configData {
|
||||
ConfigDataView(configData: configData, lastUpdated: store.lastUpdated)
|
||||
} else {
|
||||
EmptyStateView()
|
||||
// EmptyStateView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,20 +161,20 @@ struct SettingsSection: View {
|
||||
}
|
||||
|
||||
// MARK: - Empty State View
|
||||
struct EmptyStateView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "arrow.down.circle")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.blue)
|
||||
Text(LocalizedString("config.click_to_load", comment: ""))
|
||||
.font(.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
//struct EmptyStateView: View {
|
||||
// var body: some View {
|
||||
// VStack(spacing: 16) {
|
||||
// Image(systemName: "arrow.down.circle")
|
||||
// .font(.system(size: 40))
|
||||
// .foregroundColor(.blue)
|
||||
// Text(LocalizedString("config.click_to_load", comment: ""))
|
||||
// .font(.body)
|
||||
// .multilineTextAlignment(.center)
|
||||
// .foregroundColor(.secondary)
|
||||
// }
|
||||
// .frame(maxHeight: .infinity)
|
||||
// }
|
||||
//}
|
||||
|
||||
// MARK: - Action Buttons View
|
||||
struct ActionButtonsView: View {
|
||||
@@ -229,10 +229,10 @@ struct InfoRow: View {
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
#Preview {
|
||||
ConfigView(
|
||||
store: Store(initialState: ConfigFeature.State()) {
|
||||
ConfigFeature()
|
||||
}
|
||||
)
|
||||
}
|
||||
//#Preview {
|
||||
// ConfigView(
|
||||
// store: Store(initialState: ConfigFeature.State()) {
|
||||
// ConfigFeature()
|
||||
// }
|
||||
// )
|
||||
//}
|
||||
|
@@ -50,6 +50,7 @@ struct FeedListFeature {
|
||||
// 新增:CreateFeed发布成功通知
|
||||
case createFeedPublishSuccess
|
||||
// 预留后续 Action
|
||||
case checkAuthAndLoad
|
||||
}
|
||||
|
||||
func reduce(into state: inout State, action: Action) -> Effect<Action> {
|
||||
@@ -57,7 +58,35 @@ struct FeedListFeature {
|
||||
case .onAppear:
|
||||
guard state.isFirstLoad else { return .none }
|
||||
state.isFirstLoad = false
|
||||
return .send(.fetchFeeds)
|
||||
debugInfoSync("📱 FeedListFeature onAppear")
|
||||
// 直接触发认证检查和数据加载
|
||||
return .send(.checkAuthAndLoad)
|
||||
|
||||
case .checkAuthAndLoad:
|
||||
// 新增:认证检查和数据加载
|
||||
return .run { send in
|
||||
// 检查认证信息是否已保存
|
||||
let accountModel = await UserInfoManager.getAccountModel()
|
||||
if accountModel?.uid != nil {
|
||||
debugInfoSync("✅ FeedListFeature: 认证信息已准备好,开始获取动态")
|
||||
await send(.fetchFeeds)
|
||||
} else {
|
||||
debugInfoSync("⏳ FeedListFeature: 认证信息未准备好,等待...")
|
||||
// 增加等待时间和重试次数
|
||||
for attempt in 1...3 {
|
||||
try? await Task.sleep(nanoseconds: 500_000_000) // 0.5秒
|
||||
let retryAccountModel = await UserInfoManager.getAccountModel()
|
||||
if retryAccountModel?.uid != nil {
|
||||
debugInfoSync("✅ FeedListFeature: 第\(attempt)次重试成功,认证信息已保存,开始获取动态")
|
||||
await send(.fetchFeeds)
|
||||
return
|
||||
} else {
|
||||
debugInfoSync("⏳ FeedListFeature: 第\(attempt)次重试,认证信息仍未准备好")
|
||||
}
|
||||
}
|
||||
debugInfoSync("❌ FeedListFeature: 多次重试后认证信息仍未准备好")
|
||||
}
|
||||
}
|
||||
case .reload:
|
||||
// 下拉刷新,重置状态并请求第一页
|
||||
state.isLoading = true
|
||||
|
@@ -26,6 +26,19 @@ struct MainFeature {
|
||||
debugInfoSync("🏗️ MainFeature 初始化")
|
||||
debugInfoSync(" accountModel.uid: \(accountModel?.uid ?? "nil")")
|
||||
debugInfoSync(" 转换后的uid: \(uid)")
|
||||
|
||||
// 如果没有传入accountModel,尝试从Keychain获取
|
||||
if accountModel == nil {
|
||||
debugInfoSync(" 🔍 尝试从Keychain获取AccountModel")
|
||||
Task {
|
||||
if let savedAccountModel = await UserInfoManager.getAccountModel() {
|
||||
debugInfoSync(" ✅ 从Keychain获取到AccountModel: \(savedAccountModel.uid ?? "nil")")
|
||||
} else {
|
||||
debugInfoSync(" ⚠️ 从Keychain未获取到AccountModel")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var meState = MeFeature.State(displayUID: uid > 0 ? uid : nil)
|
||||
if uid > 0 {
|
||||
meState.uid = uid // 确保uid与displayUID一致
|
||||
@@ -73,15 +86,34 @@ struct MainFeature {
|
||||
await send(.accountModelLoaded(accountModel))
|
||||
}
|
||||
case .selectTab(let tab):
|
||||
debugInfoSync("🎯 MainFeature selectTab: \(tab)")
|
||||
debugInfoSync(" 当前selectedTab: \(state.selectedTab)")
|
||||
debugInfoSync(" 新selectedTab: \(tab)")
|
||||
|
||||
// 避免重复设置相同的tab
|
||||
guard state.selectedTab != tab else {
|
||||
debugInfoSync(" ⚠️ 重复设置相同tab,忽略")
|
||||
return .none
|
||||
}
|
||||
|
||||
state.selectedTab = tab
|
||||
state.navigationPath = []
|
||||
if tab == .other, let uidStr = state.accountModel?.uid, let uid = Int(uidStr), uid > 0 {
|
||||
if state.me.displayUID != uid {
|
||||
state.me.displayUID = uid
|
||||
state.me.uid = uid // 同步更新uid
|
||||
state.me.isFirstLoad = true
|
||||
debugInfoSync(" ✅ selectedTab已更新为: \(state.selectedTab)")
|
||||
|
||||
// 切换到MeView时,确保有有效的uid并触发数据加载
|
||||
if tab == .other {
|
||||
if let uidStr = state.accountModel?.uid, let uid = Int(uidStr), uid > 0 {
|
||||
if state.me.displayUID != uid {
|
||||
state.me.displayUID = uid
|
||||
state.me.uid = uid // 同步更新uid
|
||||
state.me.isFirstLoad = true
|
||||
debugInfoSync(" 🔄 更新MeFeature状态,uid: \(uid)")
|
||||
}
|
||||
debugInfoSync(" 📱 切换到MeView,触发数据加载")
|
||||
return .send(.me(.onAppear))
|
||||
} else {
|
||||
debugInfoSync(" ⚠️ 切换到MeView但uid无效,等待AccountModel加载")
|
||||
}
|
||||
return .send(.me(.onAppear))
|
||||
}
|
||||
return .none
|
||||
case .feedList(.testButtonTapped):
|
||||
@@ -97,14 +129,31 @@ struct MainFeature {
|
||||
return .none
|
||||
case let .accountModelLoaded(accountModel):
|
||||
state.accountModel = accountModel
|
||||
// 如果当前选中的是 MeView 标签页,且有有效的 uid,则触发数据加载
|
||||
if state.selectedTab == .other, let uidStr = accountModel?.uid, let uid = Int(uidStr), uid > 0 {
|
||||
debugInfoSync("📦 MainFeature: AccountModel已加载")
|
||||
debugInfoSync(" uid: \(accountModel?.uid ?? "nil")")
|
||||
|
||||
// 更新MeFeature状态
|
||||
if let uidStr = accountModel?.uid, let uid = Int(uidStr), uid > 0 {
|
||||
if state.me.displayUID != uid {
|
||||
state.me.displayUID = uid
|
||||
state.me.uid = uid // 同步更新uid
|
||||
state.me.isFirstLoad = true
|
||||
debugInfoSync(" 🔄 更新MeFeature状态,uid: \(uid)")
|
||||
}
|
||||
return .send(.me(.onAppear))
|
||||
|
||||
// 如果当前选中的是 MeView 标签页,则触发数据加载
|
||||
if state.selectedTab == .other {
|
||||
debugInfoSync(" 📱 当前在MeView,触发数据加载")
|
||||
return .send(.me(.onAppear))
|
||||
}
|
||||
|
||||
// 如果当前选中的是 FeedView 标签页,则触发数据加载
|
||||
if state.selectedTab == .feed {
|
||||
debugInfoSync(" 📱 当前在FeedView,触发数据加载")
|
||||
return .send(.feedList(.checkAuthAndLoad))
|
||||
}
|
||||
} else {
|
||||
debugInfoSync(" ⚠️ AccountModel中uid无效")
|
||||
}
|
||||
return .none
|
||||
case .me(.settingButtonTapped):
|
||||
|
@@ -7,6 +7,7 @@ struct MeFeature {
|
||||
@ObservableState
|
||||
struct State: Equatable {
|
||||
var isFirstLoad: Bool = true
|
||||
var isUserInfoFirstLoad: Bool = true
|
||||
var userInfo: UserInfo?
|
||||
var isLoadingUserInfo: Bool = false
|
||||
var userInfoError: String?
|
||||
@@ -24,6 +25,9 @@ struct MeFeature {
|
||||
// 新增:DetailView相关状态
|
||||
var showDetail: Bool = false
|
||||
var selectedMoment: MomentsInfo?
|
||||
// 新增:错误视图相关状态
|
||||
var showErrorView: Bool = false
|
||||
var momentsFirstLoadFailed: Bool = false
|
||||
|
||||
init(displayUID: Int? = nil) {
|
||||
self.displayUID = displayUID
|
||||
@@ -48,6 +52,8 @@ struct MeFeature {
|
||||
case onAppear
|
||||
case refresh
|
||||
case loadMore
|
||||
case loadUserInfo
|
||||
case retryMoments
|
||||
case userInfoResponse(Result<UserInfo, APIError>)
|
||||
case momentsResponse(Result<MyMomentsResponse, APIError>)
|
||||
// 设置按钮点击
|
||||
@@ -60,25 +66,54 @@ struct MeFeature {
|
||||
func reduce(into state: inout State, action: Action) -> Effect<Action> {
|
||||
switch action {
|
||||
case .onAppear:
|
||||
guard state.isFirstLoad else { return .none }
|
||||
debugInfoSync("📱 MeFeature onAppear")
|
||||
debugInfoSync("\n📱 MeFeature onAppear")
|
||||
debugInfoSync(" isFirstLoad: \(state.isFirstLoad)")
|
||||
debugInfoSync(" isUserInfoFirstLoad: \(state.isUserInfoFirstLoad)")
|
||||
debugInfoSync(" effectiveUID: \(state.effectiveUID)")
|
||||
state.isFirstLoad = false
|
||||
return .send(.refresh)
|
||||
|
||||
// 每次显示都获取用户信息
|
||||
let userInfoEffect = fetchUserInfo(uid: state.effectiveUID)
|
||||
|
||||
// 只在首次进入时获取动态列表
|
||||
if state.isFirstLoad {
|
||||
state.isFirstLoad = false
|
||||
return .merge(
|
||||
userInfoEffect,
|
||||
fetchMoments(uid: state.effectiveUID, page: 1, pageSize: state.pageSize)
|
||||
)
|
||||
} else {
|
||||
return userInfoEffect
|
||||
}
|
||||
case .refresh:
|
||||
guard state.effectiveUID > 0 else { return .none }
|
||||
debugInfoSync("🔄 MeFeature refresh")
|
||||
debugInfoSync("\n🔄 MeFeature refresh")
|
||||
debugInfoSync(" effectiveUID: \(state.effectiveUID)")
|
||||
state.isRefreshing = true
|
||||
state.page = 1
|
||||
state.hasMore = true
|
||||
state.userInfoError = nil // 重置错误状态
|
||||
state.momentsError = nil // 重置错误状态
|
||||
state.showErrorView = false // 隐藏错误视图
|
||||
return .merge(
|
||||
fetchUserInfo(uid: state.effectiveUID),
|
||||
fetchMoments(uid: state.effectiveUID, page: 1, pageSize: state.pageSize)
|
||||
)
|
||||
case .loadUserInfo:
|
||||
guard state.effectiveUID > 0 else { return .none }
|
||||
debugInfoSync("\n👤 MeFeature loadUserInfo")
|
||||
debugInfoSync(" effectiveUID: \(state.effectiveUID)")
|
||||
return fetchUserInfo(uid: state.effectiveUID)
|
||||
case .retryMoments:
|
||||
guard state.effectiveUID > 0 else { return .none }
|
||||
debugInfoSync("\n🔄 MeFeature retryMoments")
|
||||
debugInfoSync(" effectiveUID: \(state.effectiveUID)")
|
||||
state.showErrorView = false // 隐藏错误视图
|
||||
state.momentsFirstLoadFailed = false
|
||||
state.isLoadingMoments = true
|
||||
state.page = 1
|
||||
state.hasMore = true
|
||||
state.momentsError = nil
|
||||
return fetchMoments(uid: state.effectiveUID, page: 1, pageSize: state.pageSize)
|
||||
case .loadMore:
|
||||
guard state.effectiveUID > 0, state.hasMore, !state.isLoadingMore else { return .none }
|
||||
state.isLoadingMore = true
|
||||
@@ -151,6 +186,8 @@ struct MeFeature {
|
||||
state.hasMore = newMoments.count == state.pageSize
|
||||
if state.hasMore { state.page += 1 }
|
||||
state.momentsError = nil
|
||||
state.showErrorView = false // 隐藏错误视图
|
||||
state.momentsFirstLoadFailed = false
|
||||
|
||||
debugInfoSync("✅ 我的动态加载成功")
|
||||
debugInfoSync(" 加载数量: \(newMoments.count)")
|
||||
@@ -158,6 +195,11 @@ struct MeFeature {
|
||||
debugInfoSync(" 是否有更多: \(state.hasMore)")
|
||||
case let .failure(error):
|
||||
state.momentsError = error.localizedDescription
|
||||
// 如果是第一页加载失败,显示错误视图
|
||||
if state.page == 1 {
|
||||
state.showErrorView = true
|
||||
state.momentsFirstLoadFailed = true
|
||||
}
|
||||
debugErrorSync("❌ 我的动态加载失败: \(error.localizedDescription)")
|
||||
}
|
||||
return .none
|
||||
|
@@ -1,63 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="s0d-6b-0kx">
|
||||
<objects>
|
||||
<viewController id="Y6W-OH-hqX" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bg" translatesAutoresizingMaskIntoConstraints="NO" id="Mom-Je-A43">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
</imageView>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="logo" translatesAutoresizingMaskIntoConstraints="NO" id="jVZ-ey-zjS">
|
||||
<rect key="frame" x="146.66666666666666" y="200" width="100" height="100"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="100" id="61A-Xv-MlD"/>
|
||||
<constraint firstAttribute="height" constant="100" id="NWJ-mJ-K2O"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="E-Parti" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GrU-nK-tAY">
|
||||
<rect key="frame" x="138" y="332" width="117" height="48"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="40"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="jVZ-ey-zjS" firstAttribute="centerX" secondItem="Mom-Je-A43" secondAttribute="centerX" id="Atv-LB-aNW"/>
|
||||
<constraint firstItem="Mom-Je-A43" firstAttribute="leading" secondItem="vDu-zF-Fre" secondAttribute="leading" id="fQ2-yj-nze"/>
|
||||
<constraint firstItem="Mom-Je-A43" firstAttribute="top" secondItem="5EZ-qb-Rvc" secondAttribute="top" id="gVA-6N-OG4"/>
|
||||
<constraint firstItem="GrU-nK-tAY" firstAttribute="centerY" secondItem="Mom-Je-A43" secondAttribute="centerY" constant="-70" id="j81-qa-9vS"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Mom-Je-A43" secondAttribute="bottom" id="lBn-me-aJu"/>
|
||||
<constraint firstItem="Mom-Je-A43" firstAttribute="trailing" secondItem="vDu-zF-Fre" secondAttribute="trailing" id="lka-KN-fEy"/>
|
||||
<constraint firstItem="jVZ-ey-zjS" firstAttribute="top" secondItem="Mom-Je-A43" secondAttribute="top" constant="200" id="nCq-oK-mTB"/>
|
||||
<constraint firstItem="GrU-nK-tAY" firstAttribute="centerX" secondItem="Mom-Je-A43" secondAttribute="centerX" id="sZE-bZ-0Xj"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-208.3969465648855" y="-13.380281690140846"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="bg" width="375" height="812"/>
|
||||
<image name="logo" width="100" height="100"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
@@ -3,93 +3,62 @@ import SwiftUI
|
||||
// MARK: - API Loading Effect View
|
||||
|
||||
/// 全局 API 加载效果视图
|
||||
///
|
||||
/// 该视图显示在屏幕最顶层,包含:
|
||||
/// - Loading 动画(88x88,60% alpha 黑色圆角背景)
|
||||
/// - 错误信息显示(2秒后自动消失)
|
||||
/// - 支持多个并发显示
|
||||
/// - 不阻挡用户点击操作
|
||||
struct APILoadingEffectView: View {
|
||||
@ObservedObject private var loadingManager = APILoadingManager.shared
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// 🚨 极简渲染策略:避免复杂的 ForEach,只显示第一个需要显示的项目
|
||||
if let firstItem = getFirstDisplayItem() {
|
||||
SingleLoadingView(item: firstItem)
|
||||
.onAppear {
|
||||
debugInfoSync("🔍 Loading item appeared: \(firstItem.id)")
|
||||
}
|
||||
.onDisappear {
|
||||
debugInfoSync("🔍 Loading item disappeared: \(firstItem.id)")
|
||||
}
|
||||
LoadingItemView(item: firstItem)
|
||||
}
|
||||
}
|
||||
.allowsHitTesting(false) // 不阻挡用户点击
|
||||
.ignoresSafeArea(.all) // 覆盖整个屏幕
|
||||
.onReceive(loadingManager.$loadingItems) { items in
|
||||
debugInfoSync("🔍 Loading items updated: \(items.count) items")
|
||||
}
|
||||
.allowsHitTesting(false)
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
|
||||
/// 安全地获取第一个需要显示的项目
|
||||
private func getFirstDisplayItem() -> APILoadingItem? {
|
||||
guard Thread.isMainThread else {
|
||||
debugWarnSync("⚠️ getFirstDisplayItem called from background thread")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard Thread.isMainThread else { return nil }
|
||||
return loadingManager.loadingItems.first { $0.shouldDisplay }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Single Loading View
|
||||
// MARK: - Loading Item View
|
||||
|
||||
/// 单个加载项视图 - 极简版本
|
||||
private struct SingleLoadingView: View {
|
||||
private struct LoadingItemView: View {
|
||||
let item: APILoadingItem
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
switch item.state {
|
||||
case .loading:
|
||||
SimpleLoadingView()
|
||||
|
||||
case .error(let message):
|
||||
if item.shouldShowError {
|
||||
SimpleErrorView(message: message)
|
||||
}
|
||||
|
||||
case .success:
|
||||
EmptyView() // 成功状态不显示任何内容
|
||||
switch item.state {
|
||||
case .loading:
|
||||
LoadingSpinnerView()
|
||||
case .error(let message):
|
||||
if item.shouldShowError {
|
||||
ErrorMessageView(message: message)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
case .success:
|
||||
EmptyView()
|
||||
}
|
||||
// 🚨 移除复杂动画,避免渲染问题
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Simple Loading View
|
||||
// MARK: - Loading Spinner View
|
||||
|
||||
/// 极简 Loading 视图
|
||||
private struct SimpleLoadingView: View {
|
||||
private struct LoadingSpinnerView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
// 极简黑色背景 + 白色圆圈
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.black.opacity(0.6))
|
||||
.frame(width: 88, height: 88)
|
||||
|
||||
// 使用最简单的 ProgressView
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(1.2)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
@@ -97,10 +66,9 @@ private struct SimpleLoadingView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Simple Error View
|
||||
// MARK: - Error Message View
|
||||
|
||||
/// 极简错误视图
|
||||
private struct SimpleErrorView: View {
|
||||
private struct ErrorMessageView: View {
|
||||
let message: String
|
||||
|
||||
var body: some View {
|
||||
@@ -108,13 +76,10 @@ private struct SimpleErrorView: View {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
// 极简错误提示
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundColor(.white)
|
||||
.font(.title2)
|
||||
|
||||
Text(message)
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 14))
|
||||
@@ -127,101 +92,9 @@ private struct SimpleErrorView: View {
|
||||
.fill(Color.black.opacity(0.6))
|
||||
)
|
||||
.frame(maxWidth: 250)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
//#if DEBUG
|
||||
//struct APILoadingEffectView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ZStack {
|
||||
// // 模拟背景
|
||||
// Rectangle()
|
||||
// .fill(Color.blue.opacity(0.3))
|
||||
// .ignoresSafeArea()
|
||||
//
|
||||
// VStack(spacing: 20) {
|
||||
// Text("背景内容")
|
||||
// .font(.title)
|
||||
//
|
||||
// Button("测试按钮") {
|
||||
// debugInfoSync("按钮被点击了!")
|
||||
// }
|
||||
// .padding()
|
||||
// .background(Color.blue)
|
||||
// .foregroundColor(.white)
|
||||
// .cornerRadius(8)
|
||||
// }
|
||||
//
|
||||
// // Loading Effect View
|
||||
// APILoadingEffectView()
|
||||
// }
|
||||
// .previewDisplayName("API Loading Effect")
|
||||
// .onAppear {
|
||||
// // 模拟不同状态的预览
|
||||
// Task {
|
||||
// let manager = APILoadingManager.shared
|
||||
//
|
||||
// // 添加 loading
|
||||
// let id1 = manager.startLoading()
|
||||
//
|
||||
// // 2秒后添加错误
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
// Task {
|
||||
// manager.setError(id1, errorMessage: "网络连接失败,请检查网络设置")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// MARK: - Preview Helpers
|
||||
//
|
||||
///// 预览用的测试状态
|
||||
//private struct PreviewStateModifier: ViewModifier {
|
||||
// let showLoading: Bool
|
||||
// let showError: Bool
|
||||
// let errorMessage: String
|
||||
//
|
||||
// func body(content: Content) -> some View {
|
||||
// content
|
||||
// .onAppear {
|
||||
// Task {
|
||||
// let manager = APILoadingManager.shared
|
||||
//
|
||||
// if showLoading {
|
||||
// let _ = manager.startLoading()
|
||||
// }
|
||||
//
|
||||
// if showError {
|
||||
// let id = manager.startLoading()
|
||||
// try? await Task.sleep(nanoseconds: 1_000_000_000) // 1秒
|
||||
// manager.setError(id, errorMessage: errorMessage)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension View {
|
||||
// /// 添加预览状态
|
||||
// func previewLoadingState(
|
||||
// showLoading: Bool = false,
|
||||
// showError: Bool = false,
|
||||
// errorMessage: String = "示例错误信息"
|
||||
// ) -> some View {
|
||||
// self.modifier(PreviewStateModifier(
|
||||
// showLoading: showLoading,
|
||||
// showError: showError,
|
||||
// errorMessage: errorMessage
|
||||
// ))
|
||||
// }
|
||||
//}
|
||||
//#endif
|
||||
}
|
||||
|
@@ -3,27 +3,49 @@ import ComposableArchitecture
|
||||
|
||||
struct AppRootView: View {
|
||||
@State private var isLoggedIn = false
|
||||
@State private var mainStore: StoreOf<MainFeature>?
|
||||
|
||||
var body: some View {
|
||||
if isLoggedIn {
|
||||
MainView(
|
||||
store: Store(
|
||||
initialState: MainFeature.State()
|
||||
) {
|
||||
MainFeature()
|
||||
Group {
|
||||
if isLoggedIn {
|
||||
if let mainStore = mainStore {
|
||||
MainView(store: mainStore)
|
||||
.onAppear {
|
||||
debugInfoSync("🔄 AppRootView: 使用已存在的MainStore")
|
||||
}
|
||||
} else {
|
||||
// 简化逻辑:直接创建MainStore,避免重复创建
|
||||
let store = createMainStore()
|
||||
let _ = debugInfoSync("🆕 AppRootView: 创建MainStore")
|
||||
let _ = { mainStore = store }()
|
||||
|
||||
MainView(store: store)
|
||||
.onAppear {
|
||||
debugInfoSync("💾 AppRootView: MainStore已保存")
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
LoginView(
|
||||
store: Store(
|
||||
initialState: LoginFeature.State()
|
||||
) {
|
||||
LoginFeature()
|
||||
},
|
||||
onLoginSuccess: {
|
||||
isLoggedIn = true
|
||||
}
|
||||
)
|
||||
} else {
|
||||
LoginView(
|
||||
store: Store(
|
||||
initialState: LoginFeature.State()
|
||||
) {
|
||||
LoginFeature()
|
||||
},
|
||||
onLoginSuccess: {
|
||||
debugInfoSync("🔐 AppRootView: 登录成功,准备创建MainStore")
|
||||
isLoggedIn = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createMainStore() -> StoreOf<MainFeature> {
|
||||
debugInfoSync("🏗️ AppRootView: 创建新的MainStore实例")
|
||||
return Store(
|
||||
initialState: MainFeature.State()
|
||||
) {
|
||||
MainFeature()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
38
yana/Views/Components/EmptyStateView.swift
Normal file
38
yana/Views/Components/EmptyStateView.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
import SwiftUI
|
||||
|
||||
struct EmptyStateView: View {
|
||||
let onRetry: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: "doc.text")
|
||||
.font(.system(size: 32))
|
||||
.foregroundColor(.white.opacity(0.5))
|
||||
|
||||
Text("暂无数据")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
|
||||
Button("重试") {
|
||||
onRetry()
|
||||
}
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color.blue.opacity(0.8))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
.frame(height: 100)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ZStack {
|
||||
Color.black
|
||||
EmptyStateView {
|
||||
print("重试按钮被点击")
|
||||
}
|
||||
}
|
||||
}
|
@@ -40,7 +40,7 @@ public struct ImagePickerWithPreviewView: View {
|
||||
}
|
||||
} else {
|
||||
// 不显示任何内容,避免空页面闪烁
|
||||
EmptyView()
|
||||
CustomEmptyView(onRetry: {})
|
||||
.onChange(of: viewStore.inner.isLoading) { _, isLoading in
|
||||
if isLoading && loadingId == nil {
|
||||
loadingId = APILoadingManager.shared.startLoading()
|
||||
|
@@ -64,12 +64,11 @@ struct ErrorView: View {
|
||||
}
|
||||
|
||||
// MARK: - EmptyView
|
||||
struct EmptyView: View {
|
||||
struct CustomEmptyView: View {
|
||||
let onRetry: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Text(LocalizedString("feedList.empty", comment: "暂无动态"))
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.padding(.top, 20)
|
||||
EmptyStateView(onRetry: onRetry)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +184,9 @@ struct FeedListContentView: View {
|
||||
} else if let error = store.error {
|
||||
ErrorView(error: error)
|
||||
} else if store.moments.isEmpty {
|
||||
EmptyView()
|
||||
CustomEmptyView(onRetry: {
|
||||
store.send(.reload)
|
||||
})
|
||||
} else {
|
||||
MomentsListView(
|
||||
moments: store.moments,
|
||||
|
@@ -41,6 +41,8 @@ struct InternalMainView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
debugInfoSync("🚀 MainView onAppear")
|
||||
debugInfoSync(" 当前selectedTab: \(store.selectedTab)")
|
||||
store.send(.onAppear)
|
||||
}
|
||||
}
|
||||
@@ -85,6 +87,9 @@ struct InternalMainView: View {
|
||||
store: store,
|
||||
selectedTab: store.selectedTab
|
||||
)
|
||||
.onChange(of: store.selectedTab) { _, newTab in
|
||||
debugInfoSync("🔄 MainView selectedTab changed: \(newTab)")
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding(.bottom, 80) // 为底部导航栏留出空间
|
||||
|
||||
@@ -92,9 +97,17 @@ struct InternalMainView: View {
|
||||
VStack {
|
||||
Spacer()
|
||||
BottomTabView(selectedTab: Binding(
|
||||
get: { Tab(rawValue: store.selectedTab.rawValue) ?? .feed },
|
||||
get: {
|
||||
// 将MainFeature.Tab转换为BottomTabView.Tab
|
||||
let currentTab = store.selectedTab == .feed ? Tab.feed : Tab.me
|
||||
debugInfoSync("🔍 BottomTabView get: MainFeature.Tab.\(store.selectedTab) → BottomTabView.Tab.\(currentTab)")
|
||||
return currentTab
|
||||
},
|
||||
set: { newTab in
|
||||
store.send(.selectTab(MainFeature.Tab(rawValue: newTab.rawValue) ?? .feed))
|
||||
// 将BottomTabView.Tab转换为MainFeature.Tab
|
||||
let mainTab: MainFeature.Tab = newTab == .feed ? .feed : .other
|
||||
debugInfoSync("🔍 BottomTabView set: BottomTabView.Tab.\(newTab) → MainFeature.Tab.\(mainTab)")
|
||||
store.send(.selectTab(mainTab))
|
||||
}
|
||||
))
|
||||
}
|
||||
@@ -113,22 +126,26 @@ struct MainContentView: View {
|
||||
let store: StoreOf<MainFeature>
|
||||
let selectedTab: MainFeature.Tab
|
||||
var body: some View {
|
||||
Group {
|
||||
if selectedTab == .feed {
|
||||
FeedListView(store: store.scope(
|
||||
state: \.feedList,
|
||||
action: \.feedList
|
||||
))
|
||||
} else if selectedTab == .other {
|
||||
MeView(
|
||||
store: store.scope(
|
||||
state: \.me,
|
||||
action: \.me
|
||||
),
|
||||
showCloseButton: false // MainView中不需要关闭按钮
|
||||
)
|
||||
} else {
|
||||
EmptyView()
|
||||
WithPerceptionTracking {
|
||||
let _ = debugInfoSync("📱 MainContentView selectedTab: \(selectedTab)")
|
||||
let _ = debugInfoSync(" 与store.selectedTab一致: \(selectedTab == store.selectedTab)")
|
||||
Group {
|
||||
if selectedTab == .feed {
|
||||
FeedListView(store: store.scope(
|
||||
state: \.feedList,
|
||||
action: \.feedList
|
||||
))
|
||||
} else if selectedTab == .other {
|
||||
MeView(
|
||||
store: store.scope(
|
||||
state: \.me,
|
||||
action: \.me
|
||||
),
|
||||
showCloseButton: false // MainView中不需要关闭按钮
|
||||
)
|
||||
} else {
|
||||
CustomEmptyView(onRetry: {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -84,6 +84,7 @@ struct MeView: View {
|
||||
debugInfoSync(" 动态数量: \(store.moments.count)")
|
||||
debugInfoSync(" 用户信息错误: \(store.userInfoError ?? "nil")")
|
||||
debugInfoSync(" 动态错误: \(store.momentsError ?? "nil")")
|
||||
debugInfoSync(" 显示错误视图: \(store.showErrorView)")
|
||||
store.send(.onAppear)
|
||||
}
|
||||
// 新增:图片预览弹窗
|
||||
@@ -97,19 +98,19 @@ struct MeView: View {
|
||||
get: { store.showDetail },
|
||||
set: { _ in store.send(.detailDismissed) }
|
||||
)) {
|
||||
if let selectedMoment = store.selectedMoment {
|
||||
let detailStore = Store(
|
||||
initialState: DetailFeature.State(moment: selectedMoment)
|
||||
) {
|
||||
DetailFeature()
|
||||
}
|
||||
|
||||
DetailView(store: detailStore)
|
||||
.onChange(of: detailStore.shouldDismiss) { _, shouldDismiss in
|
||||
if shouldDismiss {
|
||||
store.send(.detailDismissed)
|
||||
}
|
||||
if let selectedMoment = store.selectedMoment {
|
||||
let detailStore = Store(
|
||||
initialState: DetailFeature.State(moment: selectedMoment)
|
||||
) {
|
||||
DetailFeature()
|
||||
}
|
||||
|
||||
DetailView(store: detailStore)
|
||||
.onChange(of: detailStore.shouldDismiss) { _, shouldDismiss in
|
||||
if shouldDismiss {
|
||||
store.send(.detailDismissed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,12 +166,12 @@ struct MeView: View {
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if let error = store.userInfoError ?? store.momentsError {
|
||||
} else if let error = store.userInfoError {
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.font(.system(size: 32))
|
||||
.foregroundColor(.orange)
|
||||
Text("加载失败")
|
||||
Text("用户信息加载失败")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
Text(error)
|
||||
@@ -179,7 +180,7 @@ struct MeView: View {
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 32)
|
||||
Button("重试") {
|
||||
store.send(.refresh)
|
||||
store.send(.loadUserInfo)
|
||||
}
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
@@ -189,6 +190,12 @@ struct MeView: View {
|
||||
.cornerRadius(8)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if store.showErrorView {
|
||||
// 显示错误视图组件
|
||||
EmptyStateView {
|
||||
store.send(.retryMoments)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if store.moments.isEmpty {
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: "doc.text")
|
||||
@@ -213,6 +220,9 @@ struct MeView: View {
|
||||
Text("调试: momentsError = \(store.momentsError ?? "nil")")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.yellow)
|
||||
Text("调试: showErrorView = \(store.showErrorView)")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.yellow)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
|
Reference in New Issue
Block a user