feat: 更新AppSettingFeature以增强用户设置功能

- 重构AppSettingFeature,采用@Reducer和@ObservableState以优化状态管理。
- 新增用户信息加载逻辑,支持从服务器获取用户信息并更新界面。
- 更新AppSettingView,整合头像、昵称及其他设置项的展示,提升用户体验。
- 增加多语言支持,更新Localizable.strings文件以适应新的设置项。
This commit is contained in:
edwinQQQ
2025-07-24 11:24:04 +08:00
parent f30026821a
commit 71c40e465d
5 changed files with 353 additions and 88 deletions

View File

@@ -9,9 +9,9 @@ alwaysApply: true
## OBJECTIVE ## OBJECTIVE
As an expert AI programming assistant, your task is to provide me with clear and readable SwiftUI code. You should: As an expert AI programming assistant, your task is to provide me with clear, readable, and effective code. You should:
- Utilize the latest versions of SwiftUI and Swift, being familiar with the newest features and best practices. - Utilize the latest versions of SwiftUI, Swift(6) and TCA(1.20.2), being familiar with the newest features and best practices.
- Provide careful and accurate answers that are well-founded and thoughtfully considered. - Provide careful and accurate answers that are well-founded and thoughtfully considered.
- **Explicitly use the Chain-of-Thought (CoT) method in your reasoning and answers, explaining your thought process step by step.** - **Explicitly use the Chain-of-Thought (CoT) method in your reasoning and answers, explaining your thought process step by step.**
- Strictly adhere to my requirements and meticulously complete the tasks. - Strictly adhere to my requirements and meticulously complete the tasks.
@@ -24,10 +24,6 @@ alwaysApply: true
- Emphasize code readability over performance optimization. - Emphasize code readability over performance optimization.
- Maintain a professional and supportive tone, ensuring clarity of content. - Maintain a professional and supportive tone, ensuring clarity of content.
## AUDIENCE
The target audience is me, a native Chinese developer eager to learn Swift 6 and Xcode 15.9, seeking guidance and advice on utilizing the latest technologies.
## RESPONSE FORMAT ## RESPONSE FORMAT
- **Utilize the Chain-of-Thought (CoT) method to reason and respond, explaining your thought process step by step.** - **Utilize the Chain-of-Thought (CoT) method to reason and respond, explaining your thought process step by step.**

View File

@@ -1,27 +1,114 @@
import Foundation import Foundation
import ComposableArchitecture import ComposableArchitecture
struct AppSettingFeature: Reducer { @Reducer
struct AppSettingFeature {
@ObservableState
struct State: Equatable { struct State: Equatable {
var nickname: String = "hahahaha" var nickname: String = ""
var avatarURL: String? = nil var avatarURL: String? = nil
var userInfo: UserInfo? = nil
var isLoadingUserInfo: Bool = false
var userInfoError: String? = nil
// WebView
var showUserAgreement: Bool = false
var showPrivacyPolicy: Bool = false
} }
enum Action: Equatable { enum Action: Equatable {
case onAppear case onAppear
case editNicknameTapped case editNicknameTapped
case logoutTapped case logoutTapped
// action
//
case loadUserInfo
case userInfoLoaded(UserInfo?)
case userInfoLoadFailed(String)
// WebView
case personalInfoPermissionsTapped
case helpTapped
case clearCacheTapped
case checkUpdatesTapped
case aboutUsTapped
// WebView
case userAgreementDismissed
case privacyPolicyDismissed
} }
func reduce(into state: inout State, action: Action) -> Effect<Action> {
@Dependency(\.apiService) var apiService
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action { switch action {
case .onAppear: case .onAppear:
return .none return .send(.loadUserInfo)
case .editNicknameTapped: case .editNicknameTapped:
// //
return .none return .none
case .logoutTapped: case .logoutTapped:
// //
return .none return .none
case .loadUserInfo:
state.isLoadingUserInfo = true
state.userInfoError = nil
return .run { send in
let currentUid = await UserInfoManager.getCurrentUserId()
if let userInfo = await UserInfoManager.fetchUserInfoFromServer(
uid: currentUid,
apiService: apiService
) {
await send(.userInfoLoaded(userInfo))
} else {
await send(.userInfoLoadFailed("获取用户信息失败"))
}
}
case let .userInfoLoaded(userInfo):
state.isLoadingUserInfo = false
state.userInfo = userInfo
state.nickname = userInfo?.nick ?? "hahahaha"
state.avatarURL = userInfo?.avatar
return .none
case let .userInfoLoadFailed(error):
state.isLoadingUserInfo = false
state.userInfoError = error
return .none
case .personalInfoPermissionsTapped:
state.showPrivacyPolicy = true
return .none
case .helpTapped:
state.showUserAgreement = true
return .none
case .clearCacheTapped:
//
return .none
case .checkUpdatesTapped:
//
return .none
case .aboutUsTapped:
//
return .none
case .userAgreementDismissed:
state.showUserAgreement = false
return .none
case .privacyPolicyDismissed:
state.showPrivacyPolicy = false
return .none
}
} }
} }
} }

