feat: 增强应用功能与用户体验
- 在Package.swift中更新依赖路径,确保项目结构清晰。 - 在AppSettingFeature中新增初始化方法,支持用户信息、头像和昵称的设置。 - 更新FeedListFeature和MainFeature,新增测试按钮和导航功能,提升用户交互体验。 - 在MeFeature中优化用户信息加载逻辑,增强错误处理能力。 - 新增TestView以支持测试功能,验证导航跳转的有效性。 - 更新多个视图以整合新功能,提升代码可读性与维护性。
This commit is contained in:
@@ -23,11 +23,13 @@ let package = Package(
|
||||
name: "yana",
|
||||
dependencies: [
|
||||
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
|
||||
]
|
||||
],
|
||||
path: "yana",
|
||||
),
|
||||
.testTarget(
|
||||
name: "yanaTests",
|
||||
dependencies: ["yana"]
|
||||
dependencies: ["yana"],
|
||||
path: "yanaAPITests",
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
@@ -515,7 +515,7 @@
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "yana/yana-Bridging-Header.h";
|
||||
SWIFT_STRICT_CONCURRENCY = complete;
|
||||
SWIFT_STRICT_CONCURRENCY = minimal;
|
||||
SWIFT_VERSION = 6.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
@@ -573,7 +573,7 @@
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "yana/yana-Bridging-Header.h";
|
||||
SWIFT_STRICT_CONCURRENCY = complete;
|
||||
SWIFT_STRICT_CONCURRENCY = minimal;
|
||||
SWIFT_VERSION = 6.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
@@ -598,7 +598,7 @@
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.9;
|
||||
SWIFT_VERSION = 6.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/yana.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/yana";
|
||||
};
|
||||
@@ -621,7 +621,7 @@
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.9;
|
||||
SWIFT_VERSION = 6.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/yana.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/yana";
|
||||
};
|
||||
|
@@ -33,8 +33,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-collections",
|
||||
"state" : {
|
||||
"revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0",
|
||||
"version" : "1.2.0"
|
||||
"revision" : "8c0c0a8b49e080e54e5e328cc552821ff07cd341",
|
||||
"version" : "1.2.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@@ -22,6 +22,13 @@ struct AppSettingFeature {
|
||||
var nicknameInput: String = ""
|
||||
var isUpdatingUser: Bool = false
|
||||
var updateUserError: String? = nil
|
||||
|
||||
// 新增:带userInfo、avatarURL、nickname的init
|
||||
init(nickname: String = "", avatarURL: String? = nil, userInfo: UserInfo? = nil) {
|
||||
self.nickname = nickname
|
||||
self.avatarURL = avatarURL
|
||||
self.userInfo = userInfo
|
||||
}
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
@@ -52,6 +59,7 @@ struct AppSettingFeature {
|
||||
case updateUser(Result<UpdateUserResponse, APIError>)
|
||||
case nicknameInputChanged(String)
|
||||
case nicknameEditAlert(Bool)
|
||||
case testPushTapped
|
||||
}
|
||||
|
||||
@Dependency(\.apiService) var apiService
|
||||
@@ -77,27 +85,27 @@ struct AppSettingFeature {
|
||||
state.isLoadingUserInfo = true
|
||||
state.userInfoError = nil
|
||||
return .run { send in
|
||||
do {
|
||||
// do {
|
||||
if let userInfo = await UserInfoManager.getUserInfo() {
|
||||
await send(.userInfoResponse(.success(userInfo)))
|
||||
} else {
|
||||
await send(.userInfoResponse(.failure(APIError.custom("用户信息不存在"))))
|
||||
}
|
||||
} catch {
|
||||
let apiError: APIError
|
||||
if let error = error as? APIError {
|
||||
apiError = error
|
||||
} else {
|
||||
apiError = APIError.custom(error.localizedDescription)
|
||||
}
|
||||
await send(.userInfoResponse(.failure(apiError)))
|
||||
}
|
||||
// } catch {
|
||||
// let apiError: APIError
|
||||
// if let error = error as? APIError {
|
||||
// apiError = error
|
||||
// } else {
|
||||
// apiError = APIError.custom(error.localizedDescription)
|
||||
// }
|
||||
// await send(.userInfoResponse(.failure(apiError)))
|
||||
// }
|
||||
}
|
||||
|
||||
case let .userInfoResponse(.success(userInfo)):
|
||||
state.userInfo = userInfo
|
||||
state.nickname = userInfo.nick ?? ""
|
||||
state.avatarURL = userInfo.avatar
|
||||
state.avatarURL = userInfo.avatar // 确保同步
|
||||
state.isLoadingUserInfo = false
|
||||
return .none
|
||||
|
||||
@@ -158,6 +166,8 @@ struct AppSettingFeature {
|
||||
state.isUpdatingUser = true
|
||||
state.updateUserError = nil
|
||||
guard let userInfo = state.userInfo else { return .none }
|
||||
// 头像上传后,先临时更新本地avatarURL,提升UI响应
|
||||
state.avatarURL = url
|
||||
return .run { send in
|
||||
let ticket = await UserInfoManager.getCurrentUserTicket() ?? ""
|
||||
let req = UpdateUserRequest(avatar: url, nick: nil, uid: userInfo.uid ?? 0, ticket: ticket)
|
||||
@@ -195,7 +205,7 @@ struct AppSettingFeature {
|
||||
await send(.updateUser(.failure(apiError)))
|
||||
}
|
||||
}
|
||||
case let .updateUser(.success(resp)):
|
||||
case .updateUser(.success(_)):
|
||||
state.isUpdatingUser = false
|
||||
// 不直接用 resp.data,触发拉取完整 userinfo,延迟1秒
|
||||
if let uid = state.userInfo?.uid {
|
||||
@@ -214,6 +224,8 @@ struct AppSettingFeature {
|
||||
state.isUpdatingUser = false
|
||||
state.updateUserError = error.localizedDescription
|
||||
return .none
|
||||
case .testPushTapped:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -27,6 +27,7 @@ struct FeedListFeature {
|
||||
case loadMoreResponse(TaskResult<MomentsLatestResponse>)
|
||||
case editFeedButtonTapped // 新增:点击 add 按钮
|
||||
case editFeedDismissed // 新增:关闭编辑页
|
||||
case testButtonTapped // 新增:点击测试按钮
|
||||
// 新增:动态内容相关
|
||||
case fetchFeeds
|
||||
case fetchFeedsResponse(TaskResult<MomentsLatestResponse>)
|
||||
@@ -125,6 +126,9 @@ struct FeedListFeature {
|
||||
case .editFeedDismissed:
|
||||
state.isEditFeedPresented = false
|
||||
return .none
|
||||
case .testButtonTapped:
|
||||
debugInfoSync("[LOG] FeedListFeature testButtonTapped")
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -22,9 +22,9 @@ struct MainFeature {
|
||||
}
|
||||
|
||||
// 新增:导航目标
|
||||
enum Destination: Hashable, Equatable {
|
||||
case test
|
||||
enum Destination: Hashable, Codable, CaseIterable {
|
||||
case appSetting
|
||||
case testView
|
||||
}
|
||||
|
||||
@CasePathable
|
||||
@@ -36,7 +36,6 @@ struct MainFeature {
|
||||
case accountModelLoaded(AccountModel?)
|
||||
// 新增:导航相关
|
||||
case navigationPathChanged([Destination])
|
||||
case testButtonTapped
|
||||
case appSettingButtonTapped
|
||||
case appSettingAction(AppSettingFeature.Action)
|
||||
// 新增:登出
|
||||
@@ -51,6 +50,8 @@ struct MainFeature {
|
||||
MeFeature()
|
||||
}
|
||||
Reduce { state, action in
|
||||
debugInfoSync("MainFeature action: \(action)")
|
||||
debugInfoSync("MainFeature state: \(state)")
|
||||
switch action {
|
||||
case .onAppear:
|
||||
return .run { send in
|
||||
@@ -68,36 +69,43 @@ struct MainFeature {
|
||||
return .send(.me(.onAppear))
|
||||
}
|
||||
return .none
|
||||
case .feedList(.testButtonTapped):
|
||||
state.navigationPath.append(.testView)
|
||||
return .none
|
||||
case .feedList:
|
||||
return .none
|
||||
case let .accountModelLoaded(accountModel):
|
||||
state.accountModel = accountModel
|
||||
return .none
|
||||
case .me(.settingButtonTapped):
|
||||
// 触发 push 到设置页
|
||||
state.appSettingState = AppSettingFeature.State()
|
||||
// 触发 push 到设置页,带入当前用户信息
|
||||
let userInfo = state.me.userInfo
|
||||
let avatarURL = userInfo?.avatar
|
||||
let nickname = userInfo?.nick ?? ""
|
||||
state.appSettingState = AppSettingFeature.State(nickname: nickname, avatarURL: avatarURL, userInfo: userInfo)
|
||||
state.navigationPath.append(.appSetting)
|
||||
debugInfoSync("\(state.navigationPath)")
|
||||
return .none
|
||||
case .me:
|
||||
return .none
|
||||
case .navigationPathChanged(let newPath):
|
||||
// pop 回来时清空 settingState
|
||||
if !newPath.contains(.appSetting) {
|
||||
state.appSettingState = nil
|
||||
}
|
||||
state.navigationPath = newPath
|
||||
return .none
|
||||
case .testButtonTapped:
|
||||
state.navigationPath.append(.test)
|
||||
return .none
|
||||
case .appSettingButtonTapped:
|
||||
state.appSettingState = AppSettingFeature.State()
|
||||
let userInfo = state.me.userInfo
|
||||
let avatarURL = userInfo?.avatar
|
||||
let nickname = userInfo?.nick ?? ""
|
||||
state.appSettingState = AppSettingFeature.State(nickname: nickname, avatarURL: avatarURL, userInfo: userInfo)
|
||||
state.navigationPath.append(.appSetting)
|
||||
return .none
|
||||
case .appSettingAction(.logoutTapped):
|
||||
// 监听到登出,设置登出标志
|
||||
state.isLoggedOut = true
|
||||
return .none
|
||||
case .appSettingAction(.updateUser(.success)):
|
||||
// 设置页用户信息变更成功,刷新Me页数据
|
||||
return .send(.me(.refresh))
|
||||
case .appSettingAction:
|
||||
return .none
|
||||
case .logout:
|
||||
@@ -110,4 +118,4 @@ struct MainFeature {
|
||||
AppSettingFeature()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -87,15 +87,15 @@ struct MeFeature {
|
||||
|
||||
private func fetchUserInfo(uid: Int) -> Effect<Action> {
|
||||
.run { send in
|
||||
do {
|
||||
if let userInfo = try await UserInfoManager.fetchUserInfoFromServer(uid: String(uid), apiService: apiService) {
|
||||
// do {
|
||||
if let userInfo = await UserInfoManager.fetchUserInfoFromServer(uid: String(uid), apiService: apiService) {
|
||||
await send(.userInfoResponse(.success(userInfo)))
|
||||
} else {
|
||||
await send(.userInfoResponse(.failure(.noData)))
|
||||
}
|
||||
} catch {
|
||||
await send(.userInfoResponse(.failure(error as? APIError ?? .unknown(error.localizedDescription))))
|
||||
}
|
||||
// } catch {
|
||||
// await send(.userInfoResponse(.failure(error as? APIError ?? .unknown(error.localizedDescription))))
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,4 +110,4 @@ struct MeFeature {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -27,7 +27,7 @@ struct AppRootView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AppRootView()
|
||||
}
|
||||
//
|
||||
//#Preview {
|
||||
// AppRootView()
|
||||
//}
|
||||
|
@@ -1,3 +1,10 @@
|
||||
//
|
||||
// AppSettingView.swift
|
||||
// yana
|
||||
//
|
||||
// Created by Edwin on 2024/11/20.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
import PhotosUI
|
||||
@@ -5,105 +12,133 @@ import PhotosUI
|
||||
struct AppSettingView: View {
|
||||
let store: StoreOf<AppSettingFeature>
|
||||
@State private var showPhotoPicker = false
|
||||
@State private var selectedPhotoItem: PhotosPickerItem? = nil
|
||||
@State private var showNicknameAlert = false
|
||||
@State private var nicknameInput: String = ""
|
||||
@State private var nicknameInput = ""
|
||||
@State private var selectedPhotoItem: PhotosPickerItem?
|
||||
|
||||
var body: some View {
|
||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||
ZStack {
|
||||
Color(red: 22/255, green: 17/255, blue: 44/255).ignoresSafeArea()
|
||||
VStack(spacing: 0) {
|
||||
// 主要内容
|
||||
VStack(spacing: 0) {
|
||||
// WithPerceptionTracking {
|
||||
WithViewStore(store, observe: { $0 }) { viewStore in
|
||||
WithPerceptionTracking{
|
||||
mainContent(viewStore: viewStore)
|
||||
.photosPicker(
|
||||
isPresented: $showPhotoPicker,
|
||||
selection: $selectedPhotoItem,
|
||||
matching: .images,
|
||||
photoLibrary: .shared()
|
||||
)
|
||||
.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")!)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
// MARK: - 主要内容
|
||||
private func mainContent(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||||
ZStack {
|
||||
Color(red: 22/255, green: 17/255, blue: 44/255).ignoresSafeArea()
|
||||
VStack(spacing: 0) {
|
||||
// 调试信息
|
||||
// Text("nickname: \(viewStore.nickname)\navatarURL: \(String(describing: viewStore.avatarURL))\nuserInfo: \(String(describing: viewStore.userInfo))\nuserInfoError: \(String(describing: viewStore.userInfoError))")
|
||||
// .foregroundColor(.yellow)
|
||||
// .font(.system(size: 12))
|
||||
// .padding(8)
|
||||
// 顶部栏
|
||||
topBar
|
||||
// 测试跳转按钮
|
||||
Button("测试跳转TestPushView") {
|
||||
viewStore.send(.testPushTapped)
|
||||
}
|
||||
.padding()
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: 32) {
|
||||
// 头像区域
|
||||
avatarSection(viewStore: viewStore)
|
||||
|
||||
// 昵称设置项
|
||||
nicknameSection(viewStore: viewStore)
|
||||
// 其他设置项
|
||||
|
||||
// 设置项区域
|
||||
settingsSection(viewStore: viewStore)
|
||||
Spacer()
|
||||
// 底部大按钮
|
||||
|
||||
// 退出登录按钮
|
||||
logoutButton(viewStore: viewStore)
|
||||
}
|
||||
}
|
||||
// Loading & 错误提示
|
||||
if viewStore.isUploadingAvatar || viewStore.isUpdatingUser {
|
||||
Color.black.opacity(0.3).ignoresSafeArea()
|
||||
ProgressView("正在上传/更新...")
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.foregroundColor(.white)
|
||||
.scaleEffect(1.2)
|
||||
}
|
||||
if let error = viewStore.avatarUploadError ?? viewStore.updateUserError {
|
||||
VStack {
|
||||
Spacer()
|
||||
Text(error)
|
||||
.foregroundColor(.red)
|
||||
.padding()
|
||||
.background(Color.white.opacity(0.9))
|
||||
.cornerRadius(10)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewStore.send(.onAppear)
|
||||
}
|
||||
.webView(
|
||||
isPresented: userAgreementBinding(viewStore: viewStore),
|
||||
url: APIConfiguration.webURL(for: .userAgreement)
|
||||
)
|
||||
.webView(
|
||||
isPresented: privacyPolicyBinding(viewStore: viewStore),
|
||||
url: APIConfiguration.webURL(for: .privacyPolicy)
|
||||
)
|
||||
// 头像选择器
|
||||
.photosPicker(isPresented: $showPhotoPicker, selection: $selectedPhotoItem, matching: .images, photoLibrary: .shared())
|
||||
.onChange(of: selectedPhotoItem) { newItem in
|
||||
if let item = newItem {
|
||||
loadAndProcessImage(item: item) { data in
|
||||
if let data = data {
|
||||
viewStore.send(.avatarSelected(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 昵称修改Alert
|
||||
.alert("修改昵称", isPresented: $showNicknameAlert, actions: {
|
||||
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) {}
|
||||
}, message: {
|
||||
Text("昵称最长15个字符")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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 {
|
||||
Button(action: {}) {
|
||||
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 avatarSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||||
@@ -341,4 +376,4 @@ private func resizeImage(image: UIImage, targetSize: CGSize) -> UIImage {
|
||||
return renderer.image { _ in
|
||||
image.draw(in: CGRect(origin: .zero, size: targetSize))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,8 @@ struct FeedListView: View {
|
||||
|
||||
var body: some View {
|
||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||
GeometryReader { geometry in
|
||||
WithPerceptionTracking {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// 背景图片
|
||||
Image("bg")
|
||||
@@ -19,6 +20,17 @@ struct FeedListView: View {
|
||||
VStack(alignment: .center, spacing: 0) {
|
||||
// 顶部栏
|
||||
HStack {
|
||||
Button(action: {
|
||||
viewStore.send(.testButtonTapped)
|
||||
}) {
|
||||
Text("测试")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.blue.opacity(0.7))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
Spacer(minLength: 0)
|
||||
Text(NSLocalizedString("feedList.title", comment: "Enjoy your Life Time"))
|
||||
@@ -66,28 +78,30 @@ struct FeedListView: View {
|
||||
.padding(.top, 20)
|
||||
} else {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 16) {
|
||||
ForEach(Array(viewStore.moments.enumerated()), id: \ .element.dynamicId) { index, moment in
|
||||
OptimizedDynamicCardView(moment: moment, allMoments: viewStore.moments, currentIndex: index)
|
||||
// 上拉加载更多触发点
|
||||
if index == viewStore.moments.count - 1, viewStore.hasMore, !viewStore.isLoadingMore {
|
||||
Color.clear
|
||||
.frame(height: 1)
|
||||
.onAppear {
|
||||
viewStore.send(.loadMore)
|
||||
}
|
||||
WithPerceptionTracking {
|
||||
LazyVStack(spacing: 16) {
|
||||
ForEach(Array(viewStore.moments.enumerated()), id: \ .element.dynamicId) { index, moment in
|
||||
OptimizedDynamicCardView(moment: moment, allMoments: viewStore.moments, currentIndex: index)
|
||||
// 上拉加载更多触发点
|
||||
if index == viewStore.moments.count - 1, viewStore.hasMore, !viewStore.isLoadingMore {
|
||||
Color.clear
|
||||
.frame(height: 1)
|
||||
.onAppear {
|
||||
viewStore.send(.loadMore)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 加载更多指示器
|
||||
if viewStore.isLoadingMore {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
// 加载更多指示器
|
||||
if viewStore.isLoadingMore {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
.refreshable {
|
||||
viewStore.send(.reload)
|
||||
@@ -119,6 +133,7 @@ struct FeedListView: View {
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -95,7 +95,7 @@ struct LanguageSettingsView: View {
|
||||
}
|
||||
|
||||
private func testCOToken() async {
|
||||
do {
|
||||
// do {
|
||||
let token = await cosManager.getToken(apiService: apiService)
|
||||
if let token = token {
|
||||
print("✅ Token 测试成功")
|
||||
@@ -105,9 +105,9 @@ struct LanguageSettingsView: View {
|
||||
} else {
|
||||
print("❌ Token 测试失败: 未能获取 Token")
|
||||
}
|
||||
} catch {
|
||||
print("❌ Token 测试异常: \(error.localizedDescription)")
|
||||
}
|
||||
// } catch {
|
||||
// print("❌ Token 测试异常: \(error.localizedDescription)")
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -7,91 +7,102 @@ struct MainView: View {
|
||||
|
||||
var body: some View {
|
||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||
InternalMainView(store: store)
|
||||
.onChange(of: viewStore.isLoggedOut) { isLoggedOut in
|
||||
if isLoggedOut {
|
||||
onLogout?()
|
||||
WithPerceptionTracking {
|
||||
InternalMainView(store: store)
|
||||
.onChange(of: viewStore.isLoggedOut) { isLoggedOut in
|
||||
if isLoggedOut {
|
||||
onLogout?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 原 MainView 内容,重命名为 InternalMainView
|
||||
struct InternalMainView: View {
|
||||
let store: StoreOf<MainFeature>
|
||||
@State private var path: [MainFeature.Destination] = []
|
||||
init(store: StoreOf<MainFeature>) {
|
||||
self.store = store
|
||||
_path = State(initialValue: store.withState { $0.navigationPath })
|
||||
}
|
||||
var body: some View {
|
||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||
NavigationStack(path: viewStore.binding(get: \.navigationPath, send: MainFeature.Action.navigationPathChanged)) {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// 背景图片
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.clipped()
|
||||
.ignoresSafeArea(.all)
|
||||
// 主内容
|
||||
ZStack {
|
||||
FeedListView(store: store.scope(
|
||||
state: \.feedList,
|
||||
action: \.feedList
|
||||
))
|
||||
.isHidden(viewStore.selectedTab != .feed)
|
||||
MeView(
|
||||
store: store.scope(
|
||||
state: \.me,
|
||||
action: \.me
|
||||
)
|
||||
)
|
||||
.isHidden(viewStore.selectedTab != .other)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
// 底部导航栏
|
||||
VStack {
|
||||
Spacer()
|
||||
BottomTabView(selectedTab: viewStore.binding(
|
||||
get: { Tab(rawValue: $0.selectedTab.rawValue) ?? .feed },
|
||||
send: { MainFeature.Action.selectTab(MainFeature.Tab(rawValue: $0.rawValue) ?? .feed) }
|
||||
))
|
||||
}
|
||||
.padding(.bottom, geometry.safeAreaInsets.bottom + 60)
|
||||
}
|
||||
}
|
||||
.navigationDestination(for: MainFeature.Destination.self) { destination in
|
||||
switch destination {
|
||||
case .test:
|
||||
TestPushView()
|
||||
case .appSetting:
|
||||
IfLetStore(
|
||||
self.store.scope(
|
||||
state: \.appSettingState,
|
||||
action: MainFeature.Action.appSettingAction
|
||||
),
|
||||
then: { appSettingStore in
|
||||
WithPerceptionTracking {
|
||||
AppSettingView(store: appSettingStore)
|
||||
WithPerceptionTracking {
|
||||
NavigationStack(path: $path) {
|
||||
GeometryReader { geometry in
|
||||
contentView(geometry: geometry, viewStore: viewStore)
|
||||
.navigationDestination(for: MainFeature.Destination.self) { destination in
|
||||
debugLogSync("[log] navigationDestination: \(destination)")
|
||||
let view: AnyView
|
||||
switch destination {
|
||||
case .appSetting:
|
||||
if let appSettingStore = store.scope(state: \.appSettingState, action: \.appSettingAction) {
|
||||
view = AnyView(AppSettingView(store: appSettingStore))
|
||||
} else {
|
||||
view = AnyView(Text("appSettingState is nil"))
|
||||
}
|
||||
case .testView:
|
||||
debugLogSync("[log] navigationDestination: .testView 渲染")
|
||||
view = AnyView(TestView())
|
||||
}
|
||||
return view
|
||||
}
|
||||
.onChange(of: path) { newPath in
|
||||
debugLogSync("[log] path changed: \(type(of: path)) = \(path)")
|
||||
viewStore.send(.navigationPathChanged(newPath))
|
||||
}
|
||||
.onChange(of: viewStore.navigationPath) { newPath in
|
||||
debugLogSync("[log] viewStore.navigationPath changed: \(type(of: newPath)) = \(newPath)")
|
||||
if path != newPath {
|
||||
path = newPath
|
||||
}
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
viewStore.send(.onAppear)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewStore.send(.onAppear)
|
||||
}
|
||||
}
|
||||
|
||||
private func contentView(geometry: GeometryProxy, viewStore: ViewStoreOf<MainFeature>) -> some View {
|
||||
WithPerceptionTracking {
|
||||
ZStack {
|
||||
// 背景图片
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.clipped()
|
||||
.ignoresSafeArea(.all)
|
||||
// 主内容
|
||||
ZStack {
|
||||
FeedListView(store: store.scope(
|
||||
state: \.feedList,
|
||||
action: \.feedList
|
||||
))
|
||||
.isHidden(viewStore.selectedTab != .feed)
|
||||
MeView(
|
||||
store: store.scope(
|
||||
state: \.me,
|
||||
action: \.me
|
||||
)
|
||||
)
|
||||
.isHidden(viewStore.selectedTab != .other)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
// 底部导航栏
|
||||
VStack {
|
||||
Spacer()
|
||||
BottomTabView(selectedTab: viewStore.binding(
|
||||
get: { Tab(rawValue: $0.selectedTab.rawValue) ?? .feed },
|
||||
send: { MainFeature.Action.selectTab(MainFeature.Tab(rawValue: $0.rawValue) ?? .feed) }
|
||||
))
|
||||
}
|
||||
.padding(.bottom, geometry.safeAreaInsets.bottom + 60)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TestPushView: View {
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.blue.ignoresSafeArea()
|
||||
Text("Test Push View")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,8 @@ struct MeView: View {
|
||||
let store: StoreOf<MeFeature>
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
WithPerceptionTracking {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
Image("bg")
|
||||
.resizable()
|
||||
@@ -93,19 +94,21 @@ struct MeView: View {
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 12) {
|
||||
ForEach(Array(viewStore.moments.enumerated()), id: \ .element.dynamicId) { index, moment in
|
||||
OptimizedDynamicCardView(moment: moment, allMoments: viewStore.moments, currentIndex: index)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
if viewStore.hasMore {
|
||||
ProgressView()
|
||||
.onAppear {
|
||||
viewStore.send(.loadMore)
|
||||
}
|
||||
WithPerceptionTracking {
|
||||
LazyVStack(spacing: 12) {
|
||||
ForEach(Array(viewStore.moments.enumerated()), id: \.element.dynamicId) { index, moment in
|
||||
OptimizedDynamicCardView(moment: moment, allMoments: viewStore.moments, currentIndex: index)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
if viewStore.hasMore {
|
||||
ProgressView()
|
||||
.onAppear {
|
||||
viewStore.send(.loadMore)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.refreshable {
|
||||
viewStore.send(.refresh)
|
||||
@@ -116,9 +119,10 @@ struct MeView: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .top)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
ViewStore(self.store, observe: { $0 }).send(.onAppear)
|
||||
}
|
||||
.onAppear {
|
||||
ViewStore(self.store, observe: { $0 }).send(.onAppear)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
61
yana/Views/TEST/TestView.swift
Normal file
61
yana/Views/TEST/TestView.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
import SwiftUI
|
||||
|
||||
struct TestView: View {
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// 背景色
|
||||
Color.purple.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 30) {
|
||||
// 标题
|
||||
Text("测试页面")
|
||||
.font(.system(size: 32, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
|
||||
// 描述文本
|
||||
Text("这是一个测试用的页面\n用于验证导航跳转功能")
|
||||
.font(.system(size: 18))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
// 测试按钮
|
||||
Button(action: {
|
||||
debugInfoSync("[LOG] TestView button tapped")
|
||||
}) {
|
||||
Text("测试按钮")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.purple)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color.white)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 100)
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(action: {
|
||||
debugInfoSync("[LOG] TestView back button tapped")
|
||||
}) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
Text("返回")
|
||||
.font(.system(size: 16))
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
TestView()
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user