diff --git a/.cursor/rules/swift-assistant-style.mdc b/.cursor/rules/swift-assistant-style.mdc index 430c95e..0aa5698 100644 --- a/.cursor/rules/swift-assistant-style.mdc +++ b/.cursor/rules/swift-assistant-style.mdc @@ -5,7 +5,7 @@ alwaysApply: true --- # Background -This project is based on iOS 16.0+, SwiftUI, and TCA 1.20.2 +This project is based on iOS 17.0+, SwiftUI, and TCA 1.20.2 I would like advice on using the latest tools and seek step-by-step guidance to fully understand the implementation process. diff --git a/README.md b/README.md index 949a590..8ed95b7 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Yana 是一个基于 iOS 平台的即时通讯应用,使用 Swift 语言开发 ## 技术栈 - **开发语言**:Swift (主要),Objective-C (部分组件) -- **最低支持版本**:iOS 16 +- **最低支持版本**:iOS 17 - **架构模式**:The Composable Architecture (TCA) - 1.20.2 - **UI 框架**:SwiftUI - **依赖管理**: @@ -45,7 +45,7 @@ yana/ ## 环境要求 - Xcode 13.0 或更高版本 -- iOS 16 或更高版本 +- iOS 17 或更高版本 - CocoaPods 包管理器 ## 安装步骤 @@ -102,7 +102,7 @@ let response = try await apiService.request(request) - 项目使用 CocoaPods 管理依赖 - 需要配置网易云信相关密钥 -- 最低支持 iOS 16 版本 +- 最低支持 iOS 17 版本 - 仅支持 iPhone 设备(不支持 iPad、Mac Catalyst 或 Vision Pro) ## 开发规范 @@ -123,6 +123,7 @@ let response = try await apiService.request(request) ## 构建配置 - 项目使用动态框架 -- 支持 iOS 16 及以上版本 +- 支持 iOS 17 及以上版本 - Swift 版本:6.0 -- 已配置框架冲突处理脚本 \ No newline at end of file +- 已配置框架冲突处理脚本 +- \ No newline at end of file diff --git a/yana.xcodeproj/project.pbxproj b/yana.xcodeproj/project.pbxproj index 8ce9f02..558d125 100644 --- a/yana.xcodeproj/project.pbxproj +++ b/yana.xcodeproj/project.pbxproj @@ -49,8 +49,6 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ 4C4C8FBE2DE5AF9200384527 /* yanaAPITests */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); path = yanaAPITests; sourceTree = ""; }; @@ -255,10 +253,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"; @@ -272,10 +274,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"; diff --git a/yana/Configs/AppConfig.swift b/yana/Configs/AppConfig.swift index 4e60e80..7390a74 100644 --- a/yana/Configs/AppConfig.swift +++ b/yana/Configs/AppConfig.swift @@ -1,10 +1,10 @@ -enum Environment { +enum AppEnvironment { case development case production } struct AppConfig { - static let current: Environment = { + static let current: AppEnvironment = { #if DEBUG return .development #else diff --git a/yana/ContentView.swift b/yana/ContentView.swift index 5f1c9d4..475d316 100644 --- a/yana/ContentView.swift +++ b/yana/ContentView.swift @@ -187,8 +187,8 @@ struct ContentView: View { } .tag(1) } - .onChange(of: selectedLogLevel) { newValue in - APILogger.logLevel = newValue + .onChange(of: selectedLogLevel) { + APILogger.logLevel = selectedLogLevel } } } diff --git a/yana/Features/AppSettingFeature.swift b/yana/Features/AppSettingFeature.swift index 6109a36..e1f9267 100644 --- a/yana/Features/AppSettingFeature.swift +++ b/yana/Features/AppSettingFeature.swift @@ -24,7 +24,12 @@ struct AppSettingFeature { var isUpdatingUser: Bool = false var updateUserError: String? = nil - // 新增:带userInfo、avatarURL、nickname的init + // 默认初始化器 + init() { + // 默认初始化 + } + + // 带userInfo、avatarURL、nickname的init init(nickname: String = "", avatarURL: String? = nil, userInfo: UserInfo? = nil) { self.nickname = nickname self.avatarURL = avatarURL diff --git a/yana/Features/ConfigFeature.swift b/yana/Features/ConfigFeature.swift index 9989c3b..b2d54e8 100644 --- a/yana/Features/ConfigFeature.swift +++ b/yana/Features/ConfigFeature.swift @@ -40,6 +40,10 @@ struct ConfigFeature { var configData: ConfigData? var errorMessage: String? var lastUpdated: Date? + + init() { + // 默认初始化 + } } enum Action: Equatable { diff --git a/yana/Features/CreateFeedFeature.swift b/yana/Features/CreateFeedFeature.swift index fa035b6..7585e80 100644 --- a/yana/Features/CreateFeedFeature.swift +++ b/yana/Features/CreateFeedFeature.swift @@ -19,6 +19,10 @@ struct CreateFeedFeature { !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isLoading } var isLoading: Bool = false + + init() { + // 默认初始化 + } } enum Action { diff --git a/yana/Features/EMailLoginFeature.swift b/yana/Features/EMailLoginFeature.swift index a03e58f..f536b4b 100644 --- a/yana/Features/EMailLoginFeature.swift +++ b/yana/Features/EMailLoginFeature.swift @@ -20,13 +20,11 @@ struct EMailLoginFeature { case failed } - #if DEBUG init() { - self.email = "exzero@126.com" + self.email = "" self.verificationCode = "" self.loginStep = .initial } - #endif } enum Action { diff --git a/yana/Features/EditFeedFeature.swift b/yana/Features/EditFeedFeature.swift index 782a29d..8779301 100644 --- a/yana/Features/EditFeedFeature.swift +++ b/yana/Features/EditFeedFeature.swift @@ -25,6 +25,20 @@ struct EditFeedFeature { var isUploadingImages: Bool = false var imageUploadProgress: Double = 0.0 // 0.0~1.0 var uploadedResList: [ResListItem] = [] + + // 新增:PhotosPicker相关状态 + var showPhotosPicker: Bool = false + var selectedPhotoItems: [PhotosPickerItem] = [] + + // 新增:删除图片确认相关状态 + var showDeleteImageAlert: Bool = false + var imageToDeleteIndex: Int? = nil + + // 默认初始化器 + init() { + // 默认初始化 + } + // 手动实现Equatable,selectedImages只比较数量(PhotosPickerItem不支持Equatable) static func == (lhs: State, rhs: State) -> Bool { lhs.content == rhs.content && @@ -35,7 +49,11 @@ struct EditFeedFeature { lhs.selectedImages.count == rhs.selectedImages.count && lhs.isUploadingImages == rhs.isUploadingImages && lhs.imageUploadProgress == rhs.imageUploadProgress && - lhs.uploadedResList == rhs.uploadedResList + lhs.uploadedResList == rhs.uploadedResList && + lhs.showPhotosPicker == rhs.showPhotosPicker && + lhs.selectedPhotoItems.count == rhs.selectedPhotoItems.count && + lhs.showDeleteImageAlert == rhs.showDeleteImageAlert && + lhs.imageToDeleteIndex == rhs.imageToDeleteIndex } } @@ -56,6 +74,12 @@ struct EditFeedFeature { case uploadImagesResponse(Result<[ResListItem], Error>) // 新增:图片上传进度 case updateImageUploadProgress(Double) + // 新增:PhotosPicker相关Action + case photosPickerDismissed + case addImageButtonTapped + // 新增:删除图片确认相关Action + case showDeleteImageAlert(Int) + case deleteImageAlertDismissed } @Dependency(\.apiService) var apiService @@ -176,6 +200,7 @@ struct EditFeedFeature { return .none case .photosPickerItemsChanged(let items): state.selectedImages = items + state.selectedPhotoItems = items return .run { send in await send(.processPhotosPickerItems(items)) } @@ -203,11 +228,30 @@ struct EditFeedFeature { if index < state.selectedImages.count { state.selectedImages.remove(at: index) } + if index < state.selectedPhotoItems.count { + state.selectedPhotoItems.remove(at: index) + } return .none // 新增:图片上传进度 case .updateImageUploadProgress(let progress): state.imageUploadProgress = progress return .none + // 新增:PhotosPicker相关Action + case .photosPickerDismissed: + state.showPhotosPicker = false + return .none + case .addImageButtonTapped: + state.showPhotosPicker = true + return .none + // 新增:删除图片确认相关Action + case .showDeleteImageAlert(let index): + state.imageToDeleteIndex = index + state.showDeleteImageAlert = true + return .none + case .deleteImageAlertDismissed: + state.showDeleteImageAlert = false + state.imageToDeleteIndex = nil + return .none } } } diff --git a/yana/Features/FeedListFeature.swift b/yana/Features/FeedListFeature.swift index 2853d3b..e682d5e 100644 --- a/yana/Features/FeedListFeature.swift +++ b/yana/Features/FeedListFeature.swift @@ -4,6 +4,7 @@ import ComposableArchitecture @Reducer struct FeedListFeature { @Dependency(\.apiService) var apiService + @ObservableState struct State: Equatable { var isFirstLoad: Bool = true var feeds: [Feed] = [] // 预留 feed 内容 @@ -23,6 +24,10 @@ struct FeedListFeature { var selectedMoment: MomentsInfo? // 新增:点赞相关状态 var likeLoadingDynamicIds: Set = [] + + init() { + // 默认初始化 + } } enum Action: Equatable { diff --git a/yana/Features/IDLoginFeature.swift b/yana/Features/IDLoginFeature.swift index e0cac39..6a32567 100644 --- a/yana/Features/IDLoginFeature.swift +++ b/yana/Features/IDLoginFeature.swift @@ -25,15 +25,15 @@ struct IDLoginFeature { case failed // 认证失败 } - #if DEBUG init() { - self.userID = "2356814" - self.password = "a123456" + self.userID = "" + self.password = "" } - #endif } enum Action: Equatable { + case userIDChanged(String) + case passwordChanged(String) case togglePasswordVisibility case loginButtonTapped(userID: String, password: String) case forgotPasswordTapped @@ -52,6 +52,12 @@ struct IDLoginFeature { var body: some ReducerOf { Reduce { state, action in switch action { + case let .userIDChanged(userID): + state.userID = userID + return .none + case let .passwordChanged(password): + state.password = password + return .none case .togglePasswordVisibility: state.isPasswordVisible.toggle() return .none diff --git a/yana/Features/InitFeature.swift b/yana/Features/InitFeature.swift index ca45454..b2c41ab 100644 --- a/yana/Features/InitFeature.swift +++ b/yana/Features/InitFeature.swift @@ -8,6 +8,10 @@ struct InitFeature { var isLoading = false var response: InitResponse? var error: String? + + init() { + // 默认初始化 + } } enum Action: Equatable { diff --git a/yana/Features/LoginFeature.swift b/yana/Features/LoginFeature.swift index 02258c3..4d24afb 100644 --- a/yana/Features/LoginFeature.swift +++ b/yana/Features/LoginFeature.swift @@ -34,13 +34,11 @@ struct LoginFeature { case failed // 认证失败 } - #if DEBUG init() { - // 移除测试用的硬编码凭据 + // 默认初始化 self.account = "" self.password = "" } - #endif } enum Action { diff --git a/yana/Features/MainFeature.swift b/yana/Features/MainFeature.swift index 46b9560..bb57f6e 100644 --- a/yana/Features/MainFeature.swift +++ b/yana/Features/MainFeature.swift @@ -19,6 +19,10 @@ struct MainFeature { var appSettingState: AppSettingFeature.State? = nil // 新增:登出标志 var isLoggedOut: Bool = false + + init() { + // 默认初始化 + } } // 新增:导航目标 diff --git a/yana/Features/MeDynamicFeature.swift b/yana/Features/MeDynamicFeature.swift index c9126a8..1c46ea8 100644 --- a/yana/Features/MeDynamicFeature.swift +++ b/yana/Features/MeDynamicFeature.swift @@ -14,6 +14,10 @@ struct MeDynamicFeature: Reducer { var hasMore: Bool = true var error: String? var isInitialized: Bool = false // 首次加载标记 + + init(uid: Int = 0) { + self.uid = uid + } } enum Action: Equatable { diff --git a/yana/Features/MeFeature.swift b/yana/Features/MeFeature.swift index da6bd2f..833b673 100644 --- a/yana/Features/MeFeature.swift +++ b/yana/Features/MeFeature.swift @@ -4,6 +4,7 @@ import ComposableArchitecture @Reducer struct MeFeature { @Dependency(\.apiService) var apiService + @ObservableState struct State: Equatable { var isFirstLoad: Bool = true var userInfo: UserInfo? @@ -21,6 +22,10 @@ struct MeFeature { // 新增:DetailView相关状态 var showDetail: Bool = false var selectedMoment: MomentsInfo? + + init() { + // 默认初始化 + } } enum Action: Equatable { diff --git a/yana/Features/SplashFeature.swift b/yana/Features/SplashFeature.swift index 565c73e..6c70321 100644 --- a/yana/Features/SplashFeature.swift +++ b/yana/Features/SplashFeature.swift @@ -12,6 +12,10 @@ struct SplashFeature { // 新增:导航目标 var navigationDestination: NavigationDestination? + + init() { + // 默认初始化 + } } // 新增:导航目标枚举 diff --git a/yana/Views/DetailView.swift b/yana/Views/DetailView.swift index 37daa86..477aa11 100644 --- a/yana/Views/DetailView.swift +++ b/yana/Views/DetailView.swift @@ -4,12 +4,10 @@ import ComposableArchitecture struct DetailView: View { @State var store: StoreOf let onLikeSuccess: ((Int, Bool) -> Void)? - let onDismiss: (() -> Void)? // 新增:关闭回调 - init(store: StoreOf, onLikeSuccess: ((Int, Bool) -> Void)? = nil, onDismiss: (() -> Void)? = nil) { + init(store: StoreOf, onLikeSuccess: ((Int, Bool) -> Void)? = nil) { self.store = store self.onLikeSuccess = onLikeSuccess - self.onDismiss = onDismiss } var body: some View { @@ -28,7 +26,7 @@ struct DetailView: View { showDeleteButton: isCurrentUserDynamic, isDeleteLoading: store.isDeleteLoading, onBack: { - onDismiss?() // 调用父视图的关闭回调 + // 移除 onDismiss?() 调用,因为现在使用 dismiss() }, onDelete: { store.send(.deleteDynamic) @@ -72,7 +70,7 @@ struct DetailView: View { .onChange(of: store.shouldDismiss) { shouldDismiss in if shouldDismiss { debugInfoSync("🔍 DetailView: shouldDismiss = true, 调用onDismiss") - onDismiss?() + // 移除 onDismiss?() 移除此行,因为我们现在使用 dismiss } } .fullScreenCover(isPresented: Binding( @@ -117,11 +115,22 @@ struct CustomNavigationBar: View { let isDeleteLoading: Bool let onBack: () -> Void let onDelete: () -> Void + init(title: String, showDeleteButton: Bool, isDeleteLoading: Bool, onBack: @escaping () -> Void, onDelete: @escaping () -> Void) { + self.title = title + self.showDeleteButton = showDeleteButton + self.isDeleteLoading = isDeleteLoading + self.onBack = onBack + self.onDelete = onDelete + } + @SwiftUI.Environment(\.dismiss) private var dismiss: SwiftUI.DismissAction var body: some View { HStack { // 返回按钮 - Button(action: onBack) { + Button(action: { + onBack() + dismiss() // 使用 dismiss 关闭视图 + }) { Image(systemName: "chevron.left") .font(.system(size: 20, weight: .medium)) .foregroundColor(.white) @@ -216,4 +225,4 @@ struct CustomNavigationBar: View { // DetailFeature() // } // ) -//} +//} diff --git a/yana/Views/EMailLoginView.swift b/yana/Views/EMailLoginView.swift index ca632e5..d500901 100644 --- a/yana/Views/EMailLoginView.swift +++ b/yana/Views/EMailLoginView.swift @@ -207,7 +207,7 @@ private struct LoginContentView: View { .keyboardType(.numberPad) Button(action: { - store.send(.getVerificationCodeButtonTapped) + store.send(.getVerificationCodeTapped) }) { Text(getCodeButtonText) .font(.system(size: 14, weight: .medium)) @@ -223,7 +223,7 @@ private struct LoginContentView: View { // 登录按钮 Button(action: { - store.send(.loginButtonTapped) + store.send(.loginButtonTapped(email: email, verificationCode: verificationCode)) }) { if store.isLoading { ProgressView() diff --git a/yana/Views/EditFeedView.swift b/yana/Views/EditFeedView.swift index bccf311..4f723e8 100644 --- a/yana/Views/EditFeedView.swift +++ b/yana/Views/EditFeedView.swift @@ -1,7 +1,6 @@ import SwiftUI import ComposableArchitecture import PhotosUI -//import ImagePreviewPager struct EditFeedView: View { let onDismiss: () -> Void @@ -46,35 +45,26 @@ struct EditFeedView: View { .onAppear { store.send(.clearError) } - .onChange(of: store.shouldDismiss) { shouldDismiss in - if shouldDismiss { + .onChange(of: store.shouldDismiss) { + if store.shouldDismiss { onDismiss() } } .photosPicker( - isPresented: store.binding( - get: \.showPhotosPicker, - send: { _ in .photosPickerDismissed } + isPresented: Binding( + get: { store.showPhotosPicker }, + set: { _ in store.send(.photosPickerDismissed) } ), - selection: store.binding( - get: \.selectedPhotoItems, - send: { .photosPickerItemsChanged($0) } + selection: Binding( + get: { store.selectedPhotoItems }, + set: { store.send(.photosPickerItemsChanged($0)) } ), maxSelectionCount: 9, matching: .images ) - .onChange(of: store.selectedPhotoItems) { items in - store.send(.photosPickerItemsChanged(items)) - } - .onChange(of: store.selectedImages) { images in - // 处理图片选择变化 - } - .onChange(of: store.content) { content in - // 处理内容变化 - } - .alert("删除图片", isPresented: store.binding( - get: \.showDeleteImageAlert, - send: { _ in .deleteImageAlertDismissed } + .alert("删除图片", isPresented: Binding( + get: { store.showDeleteImageAlert }, + set: { _ in store.send(.deleteImageAlertDismissed) } )) { Button("删除", role: .destructive) { if let indexToDelete = store.imageToDeleteIndex { @@ -98,19 +88,12 @@ struct EditFeedView: View { private func mainContent(geometry: GeometryProxy) -> some View { WithPerceptionTracking { VStack(spacing: 0) { - // 顶部导航栏 topNavigationBar - // 主要内容区域 ScrollView { VStack(spacing: 20) { - // 文本输入区域 textInputSection - - // 图片选择区域 imageSelectionSection - - // 发布按钮 publishButton } .padding(.horizontal, 20) @@ -141,7 +124,6 @@ struct EditFeedView: View { Spacer() - // 占位,保持标题居中 Color.clear .frame(width: 44, height: 44) } @@ -158,9 +140,9 @@ struct EditFeedView: View { .font(.system(size: 16, weight: .medium)) .foregroundColor(.white) - TextEditor(text: store.binding( - get: \.content, - send: { .contentChanged($0) } + TextEditor(text: Binding( + get: { store.content }, + set: { store.send(.contentChanged($0)) } )) .font(.system(size: 16)) .foregroundColor(.white) @@ -191,66 +173,19 @@ struct EditFeedView: View { .font(.system(size: 16, weight: .medium)) .foregroundColor(.white) - LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3), spacing: 8) { - // 已选择的图片 - ForEach(Array(store.selectedImages.enumerated()), id: \.offset) { index, image in - imageItem(image: image, index: index) + ImageGrid( + images: store.processedImages, + onRemoveImage: { index in + store.send(.showDeleteImageAlert(index)) + }, + onAddImage: { + store.send(.addImageButtonTapped) } - - // 添加图片按钮 - if store.selectedImages.count < 9 { - addImageButton - } - } + ) } } } - private func imageItem(image: UIImage, index: Int) -> some View { - ZStack(alignment: .topTrailing) { - Image(uiImage: image) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(height: 100) - .clipped() - .cornerRadius(8) - - Button(action: { - store.send(.showDeleteImageAlert(index)) - }) { - Image(systemName: "xmark.circle.fill") - .font(.system(size: 20)) - .foregroundColor(.white) - .background(Color.black.opacity(0.5)) - .clipShape(Circle()) - } - .padding(4) - } - } - - private var addImageButton: some View { - Button(action: { - store.send(.addImageButtonTapped) - }) { - VStack { - Image(systemName: "plus") - .font(.system(size: 24)) - .foregroundColor(.white.opacity(0.7)) - Text("添加") - .font(.system(size: 12)) - .foregroundColor(.white.opacity(0.7)) - } - .frame(height: 100) - .frame(maxWidth: .infinity) - .background(Color.white.opacity(0.1)) - .cornerRadius(8) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color.white.opacity(0.2), lineWidth: 1) - ) - } - } - private var publishButton: some View { WithPerceptionTracking { Button(action: { @@ -310,72 +245,78 @@ struct EditFeedView: View { } } -//#Preview { -// EditFeedView() -//} - -// MARK: - 九宫格图片选择组件 -struct ModernImageSelectionGrid: View { +// MARK: - 简化的图片网格组件 +struct ImageGrid: View { let images: [UIImage] - let selectedItems: [PhotosPickerItem] - let canAddMore: Bool - let onItemsChanged: ([PhotosPickerItem]) -> Void let onRemoveImage: (Int) -> Void + let onAddImage: () -> Void + private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3) - @State private var showPreview = false - @State private var previewIndex = 0 + var body: some View { - let totalSpacing: CGFloat = 8 * 2 - let totalWidth = UIScreen.main.bounds.width - 24 * 2 - totalSpacing - let gridItemSize: CGFloat = totalWidth / 3 LazyVGrid(columns: columns, spacing: 8) { ForEach(Array(images.enumerated()), id: \.offset) { index, image in - ZStack(alignment: .topTrailing) { - Image(uiImage: image) - .resizable() - .aspectRatio(contentMode: .fill) // aspectFill - .frame(width: gridItemSize, height: gridItemSize) - .clipped() - .cornerRadius(12) - .onTapGesture { - previewIndex = index - showPreview = true - } - Button(action: { - onRemoveImage(index) - }) { - Image(systemName: "xmark.circle.fill") - .font(.system(size: 20)) - .foregroundColor(.white) - .background(Color.black.opacity(0.6)) - .clipShape(Circle()) - } - .padding(4) - } + ImageGridItem( + image: image, + onRemove: { onRemoveImage(index) } + ) } - if canAddMore { - PhotosPicker( - selection: .init( - get: { selectedItems }, - set: { items in DispatchQueue.main.async { onItemsChanged(items) } } - ), - maxSelectionCount: 9 - images.count, - matching: .images - ) { - RoundedRectangle(cornerRadius: 12) - .fill(Color(hexString: "1C143A")) - .frame(width: gridItemSize, height: gridItemSize) - .overlay( - Image("add photo") - .resizable() - .frame(width: 40, height: 40) - .opacity(0.6) - ) - } + + if images.count < 9 { + AddImageButton(onTap: onAddImage) } } - .fullScreenCover(isPresented: $showPreview) { - ImagePreviewPager(images: images, currentIndex: $previewIndex, onClose: { showPreview = false }) - } + } +} + +// MARK: - 图片网格项组件 +struct ImageGridItem: View { + let image: UIImage + let onRemove: () -> Void + + var body: some View { + ZStack(alignment: .topTrailing) { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(height: 100) + .clipped() + .cornerRadius(8) + + Button(action: onRemove) { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 20)) + .foregroundColor(.white) + .background(Color.black.opacity(0.5)) + .clipShape(Circle()) + } + .padding(4) + } + } +} + +// MARK: - 添加图片按钮组件 +struct AddImageButton: View { + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + VStack { + Image(systemName: "plus") + .font(.system(size: 24)) + .foregroundColor(.white.opacity(0.7)) + Text("添加") + .font(.system(size: 12)) + .foregroundColor(.white.opacity(0.7)) + } + .frame(height: 100) + .frame(maxWidth: .infinity) + .background(Color.white.opacity(0.1)) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.white.opacity(0.2), lineWidth: 1) + ) + } } } diff --git a/yana/Views/FeedListView.swift b/yana/Views/FeedListView.swift index 05c9b19..bc6f077 100644 --- a/yana/Views/FeedListView.swift +++ b/yana/Views/FeedListView.swift @@ -172,37 +172,35 @@ struct FeedListContentView: View { @Binding var previewCurrentIndex: Int var body: some View { - WithPerceptionTracking { - if store.isLoading { - LoadingView() - } else if let error = store.error { - ErrorView(error: error) - } else if store.moments.isEmpty { - EmptyView() - } else { - MomentsListView( - moments: store.moments, - hasMore: store.hasMore, - isLoadingMore: store.isLoadingMore, - onImageTap: { images, tappedIndex in - previewCurrentIndex = tappedIndex - previewItem = PreviewItem(images: images, index: tappedIndex) - }, - onMomentTap: { moment in - store.send(.showDetail(moment)) - }, - onLikeTap: { dynamicId, uid, likedUid, worldId in - store.send(.likeDynamic(dynamicId, uid, likedUid, worldId)) - }, - onLoadMore: { - store.send(.loadMore) - }, - onRefresh: { - store.send(.reload) - }, - likeLoadingDynamicIds: store.likeLoadingDynamicIds - ) - } + if store.isLoading { + FeedListLoadingView() + } else if let error = store.error { + ErrorView(error: error) + } else if store.moments.isEmpty { + EmptyView() + } else { + MomentsListView( + moments: store.moments, + hasMore: store.hasMore, + isLoadingMore: store.isLoadingMore, + onImageTap: { images, tappedIndex in + previewCurrentIndex = tappedIndex + previewItem = PreviewItem(images: images, index: tappedIndex) + }, + onMomentTap: { moment in + store.send(.showDetail(moment)) + }, + onLikeTap: { dynamicId, uid, likedUid, worldId in + store.send(.likeDynamic(dynamicId, uid, likedUid, worldId)) + }, + onLoadMore: { + store.send(.loadMore) + }, + onRefresh: { + store.send(.reload) + }, + likeLoadingDynamicIds: store.likeLoadingDynamicIds + ) } } } @@ -214,7 +212,7 @@ struct FeedListView: View { @State private var previewCurrentIndex: Int = 0 var body: some View { - WithPerceptionTracking { + WithViewStore(store, observe: { $0 }) { viewStore in GeometryReader { geometry in ZStack { // 背景 @@ -252,41 +250,30 @@ struct FeedListView: View { .onAppear { store.send(.onAppear) } - .onRefresh { + .refreshable { store.send(.reload) } // 新增:编辑动态页面 - .sheet(isPresented: store.binding( - get: \.showEditFeed, - send: { _ in .editFeedDismissed } - )) { - WithPerceptionTracking { - EditFeedView( - onDismiss: { - store.send(.editFeedDismissed) - }, - store: Store( - initialState: EditFeedFeature.State() - ) { - EditFeedFeature() - } - ) - } + .sheet(isPresented: viewStore.binding(get: \.isEditFeedPresented, send: { _ in .editFeedDismissed })) { + EditFeedView( + onDismiss: { + store.send(.editFeedDismissed) + }, + store: Store( + initialState: EditFeedFeature.State() + ) { + EditFeedFeature() + } + ) } // 新增:详情页导航 - .navigationDestination(isPresented: store.binding( - get: \.showDetail, - send: { _ in .detailDismissed } - )) { - if let selectedMoment = store.selectedMoment { + .navigationDestination(isPresented: viewStore.binding(get: \.showDetail, send: { _ in .detailDismissed })) { + if let selectedMoment = viewStore.selectedMoment { DetailView( store: Store( initialState: DetailFeature.State(moment: selectedMoment) ) { DetailFeature() - }, - onDismiss: { - store.send(.detailDismissed) } ) } diff --git a/yana/Views/IDLoginView.swift b/yana/Views/IDLoginView.swift index c73d117..aaad9a8 100644 --- a/yana/Views/IDLoginView.swift +++ b/yana/Views/IDLoginView.swift @@ -2,17 +2,174 @@ import SwiftUI import ComposableArchitecture import Perception +// MARK: - 背景视图组件 +struct IDLoginBackgroundView: View { + var body: some View { + Image("bg") + .resizable() + .aspectRatio(contentMode: .fill) + .ignoresSafeArea(.all) + } +} + +// MARK: - 顶部导航栏组件 +struct IDLoginHeaderView: View { + let onBack: () -> Void + + var body: some View { + HStack { + Button(action: onBack) { + Image(systemName: "chevron.left") + .font(.system(size: 24, weight: .medium)) + .foregroundColor(.white) + .frame(width: 44, height: 44) + } + Spacer() + } + .padding(.horizontal, 16) + .padding(.top, 8) + } +} + +// MARK: - 输入框组件 +struct IDLoginInputFieldView: View { + let iconName: String + let title: String + let text: Binding + let onChange: (String) -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(iconName) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text(title) + .font(.system(size: 16)) + .foregroundColor(.white) + } + + TextField("", text: text) + .textFieldStyle(PlainTextFieldStyle()) + .font(.system(size: 16)) + .foregroundColor(.white) + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background(Color.white.opacity(0.1)) + .cornerRadius(8) + .onChange(of: text.wrappedValue) { newValue in + onChange(newValue) + } + } + } +} + +// MARK: - 密码输入框组件 +struct IDLoginPasswordFieldView: View { + let password: Binding + let isPasswordVisible: Binding + let onChange: (String) -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Image("email icon") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text(LocalizedString("id_login.password", comment: "")) + .font(.system(size: 16)) + .foregroundColor(.white) + } + + HStack { + Group { + if isPasswordVisible.wrappedValue { + TextField("", text: password) + .textFieldStyle(PlainTextFieldStyle()) + } else { + SecureField("", text: password) + .textFieldStyle(PlainTextFieldStyle()) + } + } + + Button(action: { + isPasswordVisible.wrappedValue.toggle() + }) { + Image(systemName: isPasswordVisible.wrappedValue ? "eye.slash" : "eye") + .foregroundColor(.white.opacity(0.7)) + } + } + .font(.system(size: 16)) + .foregroundColor(.white) + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background(Color.white.opacity(0.1)) + .cornerRadius(8) + .onChange(of: password.wrappedValue) { newValue in + onChange(newValue) + } + } + } +} + +// MARK: - 登录按钮组件 +struct IDLoginButtonView: View { + let isLoading: Bool + let isEnabled: Bool + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + Group { + if isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(1.2) + } else { + Text(LocalizedString("id_login.login", comment: "")) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white) + } + } + } + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .background(isEnabled ? Color.blue : Color.gray) + .cornerRadius(8) + .disabled(!isEnabled) + .padding(.top, 20) + } +} + +// MARK: - 错误信息组件 +struct IDLoginErrorView: View { + let errorMessage: String? + + var body: some View { + if let errorMessage = errorMessage { + Text(errorMessage) + .font(.system(size: 14)) + .foregroundColor(.red) + .padding(.top, 16) + .padding(.horizontal, 32) + } + } +} + +// MARK: - 主视图 struct IDLoginView: View { let store: StoreOf let onBack: () -> Void - @Binding var showIDLogin: Bool // 新增:绑定父视图的显示状态 + @Binding var showIDLogin: Bool // 使用本地@State管理UI状态 @State private var userID: String = "" @State private var password: String = "" @State private var isPasswordVisible: Bool = false - // 导航状态管理 - 与 LoginView 保持一致 + // 导航状态管理 @State private var showRecoverPassword: Bool = false // 计算登录按钮是否可用 @@ -24,28 +181,12 @@ struct IDLoginView: View { WithPerceptionTracking { GeometryReader { geometry in ZStack { - // 背景图片 - 使用与登录页面相同的"bg" - Image("bg") - .resizable() - .aspectRatio(contentMode: .fill) - .ignoresSafeArea(.all) + // 背景 + IDLoginBackgroundView() VStack(spacing: 0) { // 顶部导航栏 - HStack { - Button(action: { - onBack() - }) { - Image(systemName: "chevron.left") - .font(.system(size: 24, weight: .medium)) - .foregroundColor(.white) - .frame(width: 44, height: 44) - } - - Spacer() - } - .padding(.horizontal, 16) - .padding(.top, 8) + IDLoginHeaderView(onBack: onBack) Spacer() .frame(height: 60) @@ -59,68 +200,23 @@ struct IDLoginView: View { // 输入框区域 VStack(spacing: 20) { // 用户ID输入框 - VStack(alignment: .leading, spacing: 8) { - HStack { - Image("id icon") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 20, height: 20) - Text(LocalizedString("id_login.user_id", comment: "")) - .font(.system(size: 16)) - .foregroundColor(.white) + IDLoginInputFieldView( + iconName: "id icon", + title: LocalizedString("id_login.user_id", comment: ""), + text: $userID, + onChange: { newValue in + store.send(.userIDChanged(newValue)) } - - TextField("", text: $userID) - .textFieldStyle(PlainTextFieldStyle()) - .font(.system(size: 16)) - .foregroundColor(.white) - .padding(.horizontal, 16) - .padding(.vertical, 12) - .background(Color.white.opacity(0.1)) - .cornerRadius(8) - .onChange(of: userID) { newValue in - store.send(.userIDChanged(newValue)) - } - } + ) // 密码输入框 - VStack(alignment: .leading, spacing: 8) { - HStack { - Image("email icon") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 20, height: 20) - Text(LocalizedString("id_login.password", comment: "")) - .font(.system(size: 16)) - .foregroundColor(.white) - } - - HStack { - if isPasswordVisible { - TextField("", text: $password) - .textFieldStyle(PlainTextFieldStyle()) - } else { - SecureField("", text: $password) - .textFieldStyle(PlainTextFieldStyle()) - } - - Button(action: { - isPasswordVisible.toggle() - }) { - Image(systemName: isPasswordVisible ? "eye.slash" : "eye") - .foregroundColor(.white.opacity(0.7)) - } - } - .font(.system(size: 16)) - .foregroundColor(.white) - .padding(.horizontal, 16) - .padding(.vertical, 12) - .background(Color.white.opacity(0.1)) - .cornerRadius(8) - .onChange(of: password) { newValue in + IDLoginPasswordFieldView( + password: $password, + isPasswordVisible: $isPasswordVisible, + onChange: { newValue in store.send(.passwordChanged(newValue)) } - } + ) // 忘记密码按钮 HStack { @@ -135,45 +231,26 @@ struct IDLoginView: View { } // 登录按钮 - Button(action: { - store.send(.loginButtonTapped) - }) { - if store.isLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .scaleEffect(1.2) - } else { - Text(LocalizedString("id_login.login", comment: "")) - .font(.system(size: 16, weight: .medium)) - .foregroundColor(.white) + IDLoginButtonView( + isLoading: store.isLoading, + isEnabled: isLoginButtonEnabled, + onTap: { + store.send(.loginButtonTapped(userID: userID, password: password)) } - } - .frame(maxWidth: .infinity) - .padding(.vertical, 16) - .background(isLoginButtonEnabled ? Color.blue : Color.gray) - .cornerRadius(8) - .disabled(!isLoginButtonEnabled) - .padding(.top, 20) + ) // 错误信息显示 - if let errorMessage = store.errorMessage { - Text(errorMessage) - .font(.system(size: 14)) - .foregroundColor(.red) - .padding(.top, 16) - .padding(.horizontal, 32) - } + IDLoginErrorView(errorMessage: store.errorMessage) Spacer() } - // 添加API Loading和错误处理视图 + // API Loading视图 APILoadingEffectView() } } } .navigationBarHidden(true) - // 使用与 LoginView 一致的 navigationDestination 方式 .navigationDestination(isPresented: $showRecoverPassword) { WithPerceptionTracking { RecoverPasswordView( @@ -197,7 +274,6 @@ struct IDLoginView: View { isPasswordVisible = store.isPasswordVisible #if DEBUG - // 移除测试用的硬编码凭据 debugInfoSync("🐛 Debug模式: 已移除硬编码测试凭据") #endif } diff --git a/yana/Views/LoginView.swift b/yana/Views/LoginView.swift index 9f49622..c14bad0 100644 --- a/yana/Views/LoginView.swift +++ b/yana/Views/LoginView.swift @@ -12,7 +12,7 @@ struct ImageHeightPreferenceKey: PreferenceKey { struct LoginView: View { let store: StoreOf - let onLoginSuccess: () -> Void // 新增:登录成功回调 + let onLoginSuccess: () -> Void // 使用本地@State管理UI状态 @State private var showIDLogin: Bool = false @@ -21,136 +21,44 @@ struct LoginView: View { @State private var showUserAgreement: Bool = false @State private var showPrivacyPolicy: Bool = false - // 计算属性 - private var topImageHeight: CGFloat = 200 // 默认值 - var body: some View { - WithPerceptionTracking { - NavigationStack { - GeometryReader { geometry in - ZStack { - // 使用与 splash 相同的背景图片 - Image("bg") - .resizable() - .aspectRatio(contentMode: .fill) - .ignoresSafeArea(.all) - VStack(spacing: 0) { - // 上半部分的"top"图片 - ZStack { - Image("top") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(maxWidth: .infinity) - .padding(.top, -100) - .background( - GeometryReader { topImageGeometry in - Color.clear.preference(key: ImageHeightPreferenceKey.self, value: topImageGeometry.size.height) - } - ) - // E-PARTI 文本,底部对齐"top"图片底部,间距20 - HStack { - Text(LocalizedString("login.app_title", comment: "")) - .font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width)) - .foregroundColor(.white) - .padding(.leading, 20) - Spacer() - } - .padding(.top, max(0, topImageHeight - 100)) // top图片高度 - 140 - } - - // 下半部分的登录按钮区域 - VStack(spacing: 20) { - // 登录按钮 - LoginButton( - title: LocalizedString("login.id_login", comment: ""), - icon: "person.circle", - action: { - showIDLogin = true - } - ) - - LoginButton( - title: LocalizedString("login.email_login", comment: ""), - icon: "envelope", - action: { - showEmailLogin = true - } - ) - - // 底部设置按钮 - HStack(spacing: 20) { - Button(action: { - showLanguageSettings = true - }) { - Text(LocalizedString("login.language", comment: "")) - .font(.system(size: 14)) - .foregroundColor(.white.opacity(0.8)) - } - - Button(action: { - showUserAgreement = true - }) { - Text(LocalizedString("login.user_agreement", comment: "")) - .font(.system(size: 14)) - .foregroundColor(.white.opacity(0.8)) - } - - Button(action: { - showPrivacyPolicy = true - }) { - Text(LocalizedString("login.privacy_policy", comment: "")) - .font(.system(size: 14)) - .foregroundColor(.white.opacity(0.8)) - } - } - } - .padding(.horizontal, 28) - .padding(.bottom, 140) - } - - // 添加API Loading和错误处理视图 - APILoadingEffectView() - - // 移除旧的 NavigationLink,改用 navigationDestination - } - } - .navigationBarHidden(true) - // 新增:适配 iOS 16 的 navigationDestination - .navigationDestination(isPresented: $showIDLogin) { - WithPerceptionTracking { - IDLoginView( - store: store.scope( - state: \.idLoginState, - action: \.idLogin - ), - onBack: { - showIDLogin = false - }, - showIDLogin: $showIDLogin // 新增:传递Binding - ) - .navigationBarHidden(true) - } - } - .navigationDestination(isPresented: $showEmailLogin) { - WithPerceptionTracking { - EMailLoginView( - store: store.scope( - state: \.emailLoginState, - action: \.emailLogin - ), - onBack: { - showEmailLogin = false - }, - showEmailLogin: $showEmailLogin // 新增:传递Binding - ) - .navigationBarHidden(true) - } + NavigationStack { + GeometryReader { geometry in + ZStack { + backgroundView + mainContentView(geometry: geometry) + APILoadingEffectView() } } + .navigationBarHidden(true) + .navigationDestination(isPresented: $showIDLogin) { + IDLoginView( + store: store.scope( + state: \.idLoginState, + action: \.idLogin + ), + onBack: { + showIDLogin = false + }, + showIDLogin: $showIDLogin + ) + .navigationBarHidden(true) + } + .navigationDestination(isPresented: $showEmailLogin) { + EMailLoginView( + store: store.scope( + state: \.emailLoginState, + action: \.emailLogin + ), + onBack: { + showEmailLogin = false + }, + showEmailLogin: $showEmailLogin + ) + .navigationBarHidden(true) + } .sheet(isPresented: $showLanguageSettings) { - WithPerceptionTracking { - LanguageSettingsView(isPresented: $showLanguageSettings) - } + LanguageSettingsView(isPresented: $showLanguageSettings) } .webView( isPresented: $showUserAgreement, @@ -160,26 +68,122 @@ struct LoginView: View { isPresented: $showPrivacyPolicy, url: APIConfiguration.webURL(for: .privacyPolicy) ) - // 新增:监听登录成功,调用回调 - .onChange(of: store.isAnyLoginCompleted) { completed in - if completed { + .onChange(of: store.isAnyLoginCompleted) { + if store.isAnyLoginCompleted { onLoginSuccess() } } - // 新增:监听showIDLogin关闭时,若已登录则跳转首页 - .onChange(of: showIDLogin) { newValue in - if newValue == false && store.isAnyLoginCompleted { + .onChange(of: showIDLogin) { + if showIDLogin == false && store.isAnyLoginCompleted { onLoginSuccess() } } - // 新增:监听showEmailLogin关闭时,若已登录则跳转首页 - .onChange(of: showEmailLogin) { newValue in - if newValue == false && store.isAnyLoginCompleted { + .onChange(of: showEmailLogin) { + if showEmailLogin == false && store.isAnyLoginCompleted { onLoginSuccess() } } } } + + // MARK: - 子视图 + + private var backgroundView: some View { + Image("bg") + .resizable() + .aspectRatio(contentMode: .fill) + .ignoresSafeArea(.all) + } + + private func mainContentView(geometry: GeometryProxy) -> some View { + VStack(spacing: 0) { + topSection(geometry: geometry) + bottomSection + } + } + + private func topSection(geometry: GeometryProxy) -> some View { + ZStack { + Image("top") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: .infinity) + .padding(.top, -100) + .background( + GeometryReader { topImageGeometry in + Color.clear.preference(key: ImageHeightPreferenceKey.self, value: topImageGeometry.size.height) + } + ) + + HStack { + Text(LocalizedString("login.app_title", comment: "")) + .font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width)) + .foregroundColor(.white) + .padding(.leading, 20) + Spacer() + } + .padding(.top, 100) // 简化计算逻辑 + } + } + + private var bottomSection: some View { + VStack(spacing: 20) { + loginButtons + bottomButtons + } + .padding(.horizontal, 28) + .padding(.bottom, 140) + } + + private var loginButtons: some View { + VStack(spacing: 20) { + LoginButton( + iconName: "person.circle", + iconColor: .blue, + title: LocalizedString("login.id_login", comment: ""), + action: { + showIDLogin = true + } + ) + + LoginButton( + iconName: "envelope", + iconColor: .green, + title: LocalizedString("login.email_login", comment: ""), + action: { + showEmailLogin = true + } + ) + } + } + + private var bottomButtons: some View { + HStack(spacing: 20) { + Button(action: { + showLanguageSettings = true + }) { + Text(LocalizedString("login.language", comment: "")) + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.8)) + } + + Button(action: { + showUserAgreement = true + }) { + Text(LocalizedString("login.user_agreement", comment: "")) + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.8)) + } + + Button(action: { + showPrivacyPolicy = true + }) { + Text(LocalizedString("login.privacy_policy", comment: "")) + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.8)) + } + } + } } //#Preview { diff --git a/yana/Views/MainView.swift b/yana/Views/MainView.swift index 63b4ff3..2e475e7 100644 --- a/yana/Views/MainView.swift +++ b/yana/Views/MainView.swift @@ -8,8 +8,8 @@ struct MainView: View { var body: some View { WithPerceptionTracking { InternalMainView(store: store) - .onChange(of: store.isLoggedOut) { isLoggedOut in - if isLoggedOut { + .onChange(of: store.isLoggedOut) { + if store.isLoggedOut { onLogout?() } } @@ -32,12 +32,12 @@ struct InternalMainView: View { .navigationDestination(for: MainFeature.Destination.self) { destination in DestinationView(destination: destination, store: self.store) } - .onChange(of: path) { newPath in - store.send(.navigationPathChanged(newPath)) + .onChange(of: path) { + store.send(.navigationPathChanged(path)) } - .onChange(of: store.navigationPath) { newPath in - if path != newPath { - path = newPath + .onChange(of: store.navigationPath) { + if path != store.navigationPath { + path = store.navigationPath } } .onAppear { @@ -91,9 +91,11 @@ struct InternalMainView: View { // 底部导航栏 - 固定在底部 VStack { Spacer() - BottomTabView(selectedTab: store.binding( - get: { Tab(rawValue: $0.selectedTab.rawValue) ?? .feed }, - send: { MainFeature.Action.selectTab(MainFeature.Tab(rawValue: $0.rawValue) ?? .feed) } + BottomTabView(selectedTab: Binding( + get: { Tab(rawValue: store.selectedTab.rawValue) ?? .feed }, + set: { newTab in + store.send(.selectTab(MainFeature.Tab(rawValue: newTab.rawValue) ?? .feed)) + } )) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) diff --git a/yana/Views/MeView.swift b/yana/Views/MeView.swift index 307eff9..7b8bc5f 100644 --- a/yana/Views/MeView.swift +++ b/yana/Views/MeView.swift @@ -60,21 +60,23 @@ struct MeView: View { } } // 新增:详情页导航 - .navigationDestination(isPresented: store.binding( - get: \.showDetail, - send: { _ in .detailDismissed } + .navigationDestination(isPresented: Binding( + get: { store.showDetail }, + set: { _ in store.send(.detailDismissed) } )) { if let selectedMoment = store.selectedMoment { - DetailView( - store: Store( - initialState: DetailFeature.State(moment: selectedMoment) - ) { - DetailFeature() - }, - onDismiss: { - store.send(.detailDismissed) + let detailStore = Store( + initialState: DetailFeature.State(moment: selectedMoment) + ) { + DetailFeature() + } + + DetailView(store: detailStore) + .onChange(of: detailStore.shouldDismiss) { shouldDismiss in + if shouldDismiss { + store.send(.detailDismissed) + } } - ) } } } @@ -121,7 +123,7 @@ struct MeView: View { @ViewBuilder private func momentsSection() -> some View { WithPerceptionTracking { - if store.isLoading { + if store.isLoadingUserInfo || store.isLoadingMoments { VStack { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .white)) @@ -132,7 +134,7 @@ struct MeView: View { .padding(.top, 8) } .frame(maxWidth: .infinity, maxHeight: .infinity) - } else if let error = store.error { + } else if let error = store.userInfoError ?? store.momentsError { VStack(spacing: 12) { Image(systemName: "exclamationmark.triangle") .font(.system(size: 32)) diff --git a/yana/Views/SplashView.swift b/yana/Views/SplashView.swift index 6f0e7ec..eea7bc9 100644 --- a/yana/Views/SplashView.swift +++ b/yana/Views/SplashView.swift @@ -5,50 +5,48 @@ struct SplashView: View { let store: StoreOf var body: some View { - WithPerceptionTracking { - ZStack { - Group { - // 根据导航目标显示不同页面 - if let navigationDestination = store.navigationDestination { - switch navigationDestination { - case .login: - // 显示登录页面 - LoginView( - store: Store( - initialState: LoginFeature.State() - ) { - LoginFeature() - }, - onLoginSuccess: { - // 登录成功后导航到主页面 - store.send(.navigateToMain) - } - ) - case .main: - // 显示主应用页面 - MainView( - store: Store( - initialState: MainFeature.State() - ) { - MainFeature() - }, - onLogout: { - store.send(.navigateToLogin) - } - ) - } - } else { - // 显示启动画面 - splashContent + ZStack { + Group { + // 根据导航目标显示不同页面 + if let navigationDestination = store.navigationDestination { + switch navigationDestination { + case .login: + // 显示登录页面 + LoginView( + store: Store( + initialState: LoginFeature.State() + ) { + LoginFeature() + }, + onLoginSuccess: { + // 登录成功后导航到主页面 + store.send(.navigateToMain) + } + ) + case .main: + // 显示主应用页面 + MainView( + store: Store( + initialState: MainFeature.State() + ) { + MainFeature() + }, + onLogout: { + store.send(.navigateToLogin) + } + ) } + } else { + // 显示启动画面 + splashContent } - .onAppear { - store.send(.onAppear) - } - - // API Loading 效果视图 - 显示在所有内容之上 - APILoadingEffectView() } + .onAppear { + store.send(.onAppear) + } + + // API Loading 效果视图 - 显示在所有内容之上 + APILoadingEffectView() } } diff --git a/项目问题排查与解决流程.md b/项目问题排查与解决流程.md index 61efe70..dda9cb8 100644 --- a/项目问题排查与解决流程.md +++ b/项目问题排查与解决流程.md @@ -1,6 +1,7 @@ # Yana 项目问题排查与解决流程文档 ## 目录 + 1. [问题概述](#问题概述) 2. [解决流程](#解决流程) 3. [技术细节](#技术细节) @@ -13,14 +14,17 @@ ## 问题概述 ### 初始错误 + **错误信息**: `"Could not compute dependency graph: unable to load transferred PIF: The workspace contains multiple references with the same GUID"` **问题表现**: + - 项目无法启动 - Xcode 无法计算依赖图 - 出现 GUID 冲突错误 ### 根本原因分析 + 1. **混合包管理系统**: 项目同时使用了 Swift Package Manager (SPM) 和 CocoaPods 2. **缓存冲突**: Xcode DerivedData 与 SPM 状态不同步 3. **TCA 结构问题**: 代码中 HomeFeature 缺少必要的状态和 Action 定义 @@ -32,6 +36,7 @@ ### 第一阶段:GUID 冲突解决 #### 步骤 1: 清理缓存 + ```bash # 清理 Xcode DerivedData rm -rf ~/Library/Developer/Xcode/DerivedData/* @@ -42,11 +47,13 @@ swift package resolve ``` #### 步骤 2: 重新安装 CocoaPods + ```bash pod install --clean-install ``` #### 步骤 3: 验证项目解析 + ```bash xcodebuild -workspace yana.xcworkspace -list ``` @@ -54,13 +61,15 @@ xcodebuild -workspace yana.xcworkspace -list ### 第二阶段:TCA 结构修复 #### 问题识别 + - `HomeFeature.State` 缺少 `isSettingPresented` 和 `settingState` 属性 - `HomeFeature.Action` 缺少 `settingDismissed` 和 `setting` actions - `HomeView.swift` 中的 `store.scope()` 调用语法错误 #### 修复步骤 -**1. 修复 HomeFeature.swift** +1. 修复 HomeFeature.swift + ```swift @ObservableState struct State: Equatable { @@ -89,7 +98,8 @@ enum Action: Equatable { } ``` -**2. 添加子 Reducer** +2.添加子 Reducer + ```swift var body: some ReducerOf { Scope(state: \.settingState, action: \.setting) { @@ -110,7 +120,8 @@ var body: some ReducerOf { } ``` -**3. 修复 HomeView.swift** +3.修复 HomeView.swift + ```swift .sheet(isPresented: Binding( get: { store.isSettingPresented }, @@ -127,10 +138,12 @@ var body: some ReducerOf { ### 依赖管理配置 **Swift Package Manager (Package.swift)**: + - ComposableArchitecture: 1.20.2+ - 其他依赖根据需要添加 **CocoaPods (Podfile)**: + - Alamofire (网络请求) - SDWebImage (图像加载) - CocoaLumberjack (日志) @@ -147,6 +160,7 @@ Feature ``` ### 文件结构 + ``` yana/ ├── Features/ # TCA Feature 定义 @@ -161,6 +175,7 @@ yana/ ## 最终解决方案 ### 命令执行顺序 + ```bash # 1. 清理环境 rm -rf ~/Library/Developer/Xcode/DerivedData/* @@ -224,33 +239,42 @@ check_project() { ## 常见问题FAQ ### Q1: 再次出现 GUID 冲突怎么办? + **A**: 执行完整清理流程: -```bash -rm -rf ~/Library/Developer/Xcode/DerivedData/* -swift package reset && swift package resolve -pod install --clean-install -``` + ```bash + rm -rf ~/Library/Developer/Xcode/DerivedData/* + swift package reset && swift package resolve + pod install --clean-install + ``` ### Q2: TCA Reducer 编译错误如何处理? + **A**: 检查以下项目: + - State 属性完整性 - Action 枚举完整性 - Reducer body 中的 case 处理 - 子 Reducer 的 Scope 配置 ### Q3: 如何避免混合包管理器问题? -**A**: + +**A**: + - 尽量使用单一包管理工具 - 如需混合使用,确保依赖版本兼容 - 定期更新依赖并测试 ### Q4: Swift 6 兼容性警告如何处理? -**A**: + +**A**: + - 短期:可以忽略,不影响功能 - 长期:逐步迁移到 Swift 6 Sendable 模式 ### Q5: 项目构建缓慢怎么办? + **A**: + - 使用 `xcodebuild -quiet` 减少输出 - 开启 Xcode Build System 并行构建 - 定期清理 DerivedData @@ -271,5 +295,5 @@ pod install --clean-install --- **文档更新时间**: 2025-07-10 -**适用版本**: iOS 16+, Swift 6, TCA 1.20.2+ -**维护者**: AI Assistant & 开发团队 \ No newline at end of file +**适用版本**: iOS 17+, Swift 6, TCA 1.20.2+ +**维护者**: AI Assistant & 开发团队