diff --git a/Package.swift b/Package.swift index 7ba3ef0..4b93d2d 100644 --- a/Package.swift +++ b/Package.swift @@ -23,11 +23,13 @@ let package = Package( name: "yana", dependencies: [ .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), - ] + ], + path: "yana", ), .testTarget( name: "yanaTests", - dependencies: ["yana"] + dependencies: ["yana"], + path: "yanaAPITests", ), ] -) +) diff --git a/yana.xcodeproj/project.pbxproj b/yana.xcodeproj/project.pbxproj index 172b983..914c94a 100644 --- a/yana.xcodeproj/project.pbxproj +++ b/yana.xcodeproj/project.pbxproj @@ -515,7 +515,7 @@ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "yana/yana-Bridging-Header.h"; - SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = 1; }; @@ -573,7 +573,7 @@ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "yana/yana-Bridging-Header.h"; - SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = 1; }; @@ -598,7 +598,7 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.9; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = 1; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/yana.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/yana"; }; @@ -621,7 +621,7 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.9; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = 1; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/yana.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/yana"; }; diff --git a/yana.xcworkspace/xcshareddata/swiftpm/Package.resolved b/yana.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0411924..ba57ea3 100644 --- a/yana.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/yana.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0", - "version" : "1.2.0" + "revision" : "8c0c0a8b49e080e54e5e328cc552821ff07cd341", + "version" : "1.2.1" } }, { diff --git a/yana/Features/AppSettingFeature.swift b/yana/Features/AppSettingFeature.swift index a1347ef..28a458f 100644 --- a/yana/Features/AppSettingFeature.swift +++ b/yana/Features/AppSettingFeature.swift @@ -22,6 +22,13 @@ struct AppSettingFeature { var nicknameInput: String = "" var isUpdatingUser: Bool = false var updateUserError: String? = nil + + // 新增:带userInfo、avatarURL、nickname的init + init(nickname: String = "", avatarURL: String? = nil, userInfo: UserInfo? = nil) { + self.nickname = nickname + self.avatarURL = avatarURL + self.userInfo = userInfo + } } enum Action: Equatable { @@ -52,6 +59,7 @@ struct AppSettingFeature { case updateUser(Result) case nicknameInputChanged(String) case nicknameEditAlert(Bool) + case testPushTapped } @Dependency(\.apiService) var apiService @@ -77,27 +85,27 @@ struct AppSettingFeature { state.isLoadingUserInfo = true state.userInfoError = nil return .run { send in - do { +// do { if let userInfo = await UserInfoManager.getUserInfo() { await send(.userInfoResponse(.success(userInfo))) } else { await send(.userInfoResponse(.failure(APIError.custom("用户信息不存在")))) } - } catch { - let apiError: APIError - if let error = error as? APIError { - apiError = error - } else { - apiError = APIError.custom(error.localizedDescription) - } - await send(.userInfoResponse(.failure(apiError))) - } +// } catch { +// let apiError: APIError +// if let error = error as? APIError { +// apiError = error +// } else { +// apiError = APIError.custom(error.localizedDescription) +// } +// await send(.userInfoResponse(.failure(apiError))) +// } } case let .userInfoResponse(.success(userInfo)): state.userInfo = userInfo state.nickname = userInfo.nick ?? "" - state.avatarURL = userInfo.avatar + state.avatarURL = userInfo.avatar // 确保同步 state.isLoadingUserInfo = false return .none @@ -158,6 +166,8 @@ struct AppSettingFeature { state.isUpdatingUser = true state.updateUserError = nil guard let userInfo = state.userInfo else { return .none } + // 头像上传后,先临时更新本地avatarURL,提升UI响应 + state.avatarURL = url return .run { send in let ticket = await UserInfoManager.getCurrentUserTicket() ?? "" let req = UpdateUserRequest(avatar: url, nick: nil, uid: userInfo.uid ?? 0, ticket: ticket) @@ -195,7 +205,7 @@ struct AppSettingFeature { await send(.updateUser(.failure(apiError))) } } - case let .updateUser(.success(resp)): + case .updateUser(.success(_)): state.isUpdatingUser = false // 不直接用 resp.data,触发拉取完整 userinfo,延迟1秒 if let uid = state.userInfo?.uid { @@ -214,6 +224,8 @@ struct AppSettingFeature { state.isUpdatingUser = false state.updateUserError = error.localizedDescription return .none + case .testPushTapped: + return .none } } } diff --git a/yana/Features/FeedListFeature.swift b/yana/Features/FeedListFeature.swift index 6bd493c..1034b15 100644 --- a/yana/Features/FeedListFeature.swift +++ b/yana/Features/FeedListFeature.swift @@ -27,6 +27,7 @@ struct FeedListFeature { case loadMoreResponse(TaskResult) case editFeedButtonTapped // 新增:点击 add 按钮 case editFeedDismissed // 新增:关闭编辑页 + case testButtonTapped // 新增:点击测试按钮 // 新增:动态内容相关 case fetchFeeds case fetchFeedsResponse(TaskResult) @@ -125,6 +126,9 @@ struct FeedListFeature { case .editFeedDismissed: state.isEditFeedPresented = false return .none + case .testButtonTapped: + debugInfoSync("[LOG] FeedListFeature testButtonTapped") + return .none } } } diff --git a/yana/Features/MainFeature.swift b/yana/Features/MainFeature.swift index af5c99c..c02f4fd 100644 --- a/yana/Features/MainFeature.swift +++ b/yana/Features/MainFeature.swift @@ -22,9 +22,9 @@ struct MainFeature { } // 新增:导航目标 - enum Destination: Hashable, Equatable { - case test + enum Destination: Hashable, Codable, CaseIterable { case appSetting + case testView } @CasePathable @@ -36,7 +36,6 @@ struct MainFeature { case accountModelLoaded(AccountModel?) // 新增:导航相关 case navigationPathChanged([Destination]) - case testButtonTapped case appSettingButtonTapped case appSettingAction(AppSettingFeature.Action) // 新增:登出 @@ -51,6 +50,8 @@ struct MainFeature { MeFeature() } Reduce { state, action in + debugInfoSync("MainFeature action: \(action)") + debugInfoSync("MainFeature state: \(state)") switch action { case .onAppear: return .run { send in @@ -68,36 +69,43 @@ struct MainFeature { return .send(.me(.onAppear)) } return .none + case .feedList(.testButtonTapped): + state.navigationPath.append(.testView) + return .none case .feedList: return .none case let .accountModelLoaded(accountModel): state.accountModel = accountModel return .none case .me(.settingButtonTapped): - // 触发 push 到设置页 - state.appSettingState = AppSettingFeature.State() + // 触发 push 到设置页,带入当前用户信息 + let userInfo = state.me.userInfo + let avatarURL = userInfo?.avatar + let nickname = userInfo?.nick ?? "" + state.appSettingState = AppSettingFeature.State(nickname: nickname, avatarURL: avatarURL, userInfo: userInfo) state.navigationPath.append(.appSetting) + debugInfoSync("\(state.navigationPath)") return .none case .me: return .none case .navigationPathChanged(let newPath): // pop 回来时清空 settingState - if !newPath.contains(.appSetting) { - state.appSettingState = nil - } state.navigationPath = newPath return .none - case .testButtonTapped: - state.navigationPath.append(.test) - return .none case .appSettingButtonTapped: - state.appSettingState = AppSettingFeature.State() + let userInfo = state.me.userInfo + let avatarURL = userInfo?.avatar + let nickname = userInfo?.nick ?? "" + state.appSettingState = AppSettingFeature.State(nickname: nickname, avatarURL: avatarURL, userInfo: userInfo) state.navigationPath.append(.appSetting) return .none case .appSettingAction(.logoutTapped): // 监听到登出,设置登出标志 state.isLoggedOut = true return .none + case .appSettingAction(.updateUser(.success)): + // 设置页用户信息变更成功,刷新Me页数据 + return .send(.me(.refresh)) case .appSettingAction: return .none case .logout: @@ -110,4 +118,4 @@ struct MainFeature { AppSettingFeature() } } -} +} diff --git a/yana/Features/MeFeature.swift b/yana/Features/MeFeature.swift index e8589ba..5695cf2 100644 --- a/yana/Features/MeFeature.swift +++ b/yana/Features/MeFeature.swift @@ -87,15 +87,15 @@ struct MeFeature { private func fetchUserInfo(uid: Int) -> Effect { .run { send in - do { - if let userInfo = try await UserInfoManager.fetchUserInfoFromServer(uid: String(uid), apiService: apiService) { +// do { + if let userInfo = await UserInfoManager.fetchUserInfoFromServer(uid: String(uid), apiService: apiService) { await send(.userInfoResponse(.success(userInfo))) } else { await send(.userInfoResponse(.failure(.noData))) } - } catch { - await send(.userInfoResponse(.failure(error as? APIError ?? .unknown(error.localizedDescription)))) - } +// } catch { +// await send(.userInfoResponse(.failure(error as? APIError ?? .unknown(error.localizedDescription)))) +// } } } @@ -110,4 +110,4 @@ struct MeFeature { } } } -} \ No newline at end of file +} diff --git a/yana/Views/AppRootView.swift b/yana/Views/AppRootView.swift index d4d24bd..672ec8c 100644 --- a/yana/Views/AppRootView.swift +++ b/yana/Views/AppRootView.swift @@ -27,7 +27,7 @@ struct AppRootView: View { } } } - -#Preview { - AppRootView() -} \ No newline at end of file +// +//#Preview { +// AppRootView() +//} diff --git a/yana/Views/AppSettingView.swift b/yana/Views/AppSettingView.swift index 7de2980..7436241 100644 --- a/yana/Views/AppSettingView.swift +++ b/yana/Views/AppSettingView.swift @@ -1,3 +1,10 @@ +// +// AppSettingView.swift +// yana +// +// Created by Edwin on 2024/11/20. +// + import SwiftUI import ComposableArchitecture import PhotosUI @@ -5,105 +12,133 @@ import PhotosUI struct AppSettingView: View { let store: StoreOf @State private var showPhotoPicker = false - @State private var selectedPhotoItem: PhotosPickerItem? = nil @State private var showNicknameAlert = false - @State private var nicknameInput: String = "" + @State private var nicknameInput = "" + @State private var selectedPhotoItem: PhotosPickerItem? var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in - ZStack { - Color(red: 22/255, green: 17/255, blue: 44/255).ignoresSafeArea() - VStack(spacing: 0) { - // 主要内容 - VStack(spacing: 0) { +// WithPerceptionTracking { + WithViewStore(store, observe: { $0 }) { viewStore in + WithPerceptionTracking{ + mainContent(viewStore: viewStore) + .photosPicker( + isPresented: $showPhotoPicker, + selection: $selectedPhotoItem, + matching: .images, + photoLibrary: .shared() + ) + .onChange(of: selectedPhotoItem) { item in + handlePhotoSelection(item: item, viewStore: viewStore) + selectedPhotoItem = nil + } + .alert("修改昵称", isPresented: $showNicknameAlert) { + nicknameAlertContent(viewStore: viewStore) + } message: { + Text("昵称最长15个字符") + } + .sheet(isPresented: userAgreementBinding(viewStore: viewStore)) { + WebView(url: URL(string: "https://www.yana.com/user-agreement")!) + } + .sheet(isPresented: privacyPolicyBinding(viewStore: viewStore)) { + WebView(url: URL(string: "https://www.yana.com/privacy-policy")!) + } + } + + } +// } + } + + // MARK: - 主要内容 + private func mainContent(viewStore: ViewStoreOf) -> some View { + ZStack { + Color(red: 22/255, green: 17/255, blue: 44/255).ignoresSafeArea() + VStack(spacing: 0) { + // 调试信息 +// Text("nickname: \(viewStore.nickname)\navatarURL: \(String(describing: viewStore.avatarURL))\nuserInfo: \(String(describing: viewStore.userInfo))\nuserInfoError: \(String(describing: viewStore.userInfoError))") +// .foregroundColor(.yellow) +// .font(.system(size: 12)) +// .padding(8) + // 顶部栏 + topBar + // 测试跳转按钮 + Button("测试跳转TestPushView") { + viewStore.send(.testPushTapped) + } + .padding() + + ScrollView { + VStack(spacing: 32) { // 头像区域 avatarSection(viewStore: viewStore) + // 昵称设置项 nicknameSection(viewStore: viewStore) - // 其他设置项 + + // 设置项区域 settingsSection(viewStore: viewStore) - Spacer() - // 底部大按钮 + + // 退出登录按钮 logoutButton(viewStore: viewStore) } } - // Loading & 错误提示 - if viewStore.isUploadingAvatar || viewStore.isUpdatingUser { - Color.black.opacity(0.3).ignoresSafeArea() - ProgressView("正在上传/更新...") - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .foregroundColor(.white) - .scaleEffect(1.2) - } - if let error = viewStore.avatarUploadError ?? viewStore.updateUserError { - VStack { - Spacer() - Text(error) - .foregroundColor(.red) - .padding() - .background(Color.white.opacity(0.9)) - .cornerRadius(10) - Spacer() - } - } } - .navigationTitle(NSLocalizedString("appSetting.title", comment: "Edit")) - .navigationBarTitleDisplayMode(.inline) - .toolbarBackground(.hidden, for: .navigationBar) - .toolbarColorScheme(.dark, for: .navigationBar) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button(action: {}) { - Image(systemName: "chevron.left") - .font(.system(size: 24, weight: .medium)) - .foregroundColor(.white) - } - } - } - .onAppear { - viewStore.send(.onAppear) - } - .webView( - isPresented: userAgreementBinding(viewStore: viewStore), - url: APIConfiguration.webURL(for: .userAgreement) - ) - .webView( - isPresented: privacyPolicyBinding(viewStore: viewStore), - url: APIConfiguration.webURL(for: .privacyPolicy) - ) - // 头像选择器 - .photosPicker(isPresented: $showPhotoPicker, selection: $selectedPhotoItem, matching: .images, photoLibrary: .shared()) - .onChange(of: selectedPhotoItem) { newItem in - if let item = newItem { - loadAndProcessImage(item: item) { data in - if let data = data { - viewStore.send(.avatarSelected(data)) - } - } - } - } - // 昵称修改Alert - .alert("修改昵称", isPresented: $showNicknameAlert, actions: { - TextField("请输入昵称", text: $nicknameInput) - .onChange(of: nicknameInput) { newValue in - if newValue.count > 15 { - nicknameInput = String(newValue.prefix(15)) - } - } - Button("确定") { - let trimmed = nicknameInput.trimmingCharacters(in: .whitespacesAndNewlines) - if !trimmed.isEmpty && trimmed != viewStore.nickname { - viewStore.send(.nicknameEditConfirmed(trimmed)) - } - } - Button("取消", role: .cancel) {} - }, message: { - Text("昵称最长15个字符") - }) } } - + // MARK: - 照片选择处理 + private func handlePhotoSelection(item: PhotosPickerItem?, viewStore: ViewStoreOf) { + if let item = item { + loadAndProcessImage(item: item) { data in + if let data = data { + viewStore.send(.avatarSelected(data)) + } + } + } + } + + // MARK: - 昵称Alert内容 + @ViewBuilder + private func nicknameAlertContent(viewStore: ViewStoreOf) -> some View { + TextField("请输入昵称", text: $nicknameInput) + .onChange(of: nicknameInput) { newValue in + if newValue.count > 15 { + nicknameInput = String(newValue.prefix(15)) + } + } + Button("确定") { + let trimmed = nicknameInput.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmed.isEmpty && trimmed != viewStore.nickname { + viewStore.send(.nicknameEditConfirmed(trimmed)) + } + } + Button("取消", role: .cancel) {} + } + + // MARK: - 顶部栏 + private var topBar: some View { + HStack { + Button(action: {}) { + Image(systemName: "chevron.left") + .foregroundColor(.white) + .font(.system(size: 20, weight: .medium)) + } + + Spacer() + + Text(NSLocalizedString("appSetting.title", comment: "Settings")) + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.white) + + Spacer() + + // 占位符,保持标题居中 + Color.clear + .frame(width: 20, height: 20) + } + .padding(.horizontal, 20) + .padding(.top, 8) + .padding(.bottom, 16) + } // MARK: - 头像区域 private func avatarSection(viewStore: ViewStoreOf) -> some View { @@ -341,4 +376,4 @@ private func resizeImage(image: UIImage, targetSize: CGSize) -> UIImage { return renderer.image { _ in image.draw(in: CGRect(origin: .zero, size: targetSize)) } -} +} diff --git a/yana/Views/FeedListView.swift b/yana/Views/FeedListView.swift index b4f606d..4e8b558 100644 --- a/yana/Views/FeedListView.swift +++ b/yana/Views/FeedListView.swift @@ -7,7 +7,8 @@ struct FeedListView: View { var body: some View { WithViewStore(self.store, observe: { $0 }) { viewStore in - GeometryReader { geometry in + WithPerceptionTracking { + GeometryReader { geometry in ZStack { // 背景图片 Image("bg") @@ -19,6 +20,17 @@ struct FeedListView: View { VStack(alignment: .center, spacing: 0) { // 顶部栏 HStack { + Button(action: { + viewStore.send(.testButtonTapped) + }) { + Text("测试") + .font(.system(size: 14)) + .foregroundColor(.white) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color.blue.opacity(0.7)) + .cornerRadius(8) + } Spacer(minLength: 0) Spacer(minLength: 0) Text(NSLocalizedString("feedList.title", comment: "Enjoy your Life Time")) @@ -66,28 +78,30 @@ struct FeedListView: View { .padding(.top, 20) } else { ScrollView { - LazyVStack(spacing: 16) { - ForEach(Array(viewStore.moments.enumerated()), id: \ .element.dynamicId) { index, moment in - OptimizedDynamicCardView(moment: moment, allMoments: viewStore.moments, currentIndex: index) - // 上拉加载更多触发点 - if index == viewStore.moments.count - 1, viewStore.hasMore, !viewStore.isLoadingMore { - Color.clear - .frame(height: 1) - .onAppear { - viewStore.send(.loadMore) - } + WithPerceptionTracking { + LazyVStack(spacing: 16) { + ForEach(Array(viewStore.moments.enumerated()), id: \ .element.dynamicId) { index, moment in + OptimizedDynamicCardView(moment: moment, allMoments: viewStore.moments, currentIndex: index) + // 上拉加载更多触发点 + if index == viewStore.moments.count - 1, viewStore.hasMore, !viewStore.isLoadingMore { + Color.clear + .frame(height: 1) + .onAppear { + viewStore.send(.loadMore) + } + } + } + // 加载更多指示器 + if viewStore.isLoadingMore { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .padding(.vertical, 8) } } - // 加载更多指示器 - if viewStore.isLoadingMore { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .padding(.vertical, 8) - } + .padding(.horizontal, 16) + .padding(.top, 10) + .padding(.bottom, 20) } - .padding(.horizontal, 16) - .padding(.top, 10) - .padding(.bottom, 20) } .refreshable { viewStore.send(.reload) @@ -119,6 +133,7 @@ struct FeedListView: View { } ) } + } } } -} +} diff --git a/yana/Views/LanguageSettingsView.swift b/yana/Views/LanguageSettingsView.swift index a0adf3c..15393ff 100644 --- a/yana/Views/LanguageSettingsView.swift +++ b/yana/Views/LanguageSettingsView.swift @@ -95,7 +95,7 @@ struct LanguageSettingsView: View { } private func testCOToken() async { - do { +// do { let token = await cosManager.getToken(apiService: apiService) if let token = token { print("✅ Token 测试成功") @@ -105,9 +105,9 @@ struct LanguageSettingsView: View { } else { print("❌ Token 测试失败: 未能获取 Token") } - } catch { - print("❌ Token 测试异常: \(error.localizedDescription)") - } +// } catch { +// print("❌ Token 测试异常: \(error.localizedDescription)") +// } } } diff --git a/yana/Views/MainView.swift b/yana/Views/MainView.swift index a1513f1..1dffee2 100644 --- a/yana/Views/MainView.swift +++ b/yana/Views/MainView.swift @@ -7,91 +7,102 @@ struct MainView: View { var body: some View { WithViewStore(self.store, observe: { $0 }) { viewStore in - InternalMainView(store: store) - .onChange(of: viewStore.isLoggedOut) { isLoggedOut in - if isLoggedOut { - onLogout?() + WithPerceptionTracking { + InternalMainView(store: store) + .onChange(of: viewStore.isLoggedOut) { isLoggedOut in + if isLoggedOut { + onLogout?() + } } - } + } } } } -// 原 MainView 内容,重命名为 InternalMainView struct InternalMainView: View { let store: StoreOf + @State private var path: [MainFeature.Destination] = [] + init(store: StoreOf) { + self.store = store + _path = State(initialValue: store.withState { $0.navigationPath }) + } var body: some View { WithViewStore(self.store, observe: { $0 }) { viewStore in - NavigationStack(path: viewStore.binding(get: \.navigationPath, send: MainFeature.Action.navigationPathChanged)) { - GeometryReader { geometry in - ZStack { - // 背景图片 - Image("bg") - .resizable() - .aspectRatio(contentMode: .fill) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .clipped() - .ignoresSafeArea(.all) - // 主内容 - ZStack { - FeedListView(store: store.scope( - state: \.feedList, - action: \.feedList - )) - .isHidden(viewStore.selectedTab != .feed) - MeView( - store: store.scope( - state: \.me, - action: \.me - ) - ) - .isHidden(viewStore.selectedTab != .other) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - // 底部导航栏 - VStack { - Spacer() - BottomTabView(selectedTab: viewStore.binding( - get: { Tab(rawValue: $0.selectedTab.rawValue) ?? .feed }, - send: { MainFeature.Action.selectTab(MainFeature.Tab(rawValue: $0.rawValue) ?? .feed) } - )) - } - .padding(.bottom, geometry.safeAreaInsets.bottom + 60) - } - } - .navigationDestination(for: MainFeature.Destination.self) { destination in - switch destination { - case .test: - TestPushView() - case .appSetting: - IfLetStore( - self.store.scope( - state: \.appSettingState, - action: MainFeature.Action.appSettingAction - ), - then: { appSettingStore in - WithPerceptionTracking { - AppSettingView(store: appSettingStore) + WithPerceptionTracking { + NavigationStack(path: $path) { + GeometryReader { geometry in + contentView(geometry: geometry, viewStore: viewStore) + .navigationDestination(for: MainFeature.Destination.self) { destination in + debugLogSync("[log] navigationDestination: \(destination)") + let view: AnyView + switch destination { + case .appSetting: + if let appSettingStore = store.scope(state: \.appSettingState, action: \.appSettingAction) { + view = AnyView(AppSettingView(store: appSettingStore)) + } else { + view = AnyView(Text("appSettingState is nil")) + } + case .testView: + debugLogSync("[log] navigationDestination: .testView 渲染") + view = AnyView(TestView()) + } + return view + } + .onChange(of: path) { newPath in + debugLogSync("[log] path changed: \(type(of: path)) = \(path)") + viewStore.send(.navigationPathChanged(newPath)) + } + .onChange(of: viewStore.navigationPath) { newPath in + debugLogSync("[log] viewStore.navigationPath changed: \(type(of: newPath)) = \(newPath)") + if path != newPath { + path = newPath } } - ) + .onAppear { + viewStore.send(.onAppear) + } } } } - .onAppear { - viewStore.send(.onAppear) + } + } + + private func contentView(geometry: GeometryProxy, viewStore: ViewStoreOf) -> some View { + WithPerceptionTracking { + ZStack { + // 背景图片 + Image("bg") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .clipped() + .ignoresSafeArea(.all) + // 主内容 + ZStack { + FeedListView(store: store.scope( + state: \.feedList, + action: \.feedList + )) + .isHidden(viewStore.selectedTab != .feed) + MeView( + store: store.scope( + state: \.me, + action: \.me + ) + ) + .isHidden(viewStore.selectedTab != .other) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + // 底部导航栏 + VStack { + Spacer() + BottomTabView(selectedTab: viewStore.binding( + get: { Tab(rawValue: $0.selectedTab.rawValue) ?? .feed }, + send: { MainFeature.Action.selectTab(MainFeature.Tab(rawValue: $0.rawValue) ?? .feed) } + )) + } + .padding(.bottom, geometry.safeAreaInsets.bottom + 60) } } } } - -struct TestPushView: View { - var body: some View { - ZStack { - Color.blue.ignoresSafeArea() - Text("Test Push View") - .font(.largeTitle) - .foregroundColor(.white) - } - } -} diff --git a/yana/Views/MeView.swift b/yana/Views/MeView.swift index 2ad7d68..604c9ba 100644 --- a/yana/Views/MeView.swift +++ b/yana/Views/MeView.swift @@ -5,7 +5,8 @@ struct MeView: View { let store: StoreOf var body: some View { - GeometryReader { geometry in + WithPerceptionTracking { + GeometryReader { geometry in ZStack { Image("bg") .resizable() @@ -93,19 +94,21 @@ struct MeView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } else { ScrollView { - LazyVStack(spacing: 12) { - ForEach(Array(viewStore.moments.enumerated()), id: \ .element.dynamicId) { index, moment in - OptimizedDynamicCardView(moment: moment, allMoments: viewStore.moments, currentIndex: index) - .padding(.horizontal, 12) - } - if viewStore.hasMore { - ProgressView() - .onAppear { - viewStore.send(.loadMore) - } + WithPerceptionTracking { + LazyVStack(spacing: 12) { + ForEach(Array(viewStore.moments.enumerated()), id: \.element.dynamicId) { index, moment in + OptimizedDynamicCardView(moment: moment, allMoments: viewStore.moments, currentIndex: index) + .padding(.horizontal, 12) + } + if viewStore.hasMore { + ProgressView() + .onAppear { + viewStore.send(.loadMore) + } + } } + .padding(.top, 8) } - .padding(.top, 8) } .refreshable { viewStore.send(.refresh) @@ -116,9 +119,10 @@ struct MeView: View { } .frame(maxWidth: .infinity, alignment: .top) } - } - .onAppear { - ViewStore(self.store, observe: { $0 }).send(.onAppear) + } + .onAppear { + ViewStore(self.store, observe: { $0 }).send(.onAppear) + } } } } diff --git a/yana/Views/TEST/TestView.swift b/yana/Views/TEST/TestView.swift new file mode 100644 index 0000000..2ddeefb --- /dev/null +++ b/yana/Views/TEST/TestView.swift @@ -0,0 +1,61 @@ +import SwiftUI + +struct TestView: View { + var body: some View { + ZStack { + // 背景色 + Color.purple.ignoresSafeArea() + + VStack(spacing: 30) { + // 标题 + Text("测试页面") + .font(.system(size: 32, weight: .bold)) + .foregroundColor(.white) + + // 描述文本 + Text("这是一个测试用的页面\n用于验证导航跳转功能") + .font(.system(size: 18)) + .foregroundColor(.white.opacity(0.9)) + .multilineTextAlignment(.center) + + // 测试按钮 + Button(action: { + debugInfoSync("[LOG] TestView button tapped") + }) { + Text("测试按钮") + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.purple) + .padding(.horizontal, 24) + .padding(.vertical, 12) + .background(Color.white) + .cornerRadius(8) + } + + Spacer() + } + .padding(.top, 100) + } + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button(action: { + debugInfoSync("[LOG] TestView back button tapped") + }) { + HStack(spacing: 4) { + Image(systemName: "chevron.left") + .font(.system(size: 16, weight: .medium)) + Text("返回") + .font(.system(size: 16)) + } + .foregroundColor(.white) + } + } + } + } +} + +#Preview { + NavigationStack { + TestView() + } +} \ No newline at end of file