feat: 更新文档和视图以支持iOS 17及优化用户体验
- 更新Yana项目文档,调整适用版本至iOS 17,确保与最新开发环境兼容。 - 在多个视图中重构代码,优化状态管理和视图逻辑,提升用户体验。 - 添加默认初始化器以简化状态管理,确保各个Feature的状态一致性。 - 更新视图组件,移除不必要的硬编码,增强代码可读性和维护性。 - 修复多个视图中的逻辑错误,确保功能正常运行。
This commit is contained in:
@@ -5,7 +5,7 @@ alwaysApply: true
|
|||||||
---
|
---
|
||||||
# Background
|
# 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.
|
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 (部分组件)
|
- **开发语言**:Swift (主要),Objective-C (部分组件)
|
||||||
- **最低支持版本**:iOS 16
|
- **最低支持版本**:iOS 17
|
||||||
- **架构模式**:The Composable Architecture (TCA) - 1.20.2
|
- **架构模式**:The Composable Architecture (TCA) - 1.20.2
|
||||||
- **UI 框架**:SwiftUI
|
- **UI 框架**:SwiftUI
|
||||||
- **依赖管理**:
|
- **依赖管理**:
|
||||||
@@ -45,7 +45,7 @@ yana/
|
|||||||
## 环境要求
|
## 环境要求
|
||||||
|
|
||||||
- Xcode 13.0 或更高版本
|
- Xcode 13.0 或更高版本
|
||||||
- iOS 16 或更高版本
|
- iOS 17 或更高版本
|
||||||
- CocoaPods 包管理器
|
- CocoaPods 包管理器
|
||||||
|
|
||||||
## 安装步骤
|
## 安装步骤
|
||||||
@@ -102,7 +102,7 @@ let response = try await apiService.request(request)
|
|||||||
|
|
||||||
- 项目使用 CocoaPods 管理依赖
|
- 项目使用 CocoaPods 管理依赖
|
||||||
- 需要配置网易云信相关密钥
|
- 需要配置网易云信相关密钥
|
||||||
- 最低支持 iOS 16 版本
|
- 最低支持 iOS 17 版本
|
||||||
- 仅支持 iPhone 设备(不支持 iPad、Mac Catalyst 或 Vision Pro)
|
- 仅支持 iPhone 设备(不支持 iPad、Mac Catalyst 或 Vision Pro)
|
||||||
|
|
||||||
## 开发规范
|
## 开发规范
|
||||||
@@ -123,6 +123,7 @@ let response = try await apiService.request(request)
|
|||||||
## 构建配置
|
## 构建配置
|
||||||
|
|
||||||
- 项目使用动态框架
|
- 项目使用动态框架
|
||||||
- 支持 iOS 16 及以上版本
|
- 支持 iOS 17 及以上版本
|
||||||
- Swift 版本:6.0
|
- Swift 版本:6.0
|
||||||
- 已配置框架冲突处理脚本
|
- 已配置框架冲突处理脚本
|
||||||
|
-
|
||||||
@@ -49,8 +49,6 @@
|
|||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
4C4C8FBE2DE5AF9200384527 /* yanaAPITests */ = {
|
4C4C8FBE2DE5AF9200384527 /* yanaAPITests */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
|
||||||
);
|
|
||||||
path = yanaAPITests;
|
path = yanaAPITests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -255,10 +253,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources.sh\"\n";
|
||||||
@@ -272,10 +274,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks.sh\"\n";
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
enum Environment {
|
enum AppEnvironment {
|
||||||
case development
|
case development
|
||||||
case production
|
case production
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AppConfig {
|
struct AppConfig {
|
||||||
static let current: Environment = {
|
static let current: AppEnvironment = {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
return .development
|
return .development
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -187,8 +187,8 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.tag(1)
|
.tag(1)
|
||||||
}
|
}
|
||||||
.onChange(of: selectedLogLevel) { newValue in
|
.onChange(of: selectedLogLevel) {
|
||||||
APILogger.logLevel = newValue
|
APILogger.logLevel = selectedLogLevel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,12 @@ struct AppSettingFeature {
|
|||||||
var isUpdatingUser: Bool = false
|
var isUpdatingUser: Bool = false
|
||||||
var updateUserError: String? = nil
|
var updateUserError: String? = nil
|
||||||
|
|
||||||
// 新增:带userInfo、avatarURL、nickname的init
|
// 默认初始化器
|
||||||
|
init() {
|
||||||
|
// 默认初始化
|
||||||
|
}
|
||||||
|
|
||||||
|
// 带userInfo、avatarURL、nickname的init
|
||||||
init(nickname: String = "", avatarURL: String? = nil, userInfo: UserInfo? = nil) {
|
init(nickname: String = "", avatarURL: String? = nil, userInfo: UserInfo? = nil) {
|
||||||
self.nickname = nickname
|
self.nickname = nickname
|
||||||
self.avatarURL = avatarURL
|
self.avatarURL = avatarURL
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ struct ConfigFeature {
|
|||||||
var configData: ConfigData?
|
var configData: ConfigData?
|
||||||
var errorMessage: String?
|
var errorMessage: String?
|
||||||
var lastUpdated: Date?
|
var lastUpdated: Date?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// 默认初始化
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Action: Equatable {
|
enum Action: Equatable {
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ struct CreateFeedFeature {
|
|||||||
!content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isLoading
|
!content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isLoading
|
||||||
}
|
}
|
||||||
var isLoading: Bool = false
|
var isLoading: Bool = false
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// 默认初始化
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Action {
|
enum Action {
|
||||||
|
|||||||
@@ -20,13 +20,11 @@ struct EMailLoginFeature {
|
|||||||
case failed
|
case failed
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
init() {
|
init() {
|
||||||
self.email = "exzero@126.com"
|
self.email = ""
|
||||||
self.verificationCode = ""
|
self.verificationCode = ""
|
||||||
self.loginStep = .initial
|
self.loginStep = .initial
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Action {
|
enum Action {
|
||||||
|
|||||||
@@ -25,6 +25,20 @@ struct EditFeedFeature {
|
|||||||
var isUploadingImages: Bool = false
|
var isUploadingImages: Bool = false
|
||||||
var imageUploadProgress: Double = 0.0 // 0.0~1.0
|
var imageUploadProgress: Double = 0.0 // 0.0~1.0
|
||||||
var uploadedResList: [ResListItem] = []
|
var uploadedResList: [ResListItem] = []
|
||||||
|
|
||||||
|
// 新增:PhotosPicker相关状态
|
||||||
|
var showPhotosPicker: Bool = false
|
||||||
|
var selectedPhotoItems: [PhotosPickerItem] = []
|
||||||
|
|
||||||
|
// 新增:删除图片确认相关状态
|
||||||
|
var showDeleteImageAlert: Bool = false
|
||||||
|
var imageToDeleteIndex: Int? = nil
|
||||||
|
|
||||||
|
// 默认初始化器
|
||||||
|
init() {
|
||||||
|
// 默认初始化
|
||||||
|
}
|
||||||
|
|
||||||
// 手动实现Equatable,selectedImages只比较数量(PhotosPickerItem不支持Equatable)
|
// 手动实现Equatable,selectedImages只比较数量(PhotosPickerItem不支持Equatable)
|
||||||
static func == (lhs: State, rhs: State) -> Bool {
|
static func == (lhs: State, rhs: State) -> Bool {
|
||||||
lhs.content == rhs.content &&
|
lhs.content == rhs.content &&
|
||||||
@@ -35,7 +49,11 @@ struct EditFeedFeature {
|
|||||||
lhs.selectedImages.count == rhs.selectedImages.count &&
|
lhs.selectedImages.count == rhs.selectedImages.count &&
|
||||||
lhs.isUploadingImages == rhs.isUploadingImages &&
|
lhs.isUploadingImages == rhs.isUploadingImages &&
|
||||||
lhs.imageUploadProgress == rhs.imageUploadProgress &&
|
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 uploadImagesResponse(Result<[ResListItem], Error>)
|
||||||
// 新增:图片上传进度
|
// 新增:图片上传进度
|
||||||
case updateImageUploadProgress(Double)
|
case updateImageUploadProgress(Double)
|
||||||
|
// 新增:PhotosPicker相关Action
|
||||||
|
case photosPickerDismissed
|
||||||
|
case addImageButtonTapped
|
||||||
|
// 新增:删除图片确认相关Action
|
||||||
|
case showDeleteImageAlert(Int)
|
||||||
|
case deleteImageAlertDismissed
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dependency(\.apiService) var apiService
|
@Dependency(\.apiService) var apiService
|
||||||
@@ -176,6 +200,7 @@ struct EditFeedFeature {
|
|||||||
return .none
|
return .none
|
||||||
case .photosPickerItemsChanged(let items):
|
case .photosPickerItemsChanged(let items):
|
||||||
state.selectedImages = items
|
state.selectedImages = items
|
||||||
|
state.selectedPhotoItems = items
|
||||||
return .run { send in
|
return .run { send in
|
||||||
await send(.processPhotosPickerItems(items))
|
await send(.processPhotosPickerItems(items))
|
||||||
}
|
}
|
||||||
@@ -203,11 +228,30 @@ struct EditFeedFeature {
|
|||||||
if index < state.selectedImages.count {
|
if index < state.selectedImages.count {
|
||||||
state.selectedImages.remove(at: index)
|
state.selectedImages.remove(at: index)
|
||||||
}
|
}
|
||||||
|
if index < state.selectedPhotoItems.count {
|
||||||
|
state.selectedPhotoItems.remove(at: index)
|
||||||
|
}
|
||||||
return .none
|
return .none
|
||||||
// 新增:图片上传进度
|
// 新增:图片上传进度
|
||||||
case .updateImageUploadProgress(let progress):
|
case .updateImageUploadProgress(let progress):
|
||||||
state.imageUploadProgress = progress
|
state.imageUploadProgress = progress
|
||||||
return .none
|
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
|
@Reducer
|
||||||
struct FeedListFeature {
|
struct FeedListFeature {
|
||||||
@Dependency(\.apiService) var apiService
|
@Dependency(\.apiService) var apiService
|
||||||
|
@ObservableState
|
||||||
struct State: Equatable {
|
struct State: Equatable {
|
||||||
var isFirstLoad: Bool = true
|
var isFirstLoad: Bool = true
|
||||||
var feeds: [Feed] = [] // 预留 feed 内容
|
var feeds: [Feed] = [] // 预留 feed 内容
|
||||||
@@ -23,6 +24,10 @@ struct FeedListFeature {
|
|||||||
var selectedMoment: MomentsInfo?
|
var selectedMoment: MomentsInfo?
|
||||||
// 新增:点赞相关状态
|
// 新增:点赞相关状态
|
||||||
var likeLoadingDynamicIds: Set<Int> = []
|
var likeLoadingDynamicIds: Set<Int> = []
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// 默认初始化
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Action: Equatable {
|
enum Action: Equatable {
|
||||||
|
|||||||
@@ -25,15 +25,15 @@ struct IDLoginFeature {
|
|||||||
case failed // 认证失败
|
case failed // 认证失败
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
init() {
|
init() {
|
||||||
self.userID = "2356814"
|
self.userID = ""
|
||||||
self.password = "a123456"
|
self.password = ""
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Action: Equatable {
|
enum Action: Equatable {
|
||||||
|
case userIDChanged(String)
|
||||||
|
case passwordChanged(String)
|
||||||
case togglePasswordVisibility
|
case togglePasswordVisibility
|
||||||
case loginButtonTapped(userID: String, password: String)
|
case loginButtonTapped(userID: String, password: String)
|
||||||
case forgotPasswordTapped
|
case forgotPasswordTapped
|
||||||
@@ -52,6 +52,12 @@ struct IDLoginFeature {
|
|||||||
var body: some ReducerOf<Self> {
|
var body: some ReducerOf<Self> {
|
||||||
Reduce { state, action in
|
Reduce { state, action in
|
||||||
switch action {
|
switch action {
|
||||||
|
case let .userIDChanged(userID):
|
||||||
|
state.userID = userID
|
||||||
|
return .none
|
||||||
|
case let .passwordChanged(password):
|
||||||
|
state.password = password
|
||||||
|
return .none
|
||||||
case .togglePasswordVisibility:
|
case .togglePasswordVisibility:
|
||||||
state.isPasswordVisible.toggle()
|
state.isPasswordVisible.toggle()
|
||||||
return .none
|
return .none
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ struct InitFeature {
|
|||||||
var isLoading = false
|
var isLoading = false
|
||||||
var response: InitResponse?
|
var response: InitResponse?
|
||||||
var error: String?
|
var error: String?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// 默认初始化
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Action: Equatable {
|
enum Action: Equatable {
|
||||||
|
|||||||
@@ -34,13 +34,11 @@ struct LoginFeature {
|
|||||||
case failed // 认证失败
|
case failed // 认证失败
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
init() {
|
init() {
|
||||||
// 移除测试用的硬编码凭据
|
// 默认初始化
|
||||||
self.account = ""
|
self.account = ""
|
||||||
self.password = ""
|
self.password = ""
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Action {
|
enum Action {
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ struct MainFeature {
|
|||||||
var appSettingState: AppSettingFeature.State? = nil
|
var appSettingState: AppSettingFeature.State? = nil
|
||||||
// 新增:登出标志
|
// 新增:登出标志
|
||||||
var isLoggedOut: Bool = false
|
var isLoggedOut: Bool = false
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// 默认初始化
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增:导航目标
|
// 新增:导航目标
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ struct MeDynamicFeature: Reducer {
|
|||||||
var hasMore: Bool = true
|
var hasMore: Bool = true
|
||||||
var error: String?
|
var error: String?
|
||||||
var isInitialized: Bool = false // 首次加载标记
|
var isInitialized: Bool = false // 首次加载标记
|
||||||
|
|
||||||
|
init(uid: Int = 0) {
|
||||||
|
self.uid = uid
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Action: Equatable {
|
enum Action: Equatable {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import ComposableArchitecture
|
|||||||
@Reducer
|
@Reducer
|
||||||
struct MeFeature {
|
struct MeFeature {
|
||||||
@Dependency(\.apiService) var apiService
|
@Dependency(\.apiService) var apiService
|
||||||
|
@ObservableState
|
||||||
struct State: Equatable {
|
struct State: Equatable {
|
||||||
var isFirstLoad: Bool = true
|
var isFirstLoad: Bool = true
|
||||||
var userInfo: UserInfo?
|
var userInfo: UserInfo?
|
||||||
@@ -21,6 +22,10 @@ struct MeFeature {
|
|||||||
// 新增:DetailView相关状态
|
// 新增:DetailView相关状态
|
||||||
var showDetail: Bool = false
|
var showDetail: Bool = false
|
||||||
var selectedMoment: MomentsInfo?
|
var selectedMoment: MomentsInfo?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// 默认初始化
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Action: Equatable {
|
enum Action: Equatable {
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ struct SplashFeature {
|
|||||||
|
|
||||||
// 新增:导航目标
|
// 新增:导航目标
|
||||||
var navigationDestination: NavigationDestination?
|
var navigationDestination: NavigationDestination?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// 默认初始化
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增:导航目标枚举
|
// 新增:导航目标枚举
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ import ComposableArchitecture
|
|||||||
struct DetailView: View {
|
struct DetailView: View {
|
||||||
@State var store: StoreOf<DetailFeature>
|
@State var store: StoreOf<DetailFeature>
|
||||||
let onLikeSuccess: ((Int, Bool) -> Void)?
|
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.store = store
|
||||||
self.onLikeSuccess = onLikeSuccess
|
self.onLikeSuccess = onLikeSuccess
|
||||||
self.onDismiss = onDismiss
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -28,7 +26,7 @@ struct DetailView: View {
|
|||||||
showDeleteButton: isCurrentUserDynamic,
|
showDeleteButton: isCurrentUserDynamic,
|
||||||
isDeleteLoading: store.isDeleteLoading,
|
isDeleteLoading: store.isDeleteLoading,
|
||||||
onBack: {
|
onBack: {
|
||||||
onDismiss?() // 调用父视图的关闭回调
|
// 移除 onDismiss?() 调用,因为现在使用 dismiss()
|
||||||
},
|
},
|
||||||
onDelete: {
|
onDelete: {
|
||||||
store.send(.deleteDynamic)
|
store.send(.deleteDynamic)
|
||||||
@@ -72,7 +70,7 @@ struct DetailView: View {
|
|||||||
.onChange(of: store.shouldDismiss) { shouldDismiss in
|
.onChange(of: store.shouldDismiss) { shouldDismiss in
|
||||||
if shouldDismiss {
|
if shouldDismiss {
|
||||||
debugInfoSync("🔍 DetailView: shouldDismiss = true, 调用onDismiss")
|
debugInfoSync("🔍 DetailView: shouldDismiss = true, 调用onDismiss")
|
||||||
onDismiss?()
|
// 移除 onDismiss?() 移除此行,因为我们现在使用 dismiss
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fullScreenCover(isPresented: Binding(
|
.fullScreenCover(isPresented: Binding(
|
||||||
@@ -117,11 +115,22 @@ struct CustomNavigationBar: View {
|
|||||||
let isDeleteLoading: Bool
|
let isDeleteLoading: Bool
|
||||||
let onBack: () -> Void
|
let onBack: () -> Void
|
||||||
let onDelete: () -> 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 {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
// 返回按钮
|
// 返回按钮
|
||||||
Button(action: onBack) {
|
Button(action: {
|
||||||
|
onBack()
|
||||||
|
dismiss() // 使用 dismiss 关闭视图
|
||||||
|
}) {
|
||||||
Image(systemName: "chevron.left")
|
Image(systemName: "chevron.left")
|
||||||
.font(.system(size: 20, weight: .medium))
|
.font(.system(size: 20, weight: .medium))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
@@ -216,4 +225,4 @@ struct CustomNavigationBar: View {
|
|||||||
// DetailFeature()
|
// DetailFeature()
|
||||||
// }
|
// }
|
||||||
// )
|
// )
|
||||||
//}
|
//}
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ private struct LoginContentView: View {
|
|||||||
.keyboardType(.numberPad)
|
.keyboardType(.numberPad)
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
store.send(.getVerificationCodeButtonTapped)
|
store.send(.getVerificationCodeTapped)
|
||||||
}) {
|
}) {
|
||||||
Text(getCodeButtonText)
|
Text(getCodeButtonText)
|
||||||
.font(.system(size: 14, weight: .medium))
|
.font(.system(size: 14, weight: .medium))
|
||||||
@@ -223,7 +223,7 @@ private struct LoginContentView: View {
|
|||||||
|
|
||||||
// 登录按钮
|
// 登录按钮
|
||||||
Button(action: {
|
Button(action: {
|
||||||
store.send(.loginButtonTapped)
|
store.send(.loginButtonTapped(email: email, verificationCode: verificationCode))
|
||||||
}) {
|
}) {
|
||||||
if store.isLoading {
|
if store.isLoading {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import ComposableArchitecture
|
import ComposableArchitecture
|
||||||
import PhotosUI
|
import PhotosUI
|
||||||
//import ImagePreviewPager
|
|
||||||
|
|
||||||
struct EditFeedView: View {
|
struct EditFeedView: View {
|
||||||
let onDismiss: () -> Void
|
let onDismiss: () -> Void
|
||||||
@@ -46,35 +45,26 @@ struct EditFeedView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
store.send(.clearError)
|
store.send(.clearError)
|
||||||
}
|
}
|
||||||
.onChange(of: store.shouldDismiss) { shouldDismiss in
|
.onChange(of: store.shouldDismiss) {
|
||||||
if shouldDismiss {
|
if store.shouldDismiss {
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.photosPicker(
|
.photosPicker(
|
||||||
isPresented: store.binding(
|
isPresented: Binding(
|
||||||
get: \.showPhotosPicker,
|
get: { store.showPhotosPicker },
|
||||||
send: { _ in .photosPickerDismissed }
|
set: { _ in store.send(.photosPickerDismissed) }
|
||||||
),
|
),
|
||||||
selection: store.binding(
|
selection: Binding(
|
||||||
get: \.selectedPhotoItems,
|
get: { store.selectedPhotoItems },
|
||||||
send: { .photosPickerItemsChanged($0) }
|
set: { store.send(.photosPickerItemsChanged($0)) }
|
||||||
),
|
),
|
||||||
maxSelectionCount: 9,
|
maxSelectionCount: 9,
|
||||||
matching: .images
|
matching: .images
|
||||||
)
|
)
|
||||||
.onChange(of: store.selectedPhotoItems) { items in
|
.alert("删除图片", isPresented: Binding(
|
||||||
store.send(.photosPickerItemsChanged(items))
|
get: { store.showDeleteImageAlert },
|
||||||
}
|
set: { _ in store.send(.deleteImageAlertDismissed) }
|
||||||
.onChange(of: store.selectedImages) { images in
|
|
||||||
// 处理图片选择变化
|
|
||||||
}
|
|
||||||
.onChange(of: store.content) { content in
|
|
||||||
// 处理内容变化
|
|
||||||
}
|
|
||||||
.alert("删除图片", isPresented: store.binding(
|
|
||||||
get: \.showDeleteImageAlert,
|
|
||||||
send: { _ in .deleteImageAlertDismissed }
|
|
||||||
)) {
|
)) {
|
||||||
Button("删除", role: .destructive) {
|
Button("删除", role: .destructive) {
|
||||||
if let indexToDelete = store.imageToDeleteIndex {
|
if let indexToDelete = store.imageToDeleteIndex {
|
||||||
@@ -98,19 +88,12 @@ struct EditFeedView: View {
|
|||||||
private func mainContent(geometry: GeometryProxy) -> some View {
|
private func mainContent(geometry: GeometryProxy) -> some View {
|
||||||
WithPerceptionTracking {
|
WithPerceptionTracking {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// 顶部导航栏
|
|
||||||
topNavigationBar
|
topNavigationBar
|
||||||
|
|
||||||
// 主要内容区域
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
// 文本输入区域
|
|
||||||
textInputSection
|
textInputSection
|
||||||
|
|
||||||
// 图片选择区域
|
|
||||||
imageSelectionSection
|
imageSelectionSection
|
||||||
|
|
||||||
// 发布按钮
|
|
||||||
publishButton
|
publishButton
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
@@ -141,7 +124,6 @@ struct EditFeedView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
// 占位,保持标题居中
|
|
||||||
Color.clear
|
Color.clear
|
||||||
.frame(width: 44, height: 44)
|
.frame(width: 44, height: 44)
|
||||||
}
|
}
|
||||||
@@ -158,9 +140,9 @@ struct EditFeedView: View {
|
|||||||
.font(.system(size: 16, weight: .medium))
|
.font(.system(size: 16, weight: .medium))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
|
||||||
TextEditor(text: store.binding(
|
TextEditor(text: Binding(
|
||||||
get: \.content,
|
get: { store.content },
|
||||||
send: { .contentChanged($0) }
|
set: { store.send(.contentChanged($0)) }
|
||||||
))
|
))
|
||||||
.font(.system(size: 16))
|
.font(.system(size: 16))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
@@ -191,66 +173,19 @@ struct EditFeedView: View {
|
|||||||
.font(.system(size: 16, weight: .medium))
|
.font(.system(size: 16, weight: .medium))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3), spacing: 8) {
|
ImageGrid(
|
||||||
// 已选择的图片
|
images: store.processedImages,
|
||||||
ForEach(Array(store.selectedImages.enumerated()), id: \.offset) { index, image in
|
onRemoveImage: { index in
|
||||||
imageItem(image: image, index: index)
|
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 {
|
private var publishButton: some View {
|
||||||
WithPerceptionTracking {
|
WithPerceptionTracking {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
@@ -310,72 +245,78 @@ struct EditFeedView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//#Preview {
|
// MARK: - 简化的图片网格组件
|
||||||
// EditFeedView()
|
struct ImageGrid: View {
|
||||||
//}
|
|
||||||
|
|
||||||
// MARK: - 九宫格图片选择组件
|
|
||||||
struct ModernImageSelectionGrid: View {
|
|
||||||
let images: [UIImage]
|
let images: [UIImage]
|
||||||
let selectedItems: [PhotosPickerItem]
|
|
||||||
let canAddMore: Bool
|
|
||||||
let onItemsChanged: ([PhotosPickerItem]) -> Void
|
|
||||||
let onRemoveImage: (Int) -> Void
|
let onRemoveImage: (Int) -> Void
|
||||||
|
let onAddImage: () -> Void
|
||||||
|
|
||||||
private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3)
|
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 {
|
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) {
|
LazyVGrid(columns: columns, spacing: 8) {
|
||||||
ForEach(Array(images.enumerated()), id: \.offset) { index, image in
|
ForEach(Array(images.enumerated()), id: \.offset) { index, image in
|
||||||
ZStack(alignment: .topTrailing) {
|
ImageGridItem(
|
||||||
Image(uiImage: image)
|
image: image,
|
||||||
.resizable()
|
onRemove: { onRemoveImage(index) }
|
||||||
.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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if canAddMore {
|
|
||||||
PhotosPicker(
|
if images.count < 9 {
|
||||||
selection: .init(
|
AddImageButton(onTap: onAddImage)
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.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
|
@Binding var previewCurrentIndex: Int
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
WithPerceptionTracking {
|
if store.isLoading {
|
||||||
if store.isLoading {
|
FeedListLoadingView()
|
||||||
LoadingView()
|
} else if let error = store.error {
|
||||||
} else if let error = store.error {
|
ErrorView(error: error)
|
||||||
ErrorView(error: error)
|
} else if store.moments.isEmpty {
|
||||||
} else if store.moments.isEmpty {
|
EmptyView()
|
||||||
EmptyView()
|
} else {
|
||||||
} else {
|
MomentsListView(
|
||||||
MomentsListView(
|
moments: store.moments,
|
||||||
moments: store.moments,
|
hasMore: store.hasMore,
|
||||||
hasMore: store.hasMore,
|
isLoadingMore: store.isLoadingMore,
|
||||||
isLoadingMore: store.isLoadingMore,
|
onImageTap: { images, tappedIndex in
|
||||||
onImageTap: { images, tappedIndex in
|
previewCurrentIndex = tappedIndex
|
||||||
previewCurrentIndex = tappedIndex
|
previewItem = PreviewItem(images: images, index: tappedIndex)
|
||||||
previewItem = PreviewItem(images: images, index: tappedIndex)
|
},
|
||||||
},
|
onMomentTap: { moment in
|
||||||
onMomentTap: { moment in
|
store.send(.showDetail(moment))
|
||||||
store.send(.showDetail(moment))
|
},
|
||||||
},
|
onLikeTap: { dynamicId, uid, likedUid, worldId in
|
||||||
onLikeTap: { dynamicId, uid, likedUid, worldId in
|
store.send(.likeDynamic(dynamicId, uid, likedUid, worldId))
|
||||||
store.send(.likeDynamic(dynamicId, uid, likedUid, worldId))
|
},
|
||||||
},
|
onLoadMore: {
|
||||||
onLoadMore: {
|
store.send(.loadMore)
|
||||||
store.send(.loadMore)
|
},
|
||||||
},
|
onRefresh: {
|
||||||
onRefresh: {
|
store.send(.reload)
|
||||||
store.send(.reload)
|
},
|
||||||
},
|
likeLoadingDynamicIds: store.likeLoadingDynamicIds
|
||||||
likeLoadingDynamicIds: store.likeLoadingDynamicIds
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,7 +212,7 @@ struct FeedListView: View {
|
|||||||
@State private var previewCurrentIndex: Int = 0
|
@State private var previewCurrentIndex: Int = 0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
WithPerceptionTracking {
|
WithViewStore(store, observe: { $0 }) { viewStore in
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
ZStack {
|
ZStack {
|
||||||
// 背景
|
// 背景
|
||||||
@@ -252,41 +250,30 @@ struct FeedListView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
store.send(.onAppear)
|
store.send(.onAppear)
|
||||||
}
|
}
|
||||||
.onRefresh {
|
.refreshable {
|
||||||
store.send(.reload)
|
store.send(.reload)
|
||||||
}
|
}
|
||||||
// 新增:编辑动态页面
|
// 新增:编辑动态页面
|
||||||
.sheet(isPresented: store.binding(
|
.sheet(isPresented: viewStore.binding(get: \.isEditFeedPresented, send: { _ in .editFeedDismissed })) {
|
||||||
get: \.showEditFeed,
|
EditFeedView(
|
||||||
send: { _ in .editFeedDismissed }
|
onDismiss: {
|
||||||
)) {
|
store.send(.editFeedDismissed)
|
||||||
WithPerceptionTracking {
|
},
|
||||||
EditFeedView(
|
store: Store(
|
||||||
onDismiss: {
|
initialState: EditFeedFeature.State()
|
||||||
store.send(.editFeedDismissed)
|
) {
|
||||||
},
|
EditFeedFeature()
|
||||||
store: Store(
|
}
|
||||||
initialState: EditFeedFeature.State()
|
)
|
||||||
) {
|
|
||||||
EditFeedFeature()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 新增:详情页导航
|
// 新增:详情页导航
|
||||||
.navigationDestination(isPresented: store.binding(
|
.navigationDestination(isPresented: viewStore.binding(get: \.showDetail, send: { _ in .detailDismissed })) {
|
||||||
get: \.showDetail,
|
if let selectedMoment = viewStore.selectedMoment {
|
||||||
send: { _ in .detailDismissed }
|
|
||||||
)) {
|
|
||||||
if let selectedMoment = store.selectedMoment {
|
|
||||||
DetailView(
|
DetailView(
|
||||||
store: Store(
|
store: Store(
|
||||||
initialState: DetailFeature.State(moment: selectedMoment)
|
initialState: DetailFeature.State(moment: selectedMoment)
|
||||||
) {
|
) {
|
||||||
DetailFeature()
|
DetailFeature()
|
||||||
},
|
|
||||||
onDismiss: {
|
|
||||||
store.send(.detailDismissed)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,174 @@ import SwiftUI
|
|||||||
import ComposableArchitecture
|
import ComposableArchitecture
|
||||||
import Perception
|
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 {
|
struct IDLoginView: View {
|
||||||
let store: StoreOf<IDLoginFeature>
|
let store: StoreOf<IDLoginFeature>
|
||||||
let onBack: () -> Void
|
let onBack: () -> Void
|
||||||
@Binding var showIDLogin: Bool // 新增:绑定父视图的显示状态
|
@Binding var showIDLogin: Bool
|
||||||
|
|
||||||
// 使用本地@State管理UI状态
|
// 使用本地@State管理UI状态
|
||||||
@State private var userID: String = ""
|
@State private var userID: String = ""
|
||||||
@State private var password: String = ""
|
@State private var password: String = ""
|
||||||
@State private var isPasswordVisible: Bool = false
|
@State private var isPasswordVisible: Bool = false
|
||||||
|
|
||||||
// 导航状态管理 - 与 LoginView 保持一致
|
// 导航状态管理
|
||||||
@State private var showRecoverPassword: Bool = false
|
@State private var showRecoverPassword: Bool = false
|
||||||
|
|
||||||
// 计算登录按钮是否可用
|
// 计算登录按钮是否可用
|
||||||
@@ -24,28 +181,12 @@ struct IDLoginView: View {
|
|||||||
WithPerceptionTracking {
|
WithPerceptionTracking {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
ZStack {
|
ZStack {
|
||||||
// 背景图片 - 使用与登录页面相同的"bg"
|
// 背景
|
||||||
Image("bg")
|
IDLoginBackgroundView()
|
||||||
.resizable()
|
|
||||||
.aspectRatio(contentMode: .fill)
|
|
||||||
.ignoresSafeArea(.all)
|
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// 顶部导航栏
|
// 顶部导航栏
|
||||||
HStack {
|
IDLoginHeaderView(onBack: onBack)
|
||||||
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)
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
.frame(height: 60)
|
.frame(height: 60)
|
||||||
@@ -59,68 +200,23 @@ struct IDLoginView: View {
|
|||||||
// 输入框区域
|
// 输入框区域
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
// 用户ID输入框
|
// 用户ID输入框
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
IDLoginInputFieldView(
|
||||||
HStack {
|
iconName: "id icon",
|
||||||
Image("id icon")
|
title: LocalizedString("id_login.user_id", comment: ""),
|
||||||
.resizable()
|
text: $userID,
|
||||||
.aspectRatio(contentMode: .fit)
|
onChange: { newValue in
|
||||||
.frame(width: 20, height: 20)
|
store.send(.userIDChanged(newValue))
|
||||||
Text(LocalizedString("id_login.user_id", comment: ""))
|
|
||||||
.font(.system(size: 16))
|
|
||||||
.foregroundColor(.white)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
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) {
|
IDLoginPasswordFieldView(
|
||||||
HStack {
|
password: $password,
|
||||||
Image("email icon")
|
isPasswordVisible: $isPasswordVisible,
|
||||||
.resizable()
|
onChange: { newValue in
|
||||||
.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
|
|
||||||
store.send(.passwordChanged(newValue))
|
store.send(.passwordChanged(newValue))
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
// 忘记密码按钮
|
// 忘记密码按钮
|
||||||
HStack {
|
HStack {
|
||||||
@@ -135,45 +231,26 @@ struct IDLoginView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 登录按钮
|
// 登录按钮
|
||||||
Button(action: {
|
IDLoginButtonView(
|
||||||
store.send(.loginButtonTapped)
|
isLoading: store.isLoading,
|
||||||
}) {
|
isEnabled: isLoginButtonEnabled,
|
||||||
if store.isLoading {
|
onTap: {
|
||||||
ProgressView()
|
store.send(.loginButtonTapped(userID: userID, password: password))
|
||||||
.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(isLoginButtonEnabled ? Color.blue : Color.gray)
|
|
||||||
.cornerRadius(8)
|
|
||||||
.disabled(!isLoginButtonEnabled)
|
|
||||||
.padding(.top, 20)
|
|
||||||
|
|
||||||
// 错误信息显示
|
// 错误信息显示
|
||||||
if let errorMessage = store.errorMessage {
|
IDLoginErrorView(errorMessage: store.errorMessage)
|
||||||
Text(errorMessage)
|
|
||||||
.font(.system(size: 14))
|
|
||||||
.foregroundColor(.red)
|
|
||||||
.padding(.top, 16)
|
|
||||||
.padding(.horizontal, 32)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加API Loading和错误处理视图
|
// API Loading视图
|
||||||
APILoadingEffectView()
|
APILoadingEffectView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
// 使用与 LoginView 一致的 navigationDestination 方式
|
|
||||||
.navigationDestination(isPresented: $showRecoverPassword) {
|
.navigationDestination(isPresented: $showRecoverPassword) {
|
||||||
WithPerceptionTracking {
|
WithPerceptionTracking {
|
||||||
RecoverPasswordView(
|
RecoverPasswordView(
|
||||||
@@ -197,7 +274,6 @@ struct IDLoginView: View {
|
|||||||
isPasswordVisible = store.isPasswordVisible
|
isPasswordVisible = store.isPasswordVisible
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// 移除测试用的硬编码凭据
|
|
||||||
debugInfoSync("🐛 Debug模式: 已移除硬编码测试凭据")
|
debugInfoSync("🐛 Debug模式: 已移除硬编码测试凭据")
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ struct ImageHeightPreferenceKey: PreferenceKey {
|
|||||||
|
|
||||||
struct LoginView: View {
|
struct LoginView: View {
|
||||||
let store: StoreOf<LoginFeature>
|
let store: StoreOf<LoginFeature>
|
||||||
let onLoginSuccess: () -> Void // 新增:登录成功回调
|
let onLoginSuccess: () -> Void
|
||||||
|
|
||||||
// 使用本地@State管理UI状态
|
// 使用本地@State管理UI状态
|
||||||
@State private var showIDLogin: Bool = false
|
@State private var showIDLogin: Bool = false
|
||||||
@@ -21,136 +21,44 @@ struct LoginView: View {
|
|||||||
@State private var showUserAgreement: Bool = false
|
@State private var showUserAgreement: Bool = false
|
||||||
@State private var showPrivacyPolicy: Bool = false
|
@State private var showPrivacyPolicy: Bool = false
|
||||||
|
|
||||||
// 计算属性
|
|
||||||
private var topImageHeight: CGFloat = 200 // 默认值
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
WithPerceptionTracking {
|
NavigationStack {
|
||||||
NavigationStack {
|
GeometryReader { geometry in
|
||||||
GeometryReader { geometry in
|
ZStack {
|
||||||
ZStack {
|
backgroundView
|
||||||
// 使用与 splash 相同的背景图片
|
mainContentView(geometry: geometry)
|
||||||
Image("bg")
|
APILoadingEffectView()
|
||||||
.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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.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) {
|
.sheet(isPresented: $showLanguageSettings) {
|
||||||
WithPerceptionTracking {
|
LanguageSettingsView(isPresented: $showLanguageSettings)
|
||||||
LanguageSettingsView(isPresented: $showLanguageSettings)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.webView(
|
.webView(
|
||||||
isPresented: $showUserAgreement,
|
isPresented: $showUserAgreement,
|
||||||
@@ -160,26 +68,122 @@ struct LoginView: View {
|
|||||||
isPresented: $showPrivacyPolicy,
|
isPresented: $showPrivacyPolicy,
|
||||||
url: APIConfiguration.webURL(for: .privacyPolicy)
|
url: APIConfiguration.webURL(for: .privacyPolicy)
|
||||||
)
|
)
|
||||||
// 新增:监听登录成功,调用回调
|
.onChange(of: store.isAnyLoginCompleted) {
|
||||||
.onChange(of: store.isAnyLoginCompleted) { completed in
|
if store.isAnyLoginCompleted {
|
||||||
if completed {
|
|
||||||
onLoginSuccess()
|
onLoginSuccess()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 新增:监听showIDLogin关闭时,若已登录则跳转首页
|
.onChange(of: showIDLogin) {
|
||||||
.onChange(of: showIDLogin) { newValue in
|
if showIDLogin == false && store.isAnyLoginCompleted {
|
||||||
if newValue == false && store.isAnyLoginCompleted {
|
|
||||||
onLoginSuccess()
|
onLoginSuccess()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 新增:监听showEmailLogin关闭时,若已登录则跳转首页
|
.onChange(of: showEmailLogin) {
|
||||||
.onChange(of: showEmailLogin) { newValue in
|
if showEmailLogin == false && store.isAnyLoginCompleted {
|
||||||
if newValue == false && store.isAnyLoginCompleted {
|
|
||||||
onLoginSuccess()
|
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 {
|
//#Preview {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ struct MainView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
WithPerceptionTracking {
|
WithPerceptionTracking {
|
||||||
InternalMainView(store: store)
|
InternalMainView(store: store)
|
||||||
.onChange(of: store.isLoggedOut) { isLoggedOut in
|
.onChange(of: store.isLoggedOut) {
|
||||||
if isLoggedOut {
|
if store.isLoggedOut {
|
||||||
onLogout?()
|
onLogout?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,12 +32,12 @@ struct InternalMainView: View {
|
|||||||
.navigationDestination(for: MainFeature.Destination.self) { destination in
|
.navigationDestination(for: MainFeature.Destination.self) { destination in
|
||||||
DestinationView(destination: destination, store: self.store)
|
DestinationView(destination: destination, store: self.store)
|
||||||
}
|
}
|
||||||
.onChange(of: path) { newPath in
|
.onChange(of: path) {
|
||||||
store.send(.navigationPathChanged(newPath))
|
store.send(.navigationPathChanged(path))
|
||||||
}
|
}
|
||||||
.onChange(of: store.navigationPath) { newPath in
|
.onChange(of: store.navigationPath) {
|
||||||
if path != newPath {
|
if path != store.navigationPath {
|
||||||
path = newPath
|
path = store.navigationPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
@@ -91,9 +91,11 @@ struct InternalMainView: View {
|
|||||||
// 底部导航栏 - 固定在底部
|
// 底部导航栏 - 固定在底部
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
BottomTabView(selectedTab: store.binding(
|
BottomTabView(selectedTab: Binding(
|
||||||
get: { Tab(rawValue: $0.selectedTab.rawValue) ?? .feed },
|
get: { Tab(rawValue: store.selectedTab.rawValue) ?? .feed },
|
||||||
send: { MainFeature.Action.selectTab(MainFeature.Tab(rawValue: $0.rawValue) ?? .feed) }
|
set: { newTab in
|
||||||
|
store.send(.selectTab(MainFeature.Tab(rawValue: newTab.rawValue) ?? .feed))
|
||||||
|
}
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
||||||
|
|||||||
@@ -60,21 +60,23 @@ struct MeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 新增:详情页导航
|
// 新增:详情页导航
|
||||||
.navigationDestination(isPresented: store.binding(
|
.navigationDestination(isPresented: Binding(
|
||||||
get: \.showDetail,
|
get: { store.showDetail },
|
||||||
send: { _ in .detailDismissed }
|
set: { _ in store.send(.detailDismissed) }
|
||||||
)) {
|
)) {
|
||||||
if let selectedMoment = store.selectedMoment {
|
if let selectedMoment = store.selectedMoment {
|
||||||
DetailView(
|
let detailStore = Store(
|
||||||
store: Store(
|
initialState: DetailFeature.State(moment: selectedMoment)
|
||||||
initialState: DetailFeature.State(moment: selectedMoment)
|
) {
|
||||||
) {
|
DetailFeature()
|
||||||
DetailFeature()
|
}
|
||||||
},
|
|
||||||
onDismiss: {
|
DetailView(store: detailStore)
|
||||||
store.send(.detailDismissed)
|
.onChange(of: detailStore.shouldDismiss) { shouldDismiss in
|
||||||
|
if shouldDismiss {
|
||||||
|
store.send(.detailDismissed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,7 +123,7 @@ struct MeView: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func momentsSection() -> some View {
|
private func momentsSection() -> some View {
|
||||||
WithPerceptionTracking {
|
WithPerceptionTracking {
|
||||||
if store.isLoading {
|
if store.isLoadingUserInfo || store.isLoadingMoments {
|
||||||
VStack {
|
VStack {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
@@ -132,7 +134,7 @@ struct MeView: View {
|
|||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
} else if let error = store.error {
|
} else if let error = store.userInfoError ?? store.momentsError {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
Image(systemName: "exclamationmark.triangle")
|
Image(systemName: "exclamationmark.triangle")
|
||||||
.font(.system(size: 32))
|
.font(.system(size: 32))
|
||||||
|
|||||||
@@ -5,50 +5,48 @@ struct SplashView: View {
|
|||||||
let store: StoreOf<SplashFeature>
|
let store: StoreOf<SplashFeature>
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
WithPerceptionTracking {
|
ZStack {
|
||||||
ZStack {
|
Group {
|
||||||
Group {
|
// 根据导航目标显示不同页面
|
||||||
// 根据导航目标显示不同页面
|
if let navigationDestination = store.navigationDestination {
|
||||||
if let navigationDestination = store.navigationDestination {
|
switch navigationDestination {
|
||||||
switch navigationDestination {
|
case .login:
|
||||||
case .login:
|
// 显示登录页面
|
||||||
// 显示登录页面
|
LoginView(
|
||||||
LoginView(
|
store: Store(
|
||||||
store: Store(
|
initialState: LoginFeature.State()
|
||||||
initialState: LoginFeature.State()
|
) {
|
||||||
) {
|
LoginFeature()
|
||||||
LoginFeature()
|
},
|
||||||
},
|
onLoginSuccess: {
|
||||||
onLoginSuccess: {
|
// 登录成功后导航到主页面
|
||||||
// 登录成功后导航到主页面
|
store.send(.navigateToMain)
|
||||||
store.send(.navigateToMain)
|
}
|
||||||
}
|
)
|
||||||
)
|
case .main:
|
||||||
case .main:
|
// 显示主应用页面
|
||||||
// 显示主应用页面
|
MainView(
|
||||||
MainView(
|
store: Store(
|
||||||
store: Store(
|
initialState: MainFeature.State()
|
||||||
initialState: MainFeature.State()
|
) {
|
||||||
) {
|
MainFeature()
|
||||||
MainFeature()
|
},
|
||||||
},
|
onLogout: {
|
||||||
onLogout: {
|
store.send(.navigateToLogin)
|
||||||
store.send(.navigateToLogin)
|
}
|
||||||
}
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 显示启动画面
|
|
||||||
splashContent
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 显示启动画面
|
||||||
|
splashContent
|
||||||
}
|
}
|
||||||
.onAppear {
|
|
||||||
store.send(.onAppear)
|
|
||||||
}
|
|
||||||
|
|
||||||
// API Loading 效果视图 - 显示在所有内容之上
|
|
||||||
APILoadingEffectView()
|
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
store.send(.onAppear)
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Loading 效果视图 - 显示在所有内容之上
|
||||||
|
APILoadingEffectView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Yana 项目问题排查与解决流程文档
|
# Yana 项目问题排查与解决流程文档
|
||||||
|
|
||||||
## 目录
|
## 目录
|
||||||
|
|
||||||
1. [问题概述](#问题概述)
|
1. [问题概述](#问题概述)
|
||||||
2. [解决流程](#解决流程)
|
2. [解决流程](#解决流程)
|
||||||
3. [技术细节](#技术细节)
|
3. [技术细节](#技术细节)
|
||||||
@@ -13,14 +14,17 @@
|
|||||||
## 问题概述
|
## 问题概述
|
||||||
|
|
||||||
### 初始错误
|
### 初始错误
|
||||||
|
|
||||||
**错误信息**: `"Could not compute dependency graph: unable to load transferred PIF: The workspace contains multiple references with the same GUID"`
|
**错误信息**: `"Could not compute dependency graph: unable to load transferred PIF: The workspace contains multiple references with the same GUID"`
|
||||||
|
|
||||||
**问题表现**:
|
**问题表现**:
|
||||||
|
|
||||||
- 项目无法启动
|
- 项目无法启动
|
||||||
- Xcode 无法计算依赖图
|
- Xcode 无法计算依赖图
|
||||||
- 出现 GUID 冲突错误
|
- 出现 GUID 冲突错误
|
||||||
|
|
||||||
### 根本原因分析
|
### 根本原因分析
|
||||||
|
|
||||||
1. **混合包管理系统**: 项目同时使用了 Swift Package Manager (SPM) 和 CocoaPods
|
1. **混合包管理系统**: 项目同时使用了 Swift Package Manager (SPM) 和 CocoaPods
|
||||||
2. **缓存冲突**: Xcode DerivedData 与 SPM 状态不同步
|
2. **缓存冲突**: Xcode DerivedData 与 SPM 状态不同步
|
||||||
3. **TCA 结构问题**: 代码中 HomeFeature 缺少必要的状态和 Action 定义
|
3. **TCA 结构问题**: 代码中 HomeFeature 缺少必要的状态和 Action 定义
|
||||||
@@ -32,6 +36,7 @@
|
|||||||
### 第一阶段:GUID 冲突解决
|
### 第一阶段:GUID 冲突解决
|
||||||
|
|
||||||
#### 步骤 1: 清理缓存
|
#### 步骤 1: 清理缓存
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 清理 Xcode DerivedData
|
# 清理 Xcode DerivedData
|
||||||
rm -rf ~/Library/Developer/Xcode/DerivedData/*
|
rm -rf ~/Library/Developer/Xcode/DerivedData/*
|
||||||
@@ -42,11 +47,13 @@ swift package resolve
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### 步骤 2: 重新安装 CocoaPods
|
#### 步骤 2: 重新安装 CocoaPods
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pod install --clean-install
|
pod install --clean-install
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 步骤 3: 验证项目解析
|
#### 步骤 3: 验证项目解析
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
xcodebuild -workspace yana.xcworkspace -list
|
xcodebuild -workspace yana.xcworkspace -list
|
||||||
```
|
```
|
||||||
@@ -54,13 +61,15 @@ xcodebuild -workspace yana.xcworkspace -list
|
|||||||
### 第二阶段:TCA 结构修复
|
### 第二阶段:TCA 结构修复
|
||||||
|
|
||||||
#### 问题识别
|
#### 问题识别
|
||||||
|
|
||||||
- `HomeFeature.State` 缺少 `isSettingPresented` 和 `settingState` 属性
|
- `HomeFeature.State` 缺少 `isSettingPresented` 和 `settingState` 属性
|
||||||
- `HomeFeature.Action` 缺少 `settingDismissed` 和 `setting` actions
|
- `HomeFeature.Action` 缺少 `settingDismissed` 和 `setting` actions
|
||||||
- `HomeView.swift` 中的 `store.scope()` 调用语法错误
|
- `HomeView.swift` 中的 `store.scope()` 调用语法错误
|
||||||
|
|
||||||
#### 修复步骤
|
#### 修复步骤
|
||||||
|
|
||||||
**1. 修复 HomeFeature.swift**
|
1. 修复 HomeFeature.swift
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
@ObservableState
|
@ObservableState
|
||||||
struct State: Equatable {
|
struct State: Equatable {
|
||||||
@@ -89,7 +98,8 @@ enum Action: Equatable {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**2. 添加子 Reducer**
|
2.添加子 Reducer
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
var body: some ReducerOf<Self> {
|
var body: some ReducerOf<Self> {
|
||||||
Scope(state: \.settingState, action: \.setting) {
|
Scope(state: \.settingState, action: \.setting) {
|
||||||
@@ -110,7 +120,8 @@ var body: some ReducerOf<Self> {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**3. 修复 HomeView.swift**
|
3.修复 HomeView.swift
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
.sheet(isPresented: Binding(
|
.sheet(isPresented: Binding(
|
||||||
get: { store.isSettingPresented },
|
get: { store.isSettingPresented },
|
||||||
@@ -127,10 +138,12 @@ var body: some ReducerOf<Self> {
|
|||||||
### 依赖管理配置
|
### 依赖管理配置
|
||||||
|
|
||||||
**Swift Package Manager (Package.swift)**:
|
**Swift Package Manager (Package.swift)**:
|
||||||
|
|
||||||
- ComposableArchitecture: 1.20.2+
|
- ComposableArchitecture: 1.20.2+
|
||||||
- 其他依赖根据需要添加
|
- 其他依赖根据需要添加
|
||||||
|
|
||||||
**CocoaPods (Podfile)**:
|
**CocoaPods (Podfile)**:
|
||||||
|
|
||||||
- Alamofire (网络请求)
|
- Alamofire (网络请求)
|
||||||
- SDWebImage (图像加载)
|
- SDWebImage (图像加载)
|
||||||
- CocoaLumberjack (日志)
|
- CocoaLumberjack (日志)
|
||||||
@@ -147,6 +160,7 @@ Feature
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 文件结构
|
### 文件结构
|
||||||
|
|
||||||
```
|
```
|
||||||
yana/
|
yana/
|
||||||
├── Features/ # TCA Feature 定义
|
├── Features/ # TCA Feature 定义
|
||||||
@@ -161,6 +175,7 @@ yana/
|
|||||||
## 最终解决方案
|
## 最终解决方案
|
||||||
|
|
||||||
### 命令执行顺序
|
### 命令执行顺序
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 清理环境
|
# 1. 清理环境
|
||||||
rm -rf ~/Library/Developer/Xcode/DerivedData/*
|
rm -rf ~/Library/Developer/Xcode/DerivedData/*
|
||||||
@@ -224,33 +239,42 @@ check_project() {
|
|||||||
## 常见问题FAQ
|
## 常见问题FAQ
|
||||||
|
|
||||||
### Q1: 再次出现 GUID 冲突怎么办?
|
### Q1: 再次出现 GUID 冲突怎么办?
|
||||||
|
|
||||||
**A**: 执行完整清理流程:
|
**A**: 执行完整清理流程:
|
||||||
```bash
|
```bash
|
||||||
rm -rf ~/Library/Developer/Xcode/DerivedData/*
|
rm -rf ~/Library/Developer/Xcode/DerivedData/*
|
||||||
swift package reset && swift package resolve
|
swift package reset && swift package resolve
|
||||||
pod install --clean-install
|
pod install --clean-install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Q2: TCA Reducer 编译错误如何处理?
|
### Q2: TCA Reducer 编译错误如何处理?
|
||||||
|
|
||||||
**A**: 检查以下项目:
|
**A**: 检查以下项目:
|
||||||
|
|
||||||
- State 属性完整性
|
- State 属性完整性
|
||||||
- Action 枚举完整性
|
- Action 枚举完整性
|
||||||
- Reducer body 中的 case 处理
|
- Reducer body 中的 case 处理
|
||||||
- 子 Reducer 的 Scope 配置
|
- 子 Reducer 的 Scope 配置
|
||||||
|
|
||||||
### Q3: 如何避免混合包管理器问题?
|
### Q3: 如何避免混合包管理器问题?
|
||||||
**A**:
|
|
||||||
|
**A**:
|
||||||
|
|
||||||
- 尽量使用单一包管理工具
|
- 尽量使用单一包管理工具
|
||||||
- 如需混合使用,确保依赖版本兼容
|
- 如需混合使用,确保依赖版本兼容
|
||||||
- 定期更新依赖并测试
|
- 定期更新依赖并测试
|
||||||
|
|
||||||
### Q4: Swift 6 兼容性警告如何处理?
|
### Q4: Swift 6 兼容性警告如何处理?
|
||||||
**A**:
|
|
||||||
|
**A**:
|
||||||
|
|
||||||
- 短期:可以忽略,不影响功能
|
- 短期:可以忽略,不影响功能
|
||||||
- 长期:逐步迁移到 Swift 6 Sendable 模式
|
- 长期:逐步迁移到 Swift 6 Sendable 模式
|
||||||
|
|
||||||
### Q5: 项目构建缓慢怎么办?
|
### Q5: 项目构建缓慢怎么办?
|
||||||
|
|
||||||
**A**:
|
**A**:
|
||||||
|
|
||||||
- 使用 `xcodebuild -quiet` 减少输出
|
- 使用 `xcodebuild -quiet` 减少输出
|
||||||
- 开启 Xcode Build System 并行构建
|
- 开启 Xcode Build System 并行构建
|
||||||
- 定期清理 DerivedData
|
- 定期清理 DerivedData
|
||||||
@@ -271,5 +295,5 @@ pod install --clean-install
|
|||||||
---
|
---
|
||||||
|
|
||||||
**文档更新时间**: 2025-07-10
|
**文档更新时间**: 2025-07-10
|
||||||
**适用版本**: iOS 16+, Swift 6, TCA 1.20.2+
|
**适用版本**: iOS 17+, Swift 6, TCA 1.20.2+
|
||||||
**维护者**: AI Assistant & 开发团队
|
**维护者**: AI Assistant & 开发团队
|
||||||
|
|||||||
Reference in New Issue
Block a user