feat: 更新iOS和Podfile的部署目标以支持新版本

- 将iOS平台版本更新至17,确保与最新的开发环境兼容。
- 更新Podfile中的iOS部署目标至17.0,确保依赖项与新版本兼容。
- 修改Podfile.lock以反映新的依赖项版本,确保项目一致性。
- 在多个视图中重构代码,优化状态管理和视图逻辑,提升用户体验。
This commit is contained in:
edwinQQQ
2025-07-29 15:59:09 +08:00
parent 567b1f3fd9
commit 3ec1b1302f
12 changed files with 1053 additions and 1131 deletions

View File

@@ -31,476 +31,327 @@ struct AppSettingView: View {
@State private var errorMessage: String? = nil
var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
WithPerceptionTracking {
mainView(viewStore: viewStore)
}
WithPerceptionTracking {
mainView()
}
}
@ViewBuilder
private func mainView(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
contentWithImagePickers(viewStore: viewStore)
.onChange(of: selectedPhotoItems) { items in
print("[LOG] PhotosPicker选中items: \(items.count)")
guard !items.isEmpty else { return }
isLoading = true
selectedImages = []
let group = DispatchGroup()
var tempImages: [UIImage] = []
for item in items {
group.enter()
item.loadTransferable(type: Data.self) { result in
defer { group.leave() }
if let data = try? result.get(), let uiImage = UIImage(data: data) {
DispatchQueue.main.async {
tempImages.append(uiImage)
print("[LOG] 成功加载图片当前tempImages数量: \(tempImages.count)")
private func mainView() -> some View {
WithPerceptionTracking {
GeometryReader { geometry in
ZStack {
//
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.clipped()
.ignoresSafeArea(.all)
VStack(spacing: 0) {
//
HStack {
Button(action: {
store.send(.dismissTapped)
}) {
Image(systemName: "chevron.left")
.font(.system(size: 24, weight: .medium))
.foregroundColor(.white)
.frame(width: 44, height: 44)
}
} else {
print("[LOG] 图片加载失败")
Spacer()
Text(LocalizedString("app_settings.title", comment: "设置"))
.font(.system(size: 18, weight: .medium))
.foregroundColor(.white)
Spacer()
//
Color.clear
.frame(width: 44, height: 44)
}
}
}
DispatchQueue.global().async {
group.wait()
DispatchQueue.main.async {
isLoading = false
print("[LOG] 所有图片加载完成tempImages数量: \(tempImages.count)")
if tempImages.isEmpty {
errorMessage = "图片加载失败,请重试"
//
showPreview = false
showCamera = false
showPhotoPicker = false
showActionSheet = false
print("[LOG] PhotosPicker图片加载失败弹出错误提示")
} else {
// selectedImages
selectedImages = tempImages
print("[LOG] selectedImages已设置数量: \(selectedImages.count)")
// 线showPreview
DispatchQueue.main.async {
showPreview = true
print("[LOG] showPreview已设置为true")
.padding(.horizontal, 16)
.padding(.top, 8)
//
ScrollView {
VStack(spacing: 20) {
//
avatarSection()
//
personalInfoSection()
//
otherSettingsSection()
// 退
logoutSection()
}
.padding(.horizontal, 20)
.padding(.top, 20)
.padding(.bottom, 40)
}
}
}
}
.alert(isPresented: Binding<Bool>(
get: { errorMessage != nil },
set: { if !$0 { errorMessage = nil } }
)) {
print("[LOG] 错误弹窗弹出: \(errorMessage ?? "")")
return Alert(title: Text(LocalizedString("app_settings.error", comment: "")), message: Text(errorMessage ?? ""), dismissButton: .default(Text(LocalizedString("app_settings.confirm", comment: "")), action: {
// actionset
showPreview = false
showCamera = false
showPhotoPicker = false
showActionSheet = false
}))
}
.navigationBarHidden(true)
.alert("修改昵称", isPresented: $showNicknameAlert) {
nicknameAlertContent(viewStore: viewStore)
} message: {
Text(LocalizedString("app_settings.nickname_limit", comment: ""))
.font(.caption)
.foregroundColor(.secondary)
}
.sheet(isPresented: userAgreementBinding(viewStore: viewStore)) {
WebView(url: APIConfiguration.webURL(for: .userAgreement)!)
}
.sheet(isPresented: privacyPolicyBinding(viewStore: viewStore)) {
WebView(url: APIConfiguration.webURL(for: .privacyPolicy)!)
}
.sheet(isPresented: deactivateAccountBinding(viewStore: viewStore)) {
WebView(url: APIConfiguration.webURL(for: .deactivateAccount)!)
}
}
@ViewBuilder
private func contentWithImagePickers(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
ZStack {
mainContent(viewStore: viewStore)
}
.confirmationDialog(
"请选择图片来源",
isPresented: $showActionSheet,
titleVisibility: .visible
) {
Button(LocalizedString("app_settings.take_photo", comment: "")) { showCamera = true }
Button(LocalizedString("app_settings.select_from_album", comment: "")) { showPhotoPicker = true }
Button("取消", role: .cancel) {}
}
.photosPicker(
isPresented: $showPhotoPicker,
selection: $selectedPhotoItems,
maxSelectionCount: 1,
matching: .images
)
.sheet(isPresented: $showCamera) {
CameraPicker { image in
print("[LOG] CameraPicker回调image: \(image != nil)")
if let image = image {
print("[LOG] CameraPicker获得图片直接上传头像")
if let data = image.jpegData(compressionQuality: 0.8) {
viewStore.send(AppSettingFeature.Action.avatarSelected(data))
}
} else {
errorMessage = "拍照失败,请重试"
//
showPreview = false
showCamera = false
showPhotoPicker = false
showActionSheet = false
print("[LOG] CameraPicker无图片弹出错误提示")
}
showCamera = false
}
}
.fullScreenCover(isPresented: $showPreview) {
ImagePreviewView(
images: $selectedImages,
currentIndex: .constant(0),
onConfirm: {
print("[LOG] 预览确认,准备上传头像")
if let image = selectedImages.first, let data = image.jpegData(compressionQuality: 0.8) {
viewStore.send(AppSettingFeature.Action.avatarSelected(data))
}
showPreview = false
},
onCancel: {
print("[LOG] 预览取消")
showPreview = false
}
)
}
}
// MARK: -
private func mainContent(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
ZStack {
Color(red: 22/255, green: 17/255, blue: 44/255).ignoresSafeArea()
VStack(spacing: 0) {
topBar
ScrollView {
WithPerceptionTracking {
VStack(spacing: 32) {
//
avatarSection(viewStore: viewStore)
//
nicknameSection(viewStore: viewStore)
//
settingsSection(viewStore: viewStore)
// 退
logoutButton(viewStore: viewStore)
.navigationBarHidden(true)
//
.sheet(isPresented: $showImagePickerSheet) {
ImagePickerWithPreviewView(
store: pickerStore,
onUpload: { images in
if let firstImage = images.first,
let imageData = firstImage.jpegData(compressionQuality: 0.8) {
store.send(AppSettingFeature.Action.avatarSelected(imageData))
}
showImagePickerSheet = false
},
onCancel: {
showImagePickerSheet = false
}
)
}
//
.sheet(isPresented: $showCamera) {
ImagePickerWithPreviewView(
store: pickerStore,
onUpload: { images in
if let firstImage = images.first,
let imageData = firstImage.jpegData(compressionQuality: 0.8) {
store.send(AppSettingFeature.Action.avatarSelected(imageData))
}
showCamera = false
},
onCancel: {
showCamera = false
}
)
}
//
.alert(LocalizedString("app_settings.edit_nickname", comment: "编辑昵称"), isPresented: $showNicknameAlert) {
TextField(LocalizedString("app_settings.nickname_placeholder", comment: "请输入昵称"), text: $nicknameInput)
Button(LocalizedString("app_settings.cancel", comment: "取消")) {
showNicknameAlert = false
nicknameInput = ""
}
Button(LocalizedString("app_settings.confirm", comment: "确认")) {
let trimmed = nicknameInput.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty {
store.send(.nicknameEditConfirmed(trimmed))
}
showNicknameAlert = false
nicknameInput = ""
}
} message: {
Text(LocalizedString("app_settings.nickname_tip", comment: "请输入新的昵称"))
}
}
}
// MARK: -
private func avatarSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
ZStack(alignment: .bottomTrailing) {
avatarImageView(viewStore: viewStore)
.onTapGesture {
showActionSheet = true
}
cameraButton(viewStore: viewStore)
}
.padding(.top, 24)
}
// MARK: -
// MARK: -
@ViewBuilder
private func avatarImageView(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
if viewStore.isUploadingAvatar || viewStore.isLoadingUserInfo {
loadingAvatarView
} else if let avatarURLString = viewStore.avatarURL, !avatarURLString.isEmpty, let avatarURL = URL(string: avatarURLString) {
networkAvatarView(url: avatarURL)
} else {
defaultAvatarView
}
}
// MARK: -
private var loadingAvatarView: some View {
Circle()
.fill(Color.gray.opacity(0.3))
.frame(width: 120, height: 120)
.overlay(
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(1.2)
)
}
// MARK: -
private func networkAvatarView(url: URL) -> some View {
CachedAsyncImage(url: url.absoluteString) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
defaultAvatarView
}
.frame(width: 120, height: 120)
.clipShape(Circle())
}
// MARK: -
private var defaultAvatarView: some View {
Circle()
.fill(Color.gray.opacity(0.3))
.frame(width: 120, height: 120)
.overlay(
Image(systemName: "person.fill")
.font(.system(size: 40))
.foregroundColor(.white)
)
}
// MARK: -
private func cameraButton(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
Button(action: {
showActionSheet = true
}) {
ZStack {
Circle().fill(Color.purple).frame(width: 36, height: 36)
Image(systemName: "camera.fill")
.foregroundColor(.white)
}
}
.offset(x: 8, y: 8)
}
// MARK: -
private func nicknameSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
VStack(spacing: 0) {
HStack {
Text(NSLocalizedString("appSetting.nickname", comment: "Nickname"))
.foregroundColor(.white)
Spacer()
Text(viewStore.nickname)
.foregroundColor(.gray)
Image(systemName: "chevron.right")
.foregroundColor(.gray)
}
.padding(.horizontal, 32)
.padding(.vertical, 18)
.onTapGesture {
nicknameInput = viewStore.nickname
showNicknameAlert = true
}
Divider().background(Color.gray.opacity(0.3))
.padding(.horizontal, 32)
}
}
// MARK: -
private func settingsSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
VStack(spacing: 0) {
personalInfoPermissionsRow(viewStore: viewStore)
helpRow(viewStore: viewStore)
clearCacheRow(viewStore: viewStore)
checkUpdatesRow(viewStore: viewStore)
deactivateAccountRow(viewStore: viewStore)
aboutUsRow(viewStore: viewStore)
}
.background(Color.clear)
.padding(.horizontal, 0)
}
// MARK: -
private func personalInfoPermissionsRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
settingRow(
title: NSLocalizedString("appSetting.personalInfoPermissions", comment: "Personal Information and Permissions"),
action: { viewStore.send(.personalInfoPermissionsTapped) }
)
}
// MARK: -
private func helpRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
settingRow(
title: NSLocalizedString("appSetting.help", comment: "Help"),
action: { viewStore.send(.helpTapped) }
)
}
// MARK: -
private func clearCacheRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
settingRow(
title: NSLocalizedString("appSetting.clearCache", comment: "Clear Cache"),
action: { viewStore.send(.clearCacheTapped) }
)
}
// MARK: -
private func checkUpdatesRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
settingRow(
title: NSLocalizedString("appSetting.checkUpdates", comment: "Check for Updates"),
action: { viewStore.send(.checkUpdatesTapped) }
)
}
// MARK: -
private func deactivateAccountRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
settingRow(
title: NSLocalizedString("appSetting.deactivateAccount", comment: "Deactivate Account"),
action: { viewStore.send(.deactivateAccountTapped) }
)
}
// MARK: -
private func aboutUsRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
settingRow(
title: NSLocalizedString("appSetting.aboutUs", comment: "About Us"),
action: { viewStore.send(.aboutUsTapped) }
)
}
// MARK: -
private func settingRow(title: String, action: @escaping () -> Void) -> some View {
VStack(spacing: 0) {
HStack {
Text(title)
.foregroundColor(.white)
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
}
.padding(.horizontal, 32)
.padding(.vertical, 18)
.onTapGesture {
action()
}
Divider().background(Color.gray.opacity(0.3))
.padding(.horizontal, 32)
}
}
// MARK: - 退
private func logoutButton(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
Button(action: {
viewStore.send(.logoutTapped)
}) {
Text(NSLocalizedString("appSetting.logoutAccount", comment: "Log out of account"))
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 18)
.background(Color.white.opacity(0.08))
.cornerRadius(28)
.padding(.horizontal, 32)
}
.padding(.bottom, 32)
}
// MARK: -
private func userAgreementBinding(viewStore: ViewStoreOf<AppSettingFeature>) -> Binding<Bool> {
viewStore.binding(
get: \.showUserAgreement,
send: AppSettingFeature.Action.userAgreementDismissed
)
}
// MARK: -
private func privacyPolicyBinding(viewStore: ViewStoreOf<AppSettingFeature>) -> Binding<Bool> {
viewStore.binding(
get: \.showPrivacyPolicy,
send: AppSettingFeature.Action.privacyPolicyDismissed
)
}
// MARK: -
private func deactivateAccountBinding(viewStore: ViewStoreOf<AppSettingFeature>) -> Binding<Bool> {
viewStore.binding(
get: \.showDeactivateAccount,
send: AppSettingFeature.Action.deactivateAccountDismissed
)
}
// MARK: - Alert
@ViewBuilder
private func nicknameAlertContent(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
TextField("请输入昵称", text: $nicknameInput)
.onChange(of: nicknameInput) { newValue in
if newValue.count > 15 {
nicknameInput = String(newValue.prefix(15))
}
}
Button(LocalizedString("app_settings.confirm", comment: "")) {
let trimmed = nicknameInput.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty && trimmed != viewStore.nickname {
viewStore.send(.nicknameEditConfirmed(trimmed))
}
}
Button("取消", role: .cancel) {}
}
// MARK: -
private var topBar: some View {
HStack {
WithViewStore(store, observe: { $0 }) { viewStore in
private func avatarSection() -> some View {
WithPerceptionTracking {
VStack(spacing: 16) {
//
Button(action: {
viewStore.send(.dismissTapped)
showImagePickerSheet = true
}) {
Image(systemName: "chevron.left")
AsyncImage(url: URL(string: store.userInfo?.avatar ?? "")) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Image(systemName: "person.circle.fill")
.resizable()
.aspectRatio(contentMode: .fill)
.foregroundColor(.gray)
}
.frame(width: 80, height: 80)
.clipShape(Circle())
.overlay(
Circle()
.stroke(Color.white.opacity(0.3), lineWidth: 2)
)
}
Text(LocalizedString("app_settings.tap_to_change_avatar", comment: "点击更换头像"))
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.7))
}
.padding(.vertical, 20)
.frame(maxWidth: .infinity)
.background(Color.white.opacity(0.1))
.cornerRadius(12)
}
}
// MARK: -
@ViewBuilder
private func personalInfoSection() -> some View {
WithPerceptionTracking {
VStack(spacing: 0) {
//
SettingRow(
icon: "person",
title: LocalizedString("app_settings.nickname", comment: "昵称"),
subtitle: store.userInfo?.nick ?? LocalizedString("app_settings.not_set", comment: "未设置"),
action: {
nicknameInput = store.userInfo?.nick ?? ""
showNicknameAlert = true
}
)
Divider()
.background(Color.white.opacity(0.2))
.padding(.leading, 50)
// ID
SettingRow(
icon: "number",
title: LocalizedString("app_settings.user_id", comment: "用户ID"),
subtitle: "\(store.userInfo?.uid ?? 0)",
action: nil
)
}
.background(Color.white.opacity(0.1))
.cornerRadius(12)
}
}
// MARK: -
@ViewBuilder
private func otherSettingsSection() -> some View {
WithPerceptionTracking {
VStack(spacing: 0) {
SettingRow(
icon: "hand.raised",
title: LocalizedString("app_settings.personal_info_permissions", comment: "个人信息权限"),
subtitle: LocalizedString("app_settings.manage_permissions", comment: "管理权限"),
action: { store.send(.personalInfoPermissionsTapped) }
)
Divider()
.background(Color.white.opacity(0.2))
.padding(.leading, 50)
SettingRow(
icon: "questionmark.circle",
title: LocalizedString("app_settings.help", comment: "帮助"),
subtitle: LocalizedString("app_settings.get_help", comment: "获取帮助"),
action: { store.send(.helpTapped) }
)
Divider()
.background(Color.white.opacity(0.2))
.padding(.leading, 50)
SettingRow(
icon: "trash",
title: LocalizedString("app_settings.clear_cache", comment: "清除缓存"),
subtitle: LocalizedString("app_settings.free_up_space", comment: "释放空间"),
action: { store.send(.clearCacheTapped) }
)
Divider()
.background(Color.white.opacity(0.2))
.padding(.leading, 50)
SettingRow(
icon: "arrow.clockwise",
title: LocalizedString("app_settings.check_updates", comment: "检查更新"),
subtitle: LocalizedString("app_settings.latest_version", comment: "最新版本"),
action: { store.send(.checkUpdatesTapped) }
)
Divider()
.background(Color.white.opacity(0.2))
.padding(.leading, 50)
SettingRow(
icon: "person.crop.circle.badge.minus",
title: LocalizedString("app_settings.deactivate_account", comment: "注销账号"),
subtitle: LocalizedString("app_settings.permanent_deletion", comment: "永久删除"),
action: { store.send(.deactivateAccountTapped) }
)
Divider()
.background(Color.white.opacity(0.2))
.padding(.leading, 50)
SettingRow(
icon: "info.circle",
title: LocalizedString("app_settings.about_us", comment: "关于我们"),
subtitle: LocalizedString("app_settings.app_info", comment: "应用信息"),
action: { store.send(.aboutUsTapped) }
)
}
.background(Color.white.opacity(0.1))
.cornerRadius(12)
}
}
// MARK: - 退
@ViewBuilder
private func logoutSection() -> some View {
WithPerceptionTracking {
Button(action: {
store.send(.logoutTapped)
}) {
Text(LocalizedString("app_settings.logout", comment: "退出登录"))
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(Color.red.opacity(0.8))
.cornerRadius(12)
}
}
}
}
// MARK: -
struct SettingRow: View {
let icon: String
let title: String
let subtitle: String
let action: (() -> Void)?
var body: some View {
Button(action: {
action?()
}) {
HStack(spacing: 16) {
Image(systemName: icon)
.font(.system(size: 18))
.foregroundColor(.white)
.frame(width: 24)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.system(size: 16))
.foregroundColor(.white)
.font(.system(size: 20, weight: .medium))
Text(subtitle)
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.7))
}
Spacer()
if action != nil {
Image(systemName: "chevron.right")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.5))
}
}
Spacer()
Text(NSLocalizedString("appSetting.title", comment: "Settings"))
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
Spacer()
//
Color.clear
.frame(width: 20, height: 20)
}
.padding(.horizontal, 20)
.padding(.top, 8)
.padding(.bottom, 16)
}
// MARK: -
private func loadAndProcessImage(item: PhotosPickerItem, completion: @escaping @Sendable (Data?) -> Void) {
item.loadTransferable(type: Data.self) { result in
guard let data = try? result.get(), let uiImage = UIImage(data: data) else {
completion(nil)
return
}
let square = cropToSquare(image: uiImage)
let resized = resizeImage(image: square, targetSize: CGSize(width: 180, height: 180))
let jpegData = resized.jpegData(compressionQuality: 0.8)
completion(jpegData)
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
}
}
// MARK: -
private func cropToSquare(image: UIImage) -> UIImage {
let size = min(image.size.width, image.size.height)
let x = (image.size.width - size) / 2
let y = (image.size.height - size) / 2
let cropRect = CGRect(x: x, y: y, width: size, height: size)
guard let cgImage = image.cgImage?.cropping(to: cropRect) else { return image }
return UIImage(cgImage: cgImage, scale: image.scale, orientation: image.imageOrientation)
}
private func resizeImage(image: UIImage, targetSize: CGSize) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: targetSize))
.disabled(action == nil)
}
}