feat: 更新CreateFeed功能及相关视图组件

- 在CreateFeedFeature中新增isPresented依赖,确保在适当的上下文中执行视图关闭操作。
- 在FeedFeature中优化状态管理,简化CreateFeedView的呈现逻辑。
- 新增FeedListFeature和MainFeature,整合FeedListView和底部导航功能,提升用户体验。
- 更新HomeView和SplashView以集成MainView,确保应用结构一致性。
- 在多个视图中调整状态管理和导航逻辑,增强可维护性和用户体验。
This commit is contained in:
edwinQQQ
2025-07-21 19:10:31 +08:00
parent 5f65df0e7f
commit ba991598be
13 changed files with 804 additions and 553 deletions

View File

@@ -35,6 +35,7 @@ struct CreateFeedFeature {
@Dependency(\.apiService) var apiService @Dependency(\.apiService) var apiService
@Dependency(\.dismiss) var dismiss @Dependency(\.dismiss) var dismiss
@Dependency(\.isPresented) var isPresented
var body: some ReducerOf<Self> { var body: some ReducerOf<Self> {
Reduce { state, action in Reduce { state, action in
@@ -108,6 +109,11 @@ struct CreateFeedFeature {
state.errorMessage = nil state.errorMessage = nil
return .none return .none
case .dismissView: case .dismissView:
// presentation context
guard isPresented else {
// presentation contextdismiss
return .none
}
return .run { _ in return .run { _ in
await dismiss() await dismiss()
} }

View File

@@ -7,145 +7,103 @@ struct FeedFeature {
struct State: Equatable { struct State: Equatable {
var moments: [MomentsInfo] = [] var moments: [MomentsInfo] = []
var isLoading = false var isLoading = false
var isRefreshing = false
var hasMoreData = true var hasMoreData = true
var error: String? var error: String?
var nextDynamicId: Int = 0 var nextDynamicId: Int = 0
// CreateFeedView
// - var createFeedState = CreateFeedFeature.State()
var isInitialized = false
// CreateFeedView -
var isCreateFeedPresented = false
} }
enum Action { enum Action: Equatable {
case onAppear case onAppear
case refresh
case loadLatestMoments case loadLatestMoments
case loadMoreMoments case loadMoreMoments
case momentsResponse(TaskResult<MomentsLatestResponse>) case momentsResponse(TaskResult<MomentsLatestResponse>)
case clearError case clearError
case retryLoad case retryLoad
// CreateFeedView Action
// CreateFeedView Action -
case showCreateFeed
case createFeedCompleted case createFeedCompleted
case createFeedDismissed case createFeedDismissed
// CreateFeedFeature action
case createFeed(CreateFeedFeature.Action)
} }
@Dependency(\.apiService) var apiService @Dependency(\.apiService) var apiService
var body: some ReducerOf<Self> { var body: some ReducerOf<Self> {
Scope(state: \.createFeedState, action: \.createFeed) {
CreateFeedFeature()
}
Reduce { state, action in Reduce { state, action in
switch action { switch action {
case .onAppear: case .onAppear:
// guard state.moments.isEmpty && !state.isLoading else { return .none }
guard !state.isInitialized else {
return .none
}
return .send(.loadLatestMoments) return .send(.loadLatestMoments)
case .refresh:
guard !state.isRefreshing else { return .none }
state.isRefreshing = true
state.error = nil
let request = LatestDynamicsRequest(dynamicId: "", pageSize: 20, types: [.text, .picture])
return .run { send in
await send(.momentsResponse(TaskResult { try await apiService.request(request) }))
}
case .loadLatestMoments: case .loadLatestMoments:
// guard !state.isLoading else { return .none }
guard !state.isLoading else {
return .none
}
//
state.isLoading = true state.isLoading = true
state.error = nil state.error = nil
let request = LatestDynamicsRequest(dynamicId: "", pageSize: 20, types: [.text, .picture])
let request = LatestDynamicsRequest(
dynamicId: "", //
pageSize: 20,
types: [.text, .picture]
)
return .run { send in return .run { send in
await send(.momentsResponse(TaskResult { await send(.momentsResponse(TaskResult { try await apiService.request(request) }))
try await apiService.request(request)
}))
} }
case .loadMoreMoments: case .loadMoreMoments:
//
guard !state.isLoading && state.hasMoreData else { return .none } guard !state.isLoading && state.hasMoreData else { return .none }
state.isLoading = true state.isLoading = true
state.error = nil state.error = nil
let request = LatestDynamicsRequest(dynamicId: state.nextDynamicId == 0 ? "" : String(state.nextDynamicId), pageSize: 20, types: [.text, .picture])
let request = LatestDynamicsRequest(
dynamicId: state.nextDynamicId == 0 ? "" : String(state.nextDynamicId),
pageSize: 20,
types: [.text, .picture]
)
return .run { send in return .run { send in
await send(.momentsResponse(TaskResult { await send(.momentsResponse(TaskResult { try await apiService.request(request) }))
try await apiService.request(request)
}))
} }
case let .momentsResponse(.success(response)): case let .momentsResponse(.success(response)):
state.isLoading = false state.isLoading = false
state.isRefreshing = false
//
if !state.isInitialized {
state.isInitialized = true
}
//
guard response.code == 200, let data = response.data else { guard response.code == 200, let data = response.data else {
let errorMsg = response.message.isEmpty ? "获取动态失败" : response.message let errorMsg = response.message.isEmpty ? "获取动态失败" : response.message
state.error = errorMsg state.error = errorMsg
return .none return .none
} }
let isRefresh = state.nextDynamicId == 0 || state.isRefreshing
//
let isRefresh = state.nextDynamicId == 0
if isRefresh { if isRefresh {
//
state.moments = data.dynamicList state.moments = data.dynamicList
} else { } else {
//
state.moments.append(contentsOf: data.dynamicList) state.moments.append(contentsOf: data.dynamicList)
} }
//
state.nextDynamicId = data.nextDynamicId state.nextDynamicId = data.nextDynamicId
state.hasMoreData = !data.dynamicList.isEmpty state.hasMoreData = !data.dynamicList.isEmpty
return .none return .none
case let .momentsResponse(.failure(error)): case let .momentsResponse(.failure(error)):
state.isLoading = false state.isLoading = false
state.isRefreshing = false
state.error = error.localizedDescription state.error = error.localizedDescription
return .none return .none
case .clearError: case .clearError:
state.error = nil state.error = nil
return .none return .none
case .retryLoad: case .retryLoad:
//
if state.moments.isEmpty { if state.moments.isEmpty {
return .send(.loadLatestMoments) return .send(.loadLatestMoments)
} else { } else {
return .send(.loadMoreMoments) return .send(.loadMoreMoments)
} }
// CreateFeedView Action -
case .showCreateFeed:
state.isCreateFeedPresented = true
return .none
case .createFeedCompleted: case .createFeedCompleted:
// return .send(.refresh)
state.isCreateFeedPresented = false
return .send(.loadLatestMoments)
case .createFeedDismissed: case .createFeedDismissed:
state.isCreateFeedPresented = false return .none
case .createFeed(.dismissView):
return .send(.createFeedDismissed)
case .createFeed:
return .none return .none
} }
} }

View File

@@ -0,0 +1,50 @@
import Foundation
import ComposableArchitecture
struct FeedListFeature: Reducer {
struct State: Equatable {
var feeds: [Feed] = [] // feed
var isLoading: Bool = false
var error: String? = nil
var isEditFeedPresented: Bool = false // EditFeedView
}
enum Action: Equatable {
case onAppear
case reload
case loadMore
case editFeedButtonTapped // add
case editFeedDismissed //
// Action
}
func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case .onAppear:
//
return .none
case .reload:
//
return .none
case .loadMore:
//
return .none
case .editFeedButtonTapped:
state.isEditFeedPresented = true
return .none
case .editFeedDismissed:
state.isEditFeedPresented = false
return .none
}
}
}
// Feed
enum Feed: Equatable, Identifiable {
case placeholder(id: UUID = UUID())
var id: UUID {
switch self {
case .placeholder(let id): return id
}
}
}

View File

@@ -3,8 +3,12 @@ import ComposableArchitecture
@Reducer @Reducer
struct HomeFeature { struct HomeFeature {
enum Route: Equatable {
case createFeed
}
@ObservableState @ObservableState
struct State: Equatable { struct State: Equatable, Sendable {
var isInitialized = false var isInitialized = false
var userInfo: UserInfo? var userInfo: UserInfo?
var accountModel: AccountModel? var accountModel: AccountModel?
@@ -19,9 +23,13 @@ struct HomeFeature {
// //
var isLoggedOut = false var isLoggedOut = false
//
var route: Route? = nil
} }
enum Action { @CasePathable
enum Action: Equatable {
case onAppear case onAppear
case loadUserInfo case loadUserInfo
case userInfoLoaded(UserInfo?) case userInfoLoaded(UserInfo?)
@@ -39,81 +47,77 @@ struct HomeFeature {
// //
case logoutCompleted case logoutCompleted
// actions
case showCreateFeed
case createFeedDismissed
} }
var body: some ReducerOf<Self> { var body: some Reducer<State, Action> {
Scope(state: \.settingState, action: \.setting) { // Reducer<State, Action>.combine([
SettingFeature() // Reducer { state, action in
} // switch action {
// case .onAppear:
// Feed Scope // guard !state.isInitialized else {
Scope(state: \.feedState, action: \.feed) { // return Effect.none
FeedFeature() // }
} // state.isInitialized = true
// return .concatenate(
Reduce { state, action in // .send(.loadUserInfo),
switch action { // .send(.loadAccountModel)
case .onAppear: // )
// // case .loadUserInfo:
guard !state.isInitialized else { // return .run { send in
return .none // let userInfo = await UserInfoManager.getUserInfo()
} // await send(.userInfoLoaded(userInfo))
// }
state.isInitialized = true // case let .userInfoLoaded(userInfo):
return .concatenate( // state.userInfo = userInfo
.send(.loadUserInfo), // return Effect.none
.send(.loadAccountModel) // case .loadAccountModel:
) // return .run { send in
// let accountModel = await UserInfoManager.getAccountModel()
case .loadUserInfo: // await send(.accountModelLoaded(accountModel))
// // }
return .run { send in // case let .accountModelLoaded(accountModel):
let userInfo = await UserInfoManager.getUserInfo() // state.accountModel = accountModel
await send(.userInfoLoaded(userInfo)) // return Effect.none
} // case .logoutTapped:
// return .send(.logout)
case let .userInfoLoaded(userInfo): // case .logout:
state.userInfo = userInfo // return .run { send in
return .none // await UserInfoManager.clearAllAuthenticationData()
// await send(.logoutCompleted)
case .loadAccountModel: // }
// // case .logoutCompleted:
return .run { send in // state.isLoggedOut = true
let accountModel = await UserInfoManager.getAccountModel() // return Effect.none
await send(.accountModelLoaded(accountModel)) // case .settingDismissed:
} // state.isSettingPresented = false
// return Effect.none
case let .accountModelLoaded(accountModel): // case .setting:
state.accountModel = accountModel // return Effect.none
return .none // case .showCreateFeed:
// state.route = .createFeed
case .logoutTapped: // return Effect.none
return .send(.logout) // case .createFeedDismissed:
// state.route = nil
case .logout: // return Effect.none
// // case .feed:
return .run { send in // return Effect.none
await UserInfoManager.clearAllAuthenticationData() // }
await send(.logoutCompleted) // },
} // Scope(
// state: \State.settingState,
case .logoutCompleted: // action: /Action.setting,
state.isLoggedOut = true // child: SettingFeature()
return .none // ),
// Scope(
case .settingDismissed: // state: \State.feedState,
state.isSettingPresented = false // action: /Action.feed,
return .none // child: FeedFeature()
// )
case .setting: // ])
// reducer
return .none
case .feed(_):
// FeedFeature action Scope
return .none
}
}
} }
} }

View File

@@ -0,0 +1,35 @@
import Foundation
import ComposableArchitecture
import CasePaths
struct MainFeature: Reducer {
enum Tab: Int, Equatable, CaseIterable {
case feed, other
}
struct State: Equatable {
var selectedTab: Tab = .feed
var feedList: FeedListFeature.State = .init()
}
@CasePathable
enum Action: Equatable {
case selectTab(Tab)
case feedList(FeedListFeature.Action)
}
var body: some ReducerOf<Self> {
Scope(state: \.feedList, action: \.feedList) {
FeedListFeature()
}
Reduce { state, action in
switch action {
case .selectTab(let tab):
state.selectedTab = tab
return .none
case .feedList:
return .none
}
}
}
}

View File

@@ -6,14 +6,11 @@ struct AppRootView: View {
var body: some View { var body: some View {
if isLoggedIn { if isLoggedIn {
HomeView( MainView(
store: Store( store: Store(
initialState: HomeFeature.State() initialState: MainFeature.State()
) { ) {
HomeFeature() MainFeature()
},
onLogout: {
isLoggedIn = false
} }
) )
} else { } else {

View File

@@ -4,23 +4,17 @@ import PhotosUI
struct CreateFeedView: View { struct CreateFeedView: View {
let store: StoreOf<CreateFeedFeature> let store: StoreOf<CreateFeedFeature>
@State private var keyboardHeight: CGFloat = 0
var body: some View { var body: some View {
NavigationStack { NavigationStack {
GeometryReader { geometry in GeometryReader { geometry in
ZStack { VStack(spacing: 0) {
// //
LinearGradient( Color(hex: 0x0C0527)
gradient: Gradient(colors: [
Color(red: 0.1, green: 0.1, blue: 0.2),
Color(red: 0.2, green: 0.1, blue: 0.3)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea() .ignoresSafeArea()
ScrollView { // ScrollView
VStack(spacing: 20) { VStack(spacing: 20) {
// //
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
@@ -28,7 +22,7 @@ struct CreateFeedView: View {
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
RoundedRectangle(cornerRadius: 12) RoundedRectangle(cornerRadius: 12)
.fill(Color.white.opacity(0.1)) .fill(Color.white.opacity(0.1))
.frame(minHeight: 120) .frame(height: 200) // 200
if store.content.isEmpty { if store.content.isEmpty {
Text("Enter Content") Text("Enter Content")
@@ -46,6 +40,7 @@ struct CreateFeedView: View {
.padding(.horizontal, 12) .padding(.horizontal, 12)
.padding(.vertical, 8) .padding(.vertical, 8)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.frame(height: 200) // 200
} }
// //
@@ -100,15 +95,14 @@ struct CreateFeedView: View {
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
} }
// //
Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100) Color.clear.frame(height: max(keyboardHeight, geometry.safeAreaInsets.bottom) + 100)
}
} }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.ignoresSafeArea(.keyboard, edges: .bottom)
// // -
VStack { VStack {
Spacer()
Button(action: { Button(action: {
store.send(.publishButtonTapped) store.send(.publishButtonTapped)
}) { }) {
@@ -129,46 +123,48 @@ struct CreateFeedView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.frame(height: 50) .frame(height: 50)
.background( .background(
LinearGradient( Color(hex: 0x0C0527)
gradient: Gradient(colors: [
Color.purple,
Color.blue
]),
startPoint: .leading,
endPoint: .trailing
)
) )
.cornerRadius(25) .cornerRadius(25)
.disabled(store.isLoading || !store.canPublish) .disabled(store.isLoading || !store.canPublish)
.opacity(store.isLoading || !store.canPublish ? 0.6 : 1.0) .opacity(store.isLoading || !store.canPublish ? 0.6 : 1.0)
} }
.padding(.horizontal, 20) .padding(.horizontal, 20)
.padding(.bottom, geometry.safeAreaInsets.bottom + 20) .padding(.bottom, max(keyboardHeight, geometry.safeAreaInsets.bottom) + 20)
} }
.background(
Color(hex: 0x0C0527)
)
} }
} }
.navigationTitle("图文发布") .navigationTitle("图文发布")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.hidden, for: .navigationBar) .toolbarBackground(.hidden, for: .navigationBar)
.navigationBarBackButtonHidden(true)
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
Button("取消") { Button(action: {
store.send(.dismissView) store.send(.dismissView)
} }) {
Image(systemName: "xmark")
.font(.system(size: 18, weight: .medium))
.foregroundColor(.white) .foregroundColor(.white)
} }
ToolbarItem(placement: .navigationBarTrailing) {
Button("发布") {
store.send(.publishButtonTapped)
}
.foregroundColor(store.canPublish ? .white : .white.opacity(0.5))
.disabled(!store.canPublish || store.isLoading)
} }
//
} }
} }
.preferredColorScheme(.dark) .preferredColorScheme(.dark)
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notification in
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
keyboardHeight = keyboardFrame.height
} }
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
keyboardHeight = 0
}
}
} }
// MARK: - iOS 16+ // MARK: - iOS 16+
@@ -182,6 +178,7 @@ struct ModernImageSelectionGrid: View {
private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3) private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3)
var body: some View { var body: some View {
WithPerceptionTracking {
LazyVGrid(columns: columns, spacing: 8) { LazyVGrid(columns: columns, spacing: 8) {
// //
ForEach(Array(images.enumerated()), id: \.offset) { index, image in ForEach(Array(images.enumerated()), id: \.offset) { index, image in
@@ -229,13 +226,14 @@ struct ModernImageSelectionGrid: View {
} }
} }
} }
}
} }
// MARK: - // MARK: -
#Preview { //#Preview {
CreateFeedView( // CreateFeedView(
store: Store(initialState: CreateFeedFeature.State()) { // store: Store(initialState: CreateFeedFeature.State()) {
CreateFeedFeature() // CreateFeedFeature()
} // }
) // )
} //}

View File

@@ -0,0 +1,19 @@
import SwiftUI
struct EditFeedView: View {
var body: some View {
VStack(spacing: 20) {
Text("编辑动态")
.font(.title)
.bold()
Text("这里是 EditFeedView 占位内容")
.foregroundColor(.gray)
Spacer()
}
.padding()
}
}
#Preview {
EditFeedView()
}

View File

@@ -0,0 +1,67 @@
import SwiftUI
import ComposableArchitecture
struct FeedListView: View {
let store: StoreOf<FeedListFeature>
@State private var isEditFeedSheetPresented = false // sheet
var body: some View {
WithPerceptionTracking {
GeometryReader { geometry in
ZStack {
//
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.clipped()
.ignoresSafeArea(.all)
VStack(alignment: .center, spacing: 0) {
//
HStack {
Spacer(minLength: 0)
Spacer(minLength: 0)
Text("Enjoy your Life Time")
.font(.system(size: 22, weight: .semibold))
.foregroundColor(.white)
.frame(maxWidth: .infinity, alignment: .center)
Spacer(minLength: 0)
Button(action: {
store.send(.editFeedButtonTapped)
}) {
Image("add icon")
.resizable()
.frame(width: 36, height: 36)
}
}
.padding(.horizontal, 20)
.padding(.top, geometry.safeAreaInsets.top)
//
Image(systemName: "heart.fill")
.font(.system(size: 60))
.foregroundColor(.red)
.padding(.top, 40)
Text("The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable.")
.font(.system(size: 16))
.multilineTextAlignment(.center)
.foregroundColor(.white.opacity(0.9))
.padding(.horizontal, 30)
.padding(.bottom, 30)
Spacer()
}
.frame(maxWidth: .infinity, alignment: .top)
}
}
.onAppear {
store.send(.onAppear)
}
// .sheet(isPresented: store.binding(
// get: \.isEditFeedPresented,
// send: { $0 ? .editFeedButtonTapped : .editFeedDismissed }
// )) {
// EditFeedView()
// }
}
}
}

View File

@@ -3,7 +3,9 @@ import ComposableArchitecture
struct FeedTopBarView: View { struct FeedTopBarView: View {
let store: StoreOf<FeedFeature> let store: StoreOf<FeedFeature>
let onShowCreateFeed: () -> Void
var body: some View { var body: some View {
WithPerceptionTracking {
HStack { HStack {
Spacer() Spacer()
Text("Enjoy your Life Time") Text("Enjoy your Life Time")
@@ -11,7 +13,7 @@ struct FeedTopBarView: View {
.foregroundColor(.white) .foregroundColor(.white)
Spacer() Spacer()
Button(action: { Button(action: {
store.send(.showCreateFeed) onShowCreateFeed() //
}) { }) {
Image("add icon") Image("add icon")
.frame(width: 36, height: 36) .frame(width: 36, height: 36)
@@ -19,11 +21,13 @@ struct FeedTopBarView: View {
} }
.padding(.horizontal, 20) .padding(.horizontal, 20)
} }
}
} }
struct FeedMomentsListView: View { struct FeedMomentsListView: View {
let store: StoreOf<FeedFeature> let store: StoreOf<FeedFeature>
var body: some View { var body: some View {
WithPerceptionTracking {
LazyVStack(spacing: 16) { LazyVStack(spacing: 16) {
if store.moments.isEmpty { if store.moments.isEmpty {
VStack(spacing: 12) { VStack(spacing: 12) {
@@ -40,31 +44,82 @@ struct FeedMomentsListView: View {
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.horizontal, 20) .padding(.horizontal, 20)
} }
//
if store.error != nil {
Button(action: {
store.send(.retryLoad)
}) {
Text("重试")
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 8)
.background(Color.blue.opacity(0.8))
.cornerRadius(8)
}
.padding(.top, 8)
}
} }
.padding(.top, 40) .padding(.top, 40)
} else { } else {
ForEach(Array(store.moments.enumerated()), id: \.element.dynamicId) { index, moment in ForEach(Array(store.moments.enumerated()), id: \.element.dynamicId) { index, moment in
OptimizedDynamicCardView( WithPerceptionTracking {
moment: moment, Text(moment.avatar)
allMoments: store.moments, // OptimizedDynamicCardView(
currentIndex: index // moment: moment,
) // allMoments: store.moments,
// currentIndex: index
// )
.onAppear {
//
if index == store.moments.count - 1 && store.hasMoreData && !store.isLoading {
store.send(.loadMoreMoments)
}
}
}
}
//
if store.isLoading && !store.moments.isEmpty {
HStack {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
Text("加载更多...")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.8))
}
.padding(.top, 20)
} }
} }
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.top, 30) .padding(.top, 20) //
}
} }
} }
struct FeedView: View { struct FeedView: View {
let store: StoreOf<FeedFeature> let store: StoreOf<FeedFeature>
let onShowCreateFeed: () -> Void
var body: some View { var body: some View {
WithPerceptionTracking {
GeometryReader { geometry in GeometryReader { geometry in
ScrollView { ZStack {
// - HomeView
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.clipped()
.ignoresSafeArea(.all)
//
VStack(spacing: 0) {
//
VStack(spacing: 20) { VStack(spacing: 20) {
FeedTopBarView(store: store) FeedTopBarView(store: store, onShowCreateFeed: onShowCreateFeed)
Image(systemName: "heart.fill") Image(systemName: "heart.fill")
.font(.system(size: 60)) .font(.system(size: 60))
.foregroundColor(.red) .foregroundColor(.red)
@@ -74,40 +129,32 @@ struct FeedView: View {
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.foregroundColor(.white.opacity(0.9)) .foregroundColor(.white.opacity(0.9))
.padding(.horizontal, 30) .padding(.horizontal, 30)
.padding(.top, 20) .padding(.bottom, 30)
}
// .padding(.top, 60) //
// -
ScrollView {
FeedMomentsListView(store: store) FeedMomentsListView(store: store)
if store.isLoading { .padding(.bottom, 20) //
HStack {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
Text("加载中...")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.8))
}
.padding(.top, 20)
}
Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100)
}
} }
.refreshable { .refreshable {
store.send(.loadLatestMoments) //
await withCheckedContinuation { continuation in
store.send(.refresh)
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
continuation.resume()
}
}
}
}
}
} }
.onAppear { .onAppear {
store.send(.onAppear) store.send(.onAppear)
} }
} }
.sheet(isPresented: Binding(
get: { store.isCreateFeedPresented },
set: { _ in store.send(.createFeedDismissed) }
)) {
CreateFeedView(
store: Store(
initialState: CreateFeedFeature.State()
) {
CreateFeedFeature()
}
)
}
} }
} }
@@ -118,6 +165,7 @@ struct OptimizedDynamicCardView: View {
let currentIndex: Int let currentIndex: Int
var body: some View { var body: some View {
WithPerceptionTracking{
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
// //
HStack { HStack {
@@ -201,6 +249,7 @@ struct OptimizedDynamicCardView: View {
} }
.padding(.top, 8) .padding(.top, 8)
} }
}
.padding(16) .padding(16)
.background( .background(
Color.white.opacity(0.1) Color.white.opacity(0.1)
@@ -261,9 +310,13 @@ struct OptimizedImageGrid: View {
var body: some View { var body: some View {
GeometryReader { geometry in GeometryReader { geometry in
let availableWidth = geometry.size.width let availableWidth = max(geometry.size.width, 1) // 0
let spacing: CGFloat = 8 let spacing: CGFloat = 8
// availableWidth
if availableWidth < 10 {
Color.clear.frame(height: 1)
} else {
switch images.count { switch images.count {
case 1: case 1:
// //
@@ -273,7 +326,6 @@ struct OptimizedImageGrid: View {
SquareImageView(image: images[0], size: imageSize) SquareImageView(image: images[0], size: imageSize)
Spacer() Spacer()
} }
case 2: case 2:
// //
let imageSize: CGFloat = (availableWidth - spacing) / 2 let imageSize: CGFloat = (availableWidth - spacing) / 2
@@ -281,7 +333,6 @@ struct OptimizedImageGrid: View {
SquareImageView(image: images[0], size: imageSize) SquareImageView(image: images[0], size: imageSize)
SquareImageView(image: images[1], size: imageSize) SquareImageView(image: images[1], size: imageSize)
} }
case 3: case 3:
// //
let imageSize: CGFloat = (availableWidth - spacing * 2) / 3 let imageSize: CGFloat = (availableWidth - spacing * 2) / 3
@@ -290,12 +341,10 @@ struct OptimizedImageGrid: View {
SquareImageView(image: image, size: imageSize) SquareImageView(image: image, size: imageSize)
} }
} }
default: default:
// 9 // 9
let imageSize: CGFloat = (availableWidth - spacing * 2) / 3 let imageSize: CGFloat = (availableWidth - spacing * 2) / 3
let columns = Array(repeating: GridItem(.fixed(imageSize), spacing: spacing), count: 3) let columns = Array(repeating: GridItem(.fixed(imageSize), spacing: spacing), count: 3)
LazyVGrid(columns: columns, spacing: spacing) { LazyVGrid(columns: columns, spacing: spacing) {
ForEach(images.prefix(9), id: \.id) { image in ForEach(images.prefix(9), id: \.id) { image in
SquareImageView(image: image, size: imageSize) SquareImageView(image: image, size: imageSize)
@@ -303,6 +352,7 @@ struct OptimizedImageGrid: View {
} }
} }
} }
}
.frame(height: calculateGridHeight()) .frame(height: calculateGridHeight())
} }
@@ -328,6 +378,7 @@ struct SquareImageView: View {
let size: CGFloat let size: CGFloat
var body: some View { var body: some View {
let safeSize = size.isFinite && size > 0 ? size : 100 //
CachedAsyncImage(url: image.resUrl) { imageView in CachedAsyncImage(url: image.resUrl) { imageView in
imageView imageView
.resizable() .resizable()
@@ -341,7 +392,7 @@ struct SquareImageView: View {
.scaleEffect(0.8) .scaleEffect(0.8)
) )
} }
.frame(width: size, height: size) .frame(width: safeSize, height: safeSize)
.clipped() .clipped()
.cornerRadius(8) .cornerRadius(8)
} }

View File

@@ -3,11 +3,12 @@ import ComposableArchitecture
struct HomeView: View { struct HomeView: View {
let store: StoreOf<HomeFeature> let store: StoreOf<HomeFeature>
let onLogout: () -> Void // let onLogout: () -> Void
@ObservedObject private var localizationManager = LocalizationManager.shared @ObservedObject private var localizationManager = LocalizationManager.shared
@State private var selectedTab: Tab = .feed @State private var selectedTab: Tab = .feed
var body: some View { var body: some View {
NavigationStack {
GeometryReader { geometry in GeometryReader { geometry in
ZStack { ZStack {
// 使 "bg" - // 使 "bg" -
@@ -22,11 +23,15 @@ struct HomeView: View {
ZStack { ZStack {
switch selectedTab { switch selectedTab {
case .feed: case .feed:
NavigationStack {
FeedView( FeedView(
store: store.scope(state: \.feedState, action: \.feed) store: store.scope(
) state: \.feedState,
action: \.feed
),
onShowCreateFeed: {
store.send(.showCreateFeed)
} }
)
.transition(.opacity) .transition(.opacity)
case .me: case .me:
MeView(onLogout: onLogout) MeView(onLogout: onLogout)
@@ -47,11 +52,27 @@ struct HomeView: View {
store.send(.onAppear) store.send(.onAppear)
} }
.sheet(isPresented: Binding( .sheet(isPresented: Binding(
get: { store.isSettingPresented }, get: { store.withState(\.isSettingPresented) },
set: { _ in store.send(.settingDismissed) } set: { _ in store.send(.settingDismissed) }
)) { )) {
SettingView(store: store.scope(state: \.settingState, action: \.setting)) SettingView(store: store.scope(state: \.settingState, action: \.setting))
} }
.navigationDestination(isPresented: Binding(
get: { store.withState(\.route) == .createFeed },
set: { isPresented in
if !isPresented {
store.send(.createFeedDismissed)
}
}
)) {
CreateFeedView(
store: store.scope(
state: \.feedState.createFeedState,
action: \.feed.createFeed
)
)
}
}
} }
} }

49
yana/Views/MainView.swift Normal file
View File

@@ -0,0 +1,49 @@
import SwiftUI
import ComposableArchitecture
//import Components // BottomTabView Components
struct MainView: View {
let store: StoreOf<MainFeature>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
NavigationStack {
GeometryReader { geometry in
ZStack {
//
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.clipped()
.ignoresSafeArea(.all)
//
ZStack {
switch viewStore.selectedTab {
case .feed:
FeedListView(store: store.scope(
state: \.feedList,
action: \.feedList
))
.transition(.opacity)
case .other:
MeView(onLogout: {}) //
.transition(.opacity)
}
}
.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)
}
}
}
}
}
}

View File

@@ -25,15 +25,11 @@ struct SplashView: View {
) )
case .main: case .main:
// //
HomeView( MainView(
store: Store( store: Store(
initialState: HomeFeature.State() initialState: MainFeature.State()
) { ) {
HomeFeature() MainFeature()
},
onLogout: {
//
store.send(.navigateToLogin)
} }
) )
} }