feat: 更新文档和视图以支持iOS 17及优化用户体验

- 更新Yana项目文档,调整适用版本至iOS 17,确保与最新开发环境兼容。
- 在多个视图中重构代码,优化状态管理和视图逻辑,提升用户体验。
- 添加默认初始化器以简化状态管理,确保各个Feature的状态一致性。
- 更新视图组件,移除不必要的硬编码,增强代码可读性和维护性。
- 修复多个视图中的逻辑错误,确保功能正常运行。
This commit is contained in:
edwinQQQ
2025-07-29 17:57:42 +08:00
parent 3ec1b1302f
commit 3d00e459e3
28 changed files with 689 additions and 554 deletions

View File

@@ -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.

View File

@@ -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
- 已配置框架冲突处理脚本
- 已配置框架冲突处理脚本
-

View File

@@ -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";

View File

@@ -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

View File

@@ -187,8 +187,8 @@ struct ContentView: View {
}
.tag(1)
}
.onChange(of: selectedLogLevel) { newValue in
APILogger.logLevel = newValue
.onChange(of: selectedLogLevel) {
APILogger.logLevel = selectedLogLevel
}
}
}

View File

@@ -24,7 +24,12 @@ struct AppSettingFeature {
var isUpdatingUser: Bool = false
var updateUserError: String? = nil
// userInfoavatarURLnicknameinit
//
init() {
//
}
// userInfoavatarURLnicknameinit
init(nickname: String = "", avatarURL: String? = nil, userInfo: UserInfo? = nil) {
self.nickname = nickname
self.avatarURL = avatarURL

View File

@@ -40,6 +40,10 @@ struct ConfigFeature {
var configData: ConfigData?
var errorMessage: String?
var lastUpdated: Date?
init() {
//
}
}
enum Action: Equatable {

View File

@@ -19,6 +19,10 @@ struct CreateFeedFeature {
!content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isLoading
}
var isLoading: Bool = false
init() {
//
}
}
enum Action {

View File

@@ -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 {

View File

@@ -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() {
//
}
// EquatableselectedImagesPhotosPickerItemEquatable
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)
// PhotosPickerAction
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
// PhotosPickerAction
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
}
}
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -8,6 +8,10 @@ struct InitFeature {
var isLoading = false
var response: InitResponse?
var error: String?
init() {
//
}
}
enum Action: Equatable {

View File

@@ -34,13 +34,11 @@ struct LoginFeature {
case failed //
}
#if DEBUG
init() {
//
//
self.account = ""
self.password = ""
}
#endif
}
enum Action {

View File

@@ -19,6 +19,10 @@ struct MainFeature {
var appSettingState: AppSettingFeature.State? = nil
//
var isLoggedOut: Bool = false
init() {
//
}
}
//

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -12,6 +12,10 @@ struct SplashFeature {
//
var navigationDestination: NavigationDestination?
init() {
//
}
}
//

View File

@@ -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()
// }
// )
//}
//}

View File

@@ -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()

View File

@@ -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)
)
}
}
}

View File

@@ -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)
}
)
}

View File

@@ -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
// 使@StateUI
@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
}

View File

@@ -12,7 +12,7 @@ struct ImageHeightPreferenceKey: PreferenceKey {
struct LoginView: View {
let store: StoreOf<LoginFeature>
let onLoginSuccess: () -> Void //
let onLoginSuccess: () -> Void
// 使@StateUI
@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 {

View File

@@ -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)

View File

@@ -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))

View File

@@ -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()
}
}

View File

@@ -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 & 开发团队