feat: 增强应用功能与用户体验

- 在Package.swift中更新依赖路径,确保项目结构清晰。
- 在AppSettingFeature中新增初始化方法,支持用户信息、头像和昵称的设置。
- 更新FeedListFeature和MainFeature,新增测试按钮和导航功能,提升用户交互体验。
- 在MeFeature中优化用户信息加载逻辑,增强错误处理能力。
- 新增TestView以支持测试功能,验证导航跳转的有效性。
- 更新多个视图以整合新功能,提升代码可读性与维护性。
This commit is contained in:
edwinQQQ
2025-07-25 14:10:56 +08:00
parent 343fd9e2df
commit fb09ddb956
14 changed files with 391 additions and 239 deletions

View File

@@ -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",
),
]
)
)

View File

@@ -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";
};

View File

@@ -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"
}
},
{

View File

@@ -22,6 +22,13 @@ struct AppSettingFeature {
var nicknameInput: String = ""
var isUpdatingUser: Bool = false
var updateUserError: String? = nil
// userInfoavatarURLnicknameinit
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 }
// avatarURLUI
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 userinfo1
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
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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 {
}
}
}
}
}

View File

@@ -27,7 +27,7 @@ struct AppRootView: View {
}
}
}
#Preview {
AppRootView()
}
//
//#Preview {
// AppRootView()
//}

View File

@@ -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))
}
}
}

View File

@@ -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 {
}
)
}
}
}
}
}
}

View File

@@ -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)")
// }
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}
}

View 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()
}
}