View File

@@ -119,3 +119,14 @@
"setting.about" = "About Us"; "setting.about" = "About Us";
"setting.version" = "Version Info"; "setting.version" = "Version Info";
"setting.logout" = "Logout"; "setting.logout" = "Logout";
// MARK: - App Setting
"appSetting.title" = "Edit";
"appSetting.nickname" = "Nickname";
"appSetting.personalInfoPermissions" = "Personal Information and Permissions";
"appSetting.help" = "Help";
"appSetting.clearCache" = "Clear Cache";
"appSetting.checkUpdates" = "Check for Updates";
"appSetting.logout" = "Log Out";
"appSetting.aboutUs" = "About Us";
"appSetting.logoutAccount" = "Log out of account";

View File

@@ -115,3 +115,14 @@
"feed.2hoursago" = "2小时前"; "feed.2hoursago" = "2小时前";
"feed.demoContent" = "今天是美好的一天,分享一些生活中的小确幸。希望大家都能珍惜每一个当下的时刻。"; "feed.demoContent" = "今天是美好的一天,分享一些生活中的小确幸。希望大家都能珍惜每一个当下的时刻。";
"feed.vip" = "VIP%d"; "feed.vip" = "VIP%d";
// MARK: - App Setting
"appSetting.title" = "编辑";
"appSetting.nickname" = "昵称";
"appSetting.personalInfoPermissions" = "个人信息与权限";
"appSetting.help" = "帮助";
"appSetting.clearCache" = "清除缓存";
"appSetting.checkUpdates" = "检查更新";
"appSetting.logout" = "退出登录";
"appSetting.aboutUs" = "关于我们";
"appSetting.logoutAccount" = "退出账户";

View File

