feat: 更新文档和视图以支持iOS 17及优化用户体验
- 更新Yana项目文档,调整适用版本至iOS 17,确保与最新开发环境兼容。 - 在多个视图中重构代码,优化状态管理和视图逻辑,提升用户体验。 - 添加默认初始化器以简化状态管理,确保各个Feature的状态一致性。 - 更新视图组件,移除不必要的硬编码,增强代码可读性和维护性。 - 修复多个视图中的逻辑错误,确保功能正常运行。
This commit is contained in:
@@ -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.
|
||||
|
||||
|
11
README.md
11
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
|
||||
- 已配置框架冲突处理脚本
|
||||
- 已配置框架冲突处理脚本
|
||||
-
|
@@ -49,8 +49,6 @@
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
4C4C8FBE2DE5AF9200384527 /* yanaAPITests */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = yanaAPITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -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";
|
||||
|
@@ -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
|
||||
|
@@ -187,8 +187,8 @@ struct ContentView: View {
|
||||
}
|
||||
.tag(1)
|
||||
}
|
||||
.onChange(of: selectedLogLevel) { newValue in
|
||||
APILogger.logLevel = newValue
|
||||
.onChange(of: selectedLogLevel) {
|
||||
APILogger.logLevel = selectedLogLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -40,6 +40,10 @@ struct ConfigFeature {
|
||||
var configData: ConfigData?
|
||||
var errorMessage: String?
|
||||
var lastUpdated: Date?
|
||||
|
||||
init() {
|
||||
// 默认初始化
|
||||
}
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
|
@@ -19,6 +19,10 @@ struct CreateFeedFeature {
|
||||
!content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isLoading
|
||||
}
|
||||
var isLoading: Bool = false
|
||||
|
||||
init() {
|
||||
// 默认初始化
|
||||
}
|
||||
}
|
||||
|
||||
enum Action {
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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<Int> = []
|
||||
|
||||
init() {
|
||||
// 默认初始化
|
||||
}
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
|
@@ -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<Self> {
|
||||
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
|
||||
|
@@ -8,6 +8,10 @@ struct InitFeature {
|
||||
var isLoading = false
|
||||
var response: InitResponse?
|
||||
var error: String?
|
||||
|
||||
init() {
|
||||
// 默认初始化
|
||||
}
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
|
@@ -34,13 +34,11 @@ struct LoginFeature {
|
||||
case failed // 认证失败
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
init() {
|
||||
// 移除测试用的硬编码凭据
|
||||
// 默认初始化
|
||||
self.account = ""
|
||||
self.password = ""
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
enum Action {
|
||||
|
@@ -19,6 +19,10 @@ struct MainFeature {
|
||||
var appSettingState: AppSettingFeature.State? = nil
|
||||
// 新增:登出标志
|
||||
var isLoggedOut: Bool = false
|
||||
|
||||
init() {
|
||||
// 默认初始化
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:导航目标
|
||||
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -12,6 +12,10 @@ struct SplashFeature {
|
||||
|
||||
// 新增:导航目标
|
||||
var navigationDestination: NavigationDestination?
|
||||
|
||||
init() {
|
||||
// 默认初始化
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:导航目标枚举
|
||||
|
@@ -4,12 +4,10 @@ import ComposableArchitecture
|
||||
struct DetailView: View {
|
||||
@State var store: StoreOf<DetailFeature>
|
||||
let onLikeSuccess: ((Int, Bool) -> Void)?
|
||||
let onDismiss: (() -> Void)? // 新增:关闭回调
|
||||
|
||||
init(store: StoreOf<DetailFeature>, onLikeSuccess: ((Int, Bool) -> Void)? = nil, onDismiss: (() -> Void)? = nil) {
|
||||
init(store: StoreOf<DetailFeature>, 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()
|
||||
// }
|
||||
// )
|
||||
//}
|
||||
//}
|
||||
|
@@ -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()
|
||||
|
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@@ -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<String>
|
||||
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<String>
|
||||
let isPasswordVisible: Binding<Bool>
|
||||
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<IDLoginFeature>
|
||||
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
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ struct ImageHeightPreferenceKey: PreferenceKey {
|
||||
|
||||
struct LoginView: View {
|
||||
let store: StoreOf<LoginFeature>
|
||||
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 {
|
||||
|
@@ -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)
|
||||
|
@@ -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))
|
||||
|
@@ -5,50 +5,48 @@ struct SplashView: View {
|
||||
let store: StoreOf<SplashFeature>
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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<Self> {
|
||||
Scope(state: \.settingState, action: \.setting) {
|
||||
@@ -110,7 +120,8 @@ var body: some ReducerOf<Self> {
|
||||
}
|
||||
```
|
||||
|
||||
**3. 修复 HomeView.swift**
|
||||
3.修复 HomeView.swift
|
||||
|
||||
```swift
|
||||
.sheet(isPresented: Binding(
|
||||
get: { store.isSettingPresented },
|
||||
@@ -127,10 +138,12 @@ var body: some ReducerOf<Self> {
|
||||
### 依赖管理配置
|
||||
|
||||
**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 & 开发团队
|
||||
**适用版本**: iOS 17+, Swift 6, TCA 1.20.2+
|
||||
**维护者**: AI Assistant & 开发团队
|
||||
|
Reference in New Issue
Block a user