
- 将图片选择功能整合到AppSettingView中,使用ImagePickerWithPreviewView提升用户体验。 - 移除冗余的照片选择处理逻辑,简化代码结构。 - 更新昵称编辑功能的实现,确保用户输入限制在15个字符内。 - 优化导航栏和用户协议、隐私政策的展示,增强界面交互性。
368 lines
13 KiB
Swift
368 lines
13 KiB
Swift
//
|
||
// AppSettingView.swift
|
||
// yana
|
||
//
|
||
// Created by Edwin on 2024/11/20.
|
||
//
|
||
|
||
import SwiftUI
|
||
import ComposableArchitecture
|
||
import PhotosUI
|
||
|
||
struct AppSettingView: View {
|
||
let store: StoreOf<AppSettingFeature>
|
||
// 直接let声明pickerStore
|
||
let pickerStore = Store(
|
||
initialState: ImagePickerWithPreviewReducer.State(inner: ImagePickerWithPreviewState(selectionMode: .single)),
|
||
reducer: { ImagePickerWithPreviewReducer() }
|
||
)
|
||
@State private var showImagePicker = false
|
||
@State private var showNicknameAlert = false
|
||
@State private var nicknameInput = ""
|
||
|
||
var body: some View {
|
||
WithViewStore(store, observe: { $0 }) { viewStore in
|
||
ZStack {
|
||
mainContent(viewStore: viewStore)
|
||
if showImagePicker {
|
||
WithPerceptionTracking{
|
||
ImagePickerWithPreviewView(store: pickerStore) { images in
|
||
if let image = images.first, let data = image.jpegData(compressionQuality: 0.8) {
|
||
viewStore.send(.avatarSelected(data))
|
||
}
|
||
showImagePicker = false
|
||
}
|
||
.zIndex(10)
|
||
}
|
||
}
|
||
}
|
||
.navigationBarHidden(true)
|
||
.alert("修改昵称", isPresented: $showNicknameAlert) {
|
||
nicknameAlertContent(viewStore: viewStore)
|
||
} message: {
|
||
Text("昵称最长15个字符")
|
||
}
|
||
.sheet(isPresented: userAgreementBinding(viewStore: viewStore)) {
|
||
WebView(url: URL(string: "https://www.yana.com/user-agreement")!)
|
||
}
|
||
.sheet(isPresented: privacyPolicyBinding(viewStore: viewStore)) {
|
||
WebView(url: URL(string: "https://www.yana.com/privacy-policy")!)
|
||
}
|
||
.onChange(of: showImagePicker) { newValue in
|
||
if newValue {
|
||
pickerStore.send(.inner(.showActionSheet(true)))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 头像区域
|
||
private func avatarSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||
ZStack(alignment: .bottomTrailing) {
|
||
avatarImageView(viewStore: viewStore)
|
||
.onTapGesture {
|
||
showImagePicker = true
|
||
}
|
||
cameraButton
|
||
.onTapGesture {
|
||
showImagePicker = true
|
||
}
|
||
}
|
||
.padding(.top, 24)
|
||
}
|
||
|
||
// MARK: - 头像图片视图
|
||
@ViewBuilder
|
||
private func avatarImageView(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||
if 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 var cameraButton: some View {
|
||
Button(action: {}) {
|
||
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)
|
||
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 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: - 昵称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("确定") {
|
||
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
|
||
Button(action: {
|
||
viewStore.send(.dismissTapped)
|
||
}) {
|
||
Image(systemName: "chevron.left")
|
||
.foregroundColor(.white)
|
||
.font(.system(size: 20, weight: .medium))
|
||
}
|
||
}
|
||
|
||
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 (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)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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))
|
||
}
|
||
}
|