@@ -8,20 +8,114 @@ struct AppSettingView: View {
WithViewStore(self.store, observe: { $0 }) { viewStore in WithViewStore(self.store, observe: { $0 }) { viewStore in
ZStack { ZStack {
Color(red: 22/255, green: 17/255, blue: 44/255).ignoresSafeArea() Color(red: 22/255, green: 17/255, blue: 44/255).ignoresSafeArea()
// VStack(spacing: 0) {
//
VStack(spacing: 0) { VStack(spacing: 0) {
Spacer().frame(height: 24) //
// avatarSection(viewStore: viewStore)
Text("Edit")
.font(.system(size: 22, weight: .semibold)) //
nicknameSection(viewStore: viewStore)
//
settingsSection(viewStore: viewStore)
Spacer()
//
logoutButton(viewStore: viewStore)
}
// }
}
.navigationTitle(NSLocalizedString("appSetting.title", comment: "Edit"))
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.hidden, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {}) {
Image(systemName: "chevron.left")
.font(.system(size: 24, weight: .medium))
.foregroundColor(.white) .foregroundColor(.white)
.padding(.top, 8) }
// }
}
.onAppear {
viewStore.send(.onAppear)
}
.webView(
isPresented: userAgreementBinding(viewStore: viewStore),
url: APIConfiguration.webURL(for: .userAgreement)
)
.webView(
isPresented: privacyPolicyBinding(viewStore: viewStore),
url: APIConfiguration.webURL(for: .privacyPolicy)
)
}
}
// MARK: -
private func avatarSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
ZStack(alignment: .bottomTrailing) { ZStack(alignment: .bottomTrailing) {
Image("avatar_placeholder") avatarImageView(viewStore: viewStore)
cameraButton
}
.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() .resizable()
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
} placeholder: {
defaultAvatarView
}
.frame(width: 120, height: 120) .frame(width: 120, height: 120)
.clipShape(Circle()) .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: {}) { Button(action: {}) {
ZStack { ZStack {
Circle().fill(Color.purple).frame(width: 36, height: 36) Circle().fill(Color.purple).frame(width: 36, height: 36)
@@ -31,10 +125,12 @@ struct AppSettingView: View {
} }
.offset(x: 8, y: 8) .offset(x: 8, y: 8)
} }
.padding(.top, 24)
// // MARK: -
private func nicknameSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
VStack(spacing: 0) {
HStack { HStack {
Text("Nickname") Text(NSLocalizedString("appSetting.nickname", comment: "Nickname"))
.foregroundColor(.white) .foregroundColor(.white)
Spacer() Spacer()
Text(viewStore.nickname) Text(viewStore.nickname)
@@ -47,25 +143,92 @@ struct AppSettingView: View {
.onTapGesture { .onTapGesture {
viewStore.send(.editNicknameTapped) viewStore.send(.editNicknameTapped)
} }
Divider().background(Color.gray.opacity(0.3)) Divider().background(Color.gray.opacity(0.3))
.padding(.horizontal, 32) .padding(.horizontal, 32)
// }
}
// MARK: -
private func settingsSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
VStack(spacing: 0) { VStack(spacing: 0) {
settingRow("Personal Information and Permissions") personalInfoPermissionsRow(viewStore: viewStore)
settingRow("Help") helpRow(viewStore: viewStore)
settingRow("Clear Cache") clearCacheRow(viewStore: viewStore)
settingRow("Check for Updates") checkUpdatesRow(viewStore: viewStore)
settingRow("Log Out") aboutUsRow(viewStore: viewStore)
settingRow("About Us")
} }
.background(Color.clear) .background(Color.clear)
.padding(.horizontal, 0) .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() 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: { Button(action: {
viewStore.send(.logoutTapped) viewStore.send(.logoutTapped)
}) { }) {
Text("Log out of account") Text(NSLocalizedString("appSetting.logoutAccount", comment: "Log out of account"))
.font(.system(size: 18, weight: .semibold)) .font(.system(size: 18, weight: .semibold))
.foregroundColor(.white) .foregroundColor(.white)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@@ -76,23 +239,20 @@ struct AppSettingView: View {
} }
.padding(.bottom, 32) .padding(.bottom, 32)
} }
}
} // MARK: -
} private func userAgreementBinding(viewStore: ViewStoreOf<AppSettingFeature>) -> Binding<Bool> {
// viewStore.binding(
func settingRow(_ title: String) -> some View { get: \.showUserAgreement,
return VStack(spacing: 0) { send: AppSettingFeature.Action.userAgreementDismissed
HStack { )
Text(title) }
.foregroundColor(.white)
Spacer() // MARK: -
Image(systemName: "chevron.right") private func privacyPolicyBinding(viewStore: ViewStoreOf<AppSettingFeature>) -> Binding<Bool> {
.foregroundColor(.gray) viewStore.binding(
} get: \.showPrivacyPolicy,
.padding(.horizontal, 32) send: AppSettingFeature.Action.privacyPolicyDismissed
.padding(.vertical, 18) )
Divider().background(Color.gray.opacity(0.3))
.padding(.horizontal, 32)
}
} }
} }