feat: 更新AppSettingView以集成图片选择与预览功能

- 将图片选择功能整合到AppSettingView中,使用ImagePickerWithPreviewView提升用户体验。
- 移除冗余的照片选择处理逻辑,简化代码结构。
- 更新昵称编辑功能的实现,确保用户输入限制在15个字符内。
- 优化导航栏和用户协议、隐私政策的展示,增强界面交互性。
This commit is contained in:
edwinQQQ
2025-07-25 17:20:31 +08:00
parent 2cfdf110af
commit 2f3ef22ce5

View File

@@ -11,44 +11,51 @@ import PhotosUI
struct AppSettingView: View { struct AppSettingView: View {
let store: StoreOf<AppSettingFeature> let store: StoreOf<AppSettingFeature>
@State private var showPhotoPicker = false // letpickerStore
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 showNicknameAlert = false
@State private var nicknameInput = "" @State private var nicknameInput = ""
@State private var selectedPhotoItem: PhotosPickerItem?
var body: some View { var body: some View {
// WithPerceptionTracking { WithViewStore(store, observe: { $0 }) { viewStore in
WithViewStore(store, observe: { $0 }) { viewStore in ZStack {
WithPerceptionTracking{ mainContent(viewStore: viewStore)
mainContent(viewStore: viewStore) if showImagePicker {
.navigationBarHidden(true) WithPerceptionTracking{
.photosPicker( ImagePickerWithPreviewView(store: pickerStore) { images in
isPresented: $showPhotoPicker, if let image = images.first, let data = image.jpegData(compressionQuality: 0.8) {
selection: $selectedPhotoItem, viewStore.send(.avatarSelected(data))
matching: .images, }
photoLibrary: .shared() showImagePicker = false
)
.onChange(of: selectedPhotoItem) { item in
handlePhotoSelection(item: item, viewStore: viewStore)
selectedPhotoItem = nil
}
.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")!)
} }
.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: - // MARK: -
private func mainContent(viewStore: ViewStoreOf<AppSettingFeature>) -> some View { private func mainContent(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
ZStack { ZStack {
@@ -56,98 +63,38 @@ struct AppSettingView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
topBar topBar
ScrollView { ScrollView {
VStack(spacing: 32) { WithPerceptionTracking {
// VStack(spacing: 32) {
avatarSection(viewStore: viewStore) //
avatarSection(viewStore: viewStore)
// //
nicknameSection(viewStore: viewStore) nicknameSection(viewStore: viewStore)
//
// settingsSection(viewStore: viewStore)
settingsSection(viewStore: viewStore) // 退
logoutButton(viewStore: viewStore)
// 退 }
logoutButton(viewStore: viewStore)
} }
} }
} }
} }
} }
// MARK: -
private func handlePhotoSelection(item: PhotosPickerItem?, viewStore: ViewStoreOf<AppSettingFeature>) {
if let item = item {
loadAndProcessImage(item: item) { data in
if let data = data {
viewStore.send(.avatarSelected(data))
}
}
}
}
// 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: - // MARK: -
private func avatarSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View { private func avatarSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
ZStack(alignment: .bottomTrailing) { ZStack(alignment: .bottomTrailing) {
avatarImageView(viewStore: viewStore) avatarImageView(viewStore: viewStore)
.onTapGesture { .onTapGesture {
showPhotoPicker = true showImagePicker = true
} }
cameraButton cameraButton
.onTapGesture { .onTapGesture {
showPhotoPicker = true showImagePicker = true
} }
} }
.padding(.top, 24) .padding(.top, 24)
} }
// MARK: - // MARK: -
@ViewBuilder @ViewBuilder
private func avatarImageView(viewStore: ViewStoreOf<AppSettingFeature>) -> some View { private func avatarImageView(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
@@ -159,7 +106,7 @@ struct AppSettingView: View {
defaultAvatarView defaultAvatarView
} }
} }
// MARK: - // MARK: -
private var loadingAvatarView: some View { private var loadingAvatarView: some View {
Circle() Circle()
@@ -171,7 +118,7 @@ struct AppSettingView: View {
.scaleEffect(1.2) .scaleEffect(1.2)
) )
} }
// MARK: - // MARK: -
private func networkAvatarView(url: URL) -> some View { private func networkAvatarView(url: URL) -> some View {
CachedAsyncImage(url: url.absoluteString) { image in CachedAsyncImage(url: url.absoluteString) { image in
@@ -184,7 +131,7 @@ struct AppSettingView: View {
.frame(width: 120, height: 120) .frame(width: 120, height: 120)
.clipShape(Circle()) .clipShape(Circle())
} }
// MARK: - // MARK: -
private var defaultAvatarView: some View { private var defaultAvatarView: some View {
Circle() Circle()
@@ -196,7 +143,7 @@ struct AppSettingView: View {
.foregroundColor(.white) .foregroundColor(.white)
) )
} }
// MARK: - // MARK: -
private var cameraButton: some View { private var cameraButton: some View {
Button(action: {}) { Button(action: {}) {
@@ -208,7 +155,7 @@ struct AppSettingView: View {
} }
.offset(x: 8, y: 8) .offset(x: 8, y: 8)
} }
// MARK: - // MARK: -
private func nicknameSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View { private func nicknameSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
VStack(spacing: 0) { VStack(spacing: 0) {
@@ -227,12 +174,12 @@ struct AppSettingView: View {
nicknameInput = viewStore.nickname nicknameInput = viewStore.nickname
showNicknameAlert = true showNicknameAlert = true
} }
Divider().background(Color.gray.opacity(0.3)) Divider().background(Color.gray.opacity(0.3))
.padding(.horizontal, 32) .padding(.horizontal, 32)
} }
} }
// MARK: - // MARK: -
private func settingsSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View { private func settingsSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
VStack(spacing: 0) { VStack(spacing: 0) {
@@ -245,7 +192,7 @@ struct AppSettingView: View {
.background(Color.clear) .background(Color.clear)
.padding(.horizontal, 0) .padding(.horizontal, 0)
} }
// MARK: - // MARK: -
private func personalInfoPermissionsRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View { private func personalInfoPermissionsRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
settingRow( settingRow(
@@ -253,7 +200,7 @@ struct AppSettingView: View {
action: { viewStore.send(.personalInfoPermissionsTapped) } action: { viewStore.send(.personalInfoPermissionsTapped) }
) )
} }
// MARK: - // MARK: -
private func helpRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View { private func helpRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
settingRow( settingRow(
@@ -261,7 +208,7 @@ struct AppSettingView: View {
action: { viewStore.send(.helpTapped) } action: { viewStore.send(.helpTapped) }
) )
} }
// MARK: - // MARK: -
private func clearCacheRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View { private func clearCacheRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
settingRow( settingRow(
@@ -269,7 +216,7 @@ struct AppSettingView: View {
action: { viewStore.send(.clearCacheTapped) } action: { viewStore.send(.clearCacheTapped) }
) )
} }
// MARK: - // MARK: -
private func checkUpdatesRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View { private func checkUpdatesRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
settingRow( settingRow(
@@ -277,7 +224,7 @@ struct AppSettingView: View {
action: { viewStore.send(.checkUpdatesTapped) } action: { viewStore.send(.checkUpdatesTapped) }
) )
} }
// MARK: - // MARK: -
private func aboutUsRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View { private func aboutUsRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
settingRow( settingRow(
@@ -285,7 +232,7 @@ struct AppSettingView: View {
action: { viewStore.send(.aboutUsTapped) } action: { viewStore.send(.aboutUsTapped) }
) )
} }
// MARK: - // MARK: -
private func settingRow(title: String, action: @escaping () -> Void) -> some View { private func settingRow(title: String, action: @escaping () -> Void) -> some View {
VStack(spacing: 0) { VStack(spacing: 0) {
@@ -301,12 +248,12 @@ struct AppSettingView: View {
.onTapGesture { .onTapGesture {
action() action()
} }
Divider().background(Color.gray.opacity(0.3)) Divider().background(Color.gray.opacity(0.3))
.padding(.horizontal, 32) .padding(.horizontal, 32)
} }
} }
// MARK: - 退 // MARK: - 退
private func logoutButton(viewStore: ViewStoreOf<AppSettingFeature>) -> some View { private func logoutButton(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
Button(action: { Button(action: {
@@ -323,7 +270,7 @@ struct AppSettingView: View {
} }
.padding(.bottom, 32) .padding(.bottom, 32)
} }
// MARK: - // MARK: -
private func userAgreementBinding(viewStore: ViewStoreOf<AppSettingFeature>) -> Binding<Bool> { private func userAgreementBinding(viewStore: ViewStoreOf<AppSettingFeature>) -> Binding<Bool> {
viewStore.binding( viewStore.binding(
@@ -331,7 +278,7 @@ struct AppSettingView: View {
send: AppSettingFeature.Action.userAgreementDismissed send: AppSettingFeature.Action.userAgreementDismissed
) )
} }
// MARK: - // MARK: -
private func privacyPolicyBinding(viewStore: ViewStoreOf<AppSettingFeature>) -> Binding<Bool> { private func privacyPolicyBinding(viewStore: ViewStoreOf<AppSettingFeature>) -> Binding<Bool> {
viewStore.binding( viewStore.binding(
@@ -339,7 +286,55 @@ struct AppSettingView: View {
send: AppSettingFeature.Action.privacyPolicyDismissed 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: - // MARK: -
private func loadAndProcessImage(item: PhotosPickerItem, completion: @escaping (Data?) -> Void) { private func loadAndProcessImage(item: PhotosPickerItem, completion: @escaping (Data?) -> Void) {
item.loadTransferable(type: Data.self) { result in item.loadTransferable(type: Data.self) { result in