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

View File

@@ -7,145 +7,103 @@ struct FeedFeature {
struct State: Equatable {
var moments: [MomentsInfo] = []
var isLoading = false
var isRefreshing = false
var hasMoreData = true
var error: String?
var nextDynamicId: Int = 0
// -
var isInitialized = false
// CreateFeedView -
var isCreateFeedPresented = false
// CreateFeedView
var createFeedState = CreateFeedFeature.State()
}
enum Action {
enum Action: Equatable {
case onAppear
case refresh
case loadLatestMoments
case loadMoreMoments
case momentsResponse(TaskResult<MomentsLatestResponse>)
case clearError
case retryLoad
// CreateFeedView Action -
case showCreateFeed
// CreateFeedView Action
case createFeedCompleted
case createFeedDismissed
// CreateFeedFeature action
case createFeed(CreateFeedFeature.Action)
}
@Dependency(\.apiService) var apiService
var body: some ReducerOf<Self> {
Scope(state: \.createFeedState, action: \.createFeed) {
CreateFeedFeature()
}
Reduce { state, action in
switch action {
case .onAppear:
//
guard !state.isInitialized else {
return .none
}
guard state.moments.isEmpty && !state.isLoading else { return .none }
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:
//
guard !state.isLoading else {
return .none
}
//
guard !state.isLoading else { return .none }
state.isLoading = true
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
await send(.momentsResponse(TaskResult {
try await apiService.request(request)
}))
await send(.momentsResponse(TaskResult { try await apiService.request(request) }))
}
case .loadMoreMoments:
//
guard !state.isLoading && state.hasMoreData else { return .none }
state.isLoading = true
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
await send(.momentsResponse(TaskResult {
try await apiService.request(request)
}))
await send(.momentsResponse(TaskResult { try await apiService.request(request) }))
}
case let .momentsResponse(.success(response)):
state.isLoading = false
//
if !state.isInitialized {
state.isInitialized = true
}
//
state.isRefreshing = false
guard response.code == 200, let data = response.data else {
let errorMsg = response.message.isEmpty ? "获取动态失败" : response.message
state.error = errorMsg
return .none
}
//
let isRefresh = state.nextDynamicId == 0
let isRefresh = state.nextDynamicId == 0 || state.isRefreshing
if isRefresh {
//
state.moments = data.dynamicList
} else {
//
state.moments.append(contentsOf: data.dynamicList)
}
//
state.nextDynamicId = data.nextDynamicId
state.hasMoreData = !data.dynamicList.isEmpty
return .none
case let .momentsResponse(.failure(error)):
state.isLoading = false
state.isRefreshing = false
state.error = error.localizedDescription
return .none
case .clearError:
state.error = nil
return .none
case .retryLoad:
//
if state.moments.isEmpty {
return .send(.loadLatestMoments)
} else {
return .send(.loadMoreMoments)
}
// CreateFeedView Action -
case .showCreateFeed:
state.isCreateFeedPresented = true
return .none
case .createFeedCompleted:
//
state.isCreateFeedPresented = false
return .send(.loadLatestMoments)
return .send(.refresh)
case .createFeedDismissed:
state.isCreateFeedPresented = false
return .none
case .createFeed(.dismissView):
return .send(.createFeedDismissed)
case .createFeed:
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
struct HomeFeature {
enum Route: Equatable {
case createFeed
}
@ObservableState
struct State: Equatable {
struct State: Equatable, Sendable {
var isInitialized = false
var userInfo: UserInfo?
var accountModel: AccountModel?
@@ -19,9 +23,13 @@ struct HomeFeature {
//
var isLoggedOut = false
//
var route: Route? = nil
}
enum Action {
@CasePathable
enum Action: Equatable {
case onAppear
case loadUserInfo
case userInfoLoaded(UserInfo?)
@@ -39,81 +47,77 @@ struct HomeFeature {
//
case logoutCompleted
// actions
case showCreateFeed
case createFeedDismissed
}
var body: some ReducerOf<Self> {
Scope(state: \.settingState, action: \.setting) {
SettingFeature()
}
// Feed Scope
Scope(state: \.feedState, action: \.feed) {
FeedFeature()
}
Reduce { state, action in
switch action {
case .onAppear:
//
guard !state.isInitialized else {
return .none
}
state.isInitialized = true
return .concatenate(
.send(.loadUserInfo),
.send(.loadAccountModel)
)
case .loadUserInfo:
//
return .run { send in
let userInfo = await UserInfoManager.getUserInfo()
await send(.userInfoLoaded(userInfo))
}
case let .userInfoLoaded(userInfo):
state.userInfo = userInfo
return .none
case .loadAccountModel:
//
return .run { send in
let accountModel = await UserInfoManager.getAccountModel()
await send(.accountModelLoaded(accountModel))
}
case let .accountModelLoaded(accountModel):
state.accountModel = accountModel
return .none
case .logoutTapped:
return .send(.logout)
case .logout:
//
return .run { send in
await UserInfoManager.clearAllAuthenticationData()
await send(.logoutCompleted)
}
case .logoutCompleted:
state.isLoggedOut = true
return .none
case .settingDismissed:
state.isSettingPresented = false
return .none
case .setting:
// reducer
return .none
case .feed(_):
// FeedFeature action Scope
return .none
}
}
var body: some Reducer<State, Action> {
// Reducer<State, Action>.combine([
// Reducer { state, action in
// switch action {
// case .onAppear:
// guard !state.isInitialized else {
// return Effect.none
// }
// state.isInitialized = true
// return .concatenate(
// .send(.loadUserInfo),
// .send(.loadAccountModel)
// )
// case .loadUserInfo:
// return .run { send in
// let userInfo = await UserInfoManager.getUserInfo()
// await send(.userInfoLoaded(userInfo))
// }
// case let .userInfoLoaded(userInfo):
// state.userInfo = userInfo
// return Effect.none
// case .loadAccountModel:
// return .run { send in
// let accountModel = await UserInfoManager.getAccountModel()
// await send(.accountModelLoaded(accountModel))
// }
// case let .accountModelLoaded(accountModel):
// state.accountModel = accountModel
// return Effect.none
// case .logoutTapped:
// return .send(.logout)
// case .logout:
// return .run { send in
// await UserInfoManager.clearAllAuthenticationData()
// await send(.logoutCompleted)
// }
// case .logoutCompleted:
// state.isLoggedOut = true
// return Effect.none
// case .settingDismissed:
// state.isSettingPresented = false
// return Effect.none
// case .setting:
// return Effect.none
// case .showCreateFeed:
// state.route = .createFeed
// return Effect.none
// case .createFeedDismissed:
// state.route = nil
// return Effect.none
// case .feed:
// return Effect.none
// }
// },
// Scope(
// state: \State.settingState,
// action: /Action.setting,
// child: SettingFeature()
// ),
// Scope(
// state: \State.feedState,
// action: /Action.feed,
// child: FeedFeature()
// )
// ])
}
}

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 {
if isLoggedIn {
HomeView(
MainView(
store: Store(
initialState: HomeFeature.State()
initialState: MainFeature.State()
) {
HomeFeature()
},
onLogout: {
isLoggedIn = false
MainFeature()
}
)
} else {

View File

@@ -4,23 +4,17 @@ import PhotosUI
struct CreateFeedView: View {
let store: StoreOf<CreateFeedFeature>
@State private var keyboardHeight: CGFloat = 0
var body: some View {
NavigationStack {
GeometryReader { geometry in
ZStack {
//
LinearGradient(
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
)
VStack(spacing: 0) {
//
Color(hex: 0x0C0527)
.ignoresSafeArea()
ScrollView {
// ScrollView
VStack(spacing: 20) {
//
VStack(alignment: .leading, spacing: 12) {
@@ -28,7 +22,7 @@ struct CreateFeedView: View {
ZStack(alignment: .topLeading) {
RoundedRectangle(cornerRadius: 12)
.fill(Color.white.opacity(0.1))
.frame(minHeight: 120)
.frame(height: 200) // 200
if store.content.isEmpty {
Text("Enter Content")
@@ -46,6 +40,7 @@ struct CreateFeedView: View {
.padding(.horizontal, 12)
.padding(.vertical, 8)
.scrollContentBackground(.hidden)
.frame(height: 200) // 200
}
//
@@ -100,15 +95,14 @@ struct CreateFeedView: View {
.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 {
Spacer()
Button(action: {
store.send(.publishButtonTapped)
}) {
@@ -129,46 +123,48 @@ struct CreateFeedView: View {
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(
LinearGradient(
gradient: Gradient(colors: [
Color.purple,
Color.blue
]),
startPoint: .leading,
endPoint: .trailing
)
Color(hex: 0x0C0527)
)
.cornerRadius(25)
.disabled(store.isLoading || !store.canPublish)
.opacity(store.isLoading || !store.canPublish ? 0.6 : 1.0)
}
.padding(.horizontal, 20)
.padding(.bottom, geometry.safeAreaInsets.bottom + 20)
.padding(.bottom, max(keyboardHeight, geometry.safeAreaInsets.bottom) + 20)
}
.background(
Color(hex: 0x0C0527)
)
}
}
.navigationTitle("图文发布")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.hidden, for: .navigationBar)
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("取消") {
Button(action: {
store.send(.dismissView)
}
}) {
Image(systemName: "xmark")
.font(.system(size: 18, weight: .medium))
.foregroundColor(.white)
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("发布") {
store.send(.publishButtonTapped)
}
.foregroundColor(store.canPublish ? .white : .white.opacity(0.5))
.disabled(!store.canPublish || store.isLoading)
}
//
}
}
.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+
@@ -182,6 +178,7 @@ struct ModernImageSelectionGrid: View {
private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3)
var body: some View {
WithPerceptionTracking {
LazyVGrid(columns: columns, spacing: 8) {
//
ForEach(Array(images.enumerated()), id: \.offset) { index, image in
@@ -229,13 +226,14 @@ struct ModernImageSelectionGrid: View {
}
}
}
}
}
// MARK: -
#Preview {
CreateFeedView(
store: Store(initialState: CreateFeedFeature.State()) {
CreateFeedFeature()
}
)
}
//#Preview {
// CreateFeedView(
// store: Store(initialState: CreateFeedFeature.State()) {
// 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 {
let store: StoreOf<FeedFeature>
let onShowCreateFeed: () -> Void
var body: some View {
WithPerceptionTracking {
HStack {
Spacer()
Text("Enjoy your Life Time")
@@ -11,7 +13,7 @@ struct FeedTopBarView: View {
.foregroundColor(.white)
Spacer()
Button(action: {
store.send(.showCreateFeed)
onShowCreateFeed() //
}) {
Image("add icon")
.frame(width: 36, height: 36)
@@ -19,11 +21,13 @@ struct FeedTopBarView: View {
}
.padding(.horizontal, 20)
}
}
}
struct FeedMomentsListView: View {
let store: StoreOf<FeedFeature>
var body: some View {
WithPerceptionTracking {
LazyVStack(spacing: 16) {
if store.moments.isEmpty {
VStack(spacing: 12) {
@@ -40,31 +44,82 @@ struct FeedMomentsListView: View {
.multilineTextAlignment(.center)
.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)
} else {
ForEach(Array(store.moments.enumerated()), id: \.element.dynamicId) { index, moment in
OptimizedDynamicCardView(
moment: moment,
allMoments: store.moments,
currentIndex: index
)
WithPerceptionTracking {
Text(moment.avatar)
// OptimizedDynamicCardView(
// 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(.top, 30)
.padding(.top, 20) //
}
}
}
struct FeedView: View {
let store: StoreOf<FeedFeature>
let onShowCreateFeed: () -> Void
var body: some View {
WithPerceptionTracking {
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) {
FeedTopBarView(store: store)
FeedTopBarView(store: store, onShowCreateFeed: onShowCreateFeed)
Image(systemName: "heart.fill")
.font(.system(size: 60))
.foregroundColor(.red)
@@ -74,40 +129,32 @@ struct FeedView: View {
.multilineTextAlignment(.center)
.foregroundColor(.white.opacity(0.9))
.padding(.horizontal, 30)
.padding(.top, 20)
.padding(.bottom, 30)
}
// .padding(.top, 60) //
// -
ScrollView {
FeedMomentsListView(store: store)
if store.isLoading {
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)
}
.padding(.bottom, 20) //
}
.refreshable {
store.send(.loadLatestMoments)
//
await withCheckedContinuation { continuation in
store.send(.refresh)
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
continuation.resume()
}
}
}
}
}
}
.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
var body: some View {
WithPerceptionTracking{
VStack(alignment: .leading, spacing: 12) {
//
HStack {
@@ -201,6 +249,7 @@ struct OptimizedDynamicCardView: View {
}
.padding(.top, 8)
}
}
.padding(16)
.background(
Color.white.opacity(0.1)
@@ -261,9 +310,13 @@ struct OptimizedImageGrid: View {
var body: some View {
GeometryReader { geometry in
let availableWidth = geometry.size.width
let availableWidth = max(geometry.size.width, 1) // 0
let spacing: CGFloat = 8
// availableWidth
if availableWidth < 10 {
Color.clear.frame(height: 1)
} else {
switch images.count {
case 1:
//
@@ -273,7 +326,6 @@ struct OptimizedImageGrid: View {
SquareImageView(image: images[0], size: imageSize)
Spacer()
}
case 2:
//
let imageSize: CGFloat = (availableWidth - spacing) / 2
@@ -281,7 +333,6 @@ struct OptimizedImageGrid: View {
SquareImageView(image: images[0], size: imageSize)
SquareImageView(image: images[1], size: imageSize)
}
case 3:
//
let imageSize: CGFloat = (availableWidth - spacing * 2) / 3
@@ -290,12 +341,10 @@ struct OptimizedImageGrid: View {
SquareImageView(image: image, size: imageSize)
}
}
default:
// 9
let imageSize: CGFloat = (availableWidth - spacing * 2) / 3
let columns = Array(repeating: GridItem(.fixed(imageSize), spacing: spacing), count: 3)
LazyVGrid(columns: columns, spacing: spacing) {
ForEach(images.prefix(9), id: \.id) { image in
SquareImageView(image: image, size: imageSize)
@@ -303,6 +352,7 @@ struct OptimizedImageGrid: View {
}
}
}
}
.frame(height: calculateGridHeight())
}
@@ -328,6 +378,7 @@ struct SquareImageView: View {
let size: CGFloat
var body: some View {
let safeSize = size.isFinite && size > 0 ? size : 100 //
CachedAsyncImage(url: image.resUrl) { imageView in
imageView
.resizable()
@@ -341,7 +392,7 @@ struct SquareImageView: View {
.scaleEffect(0.8)
)
}
.frame(width: size, height: size)
.frame(width: safeSize, height: safeSize)
.clipped()
.cornerRadius(8)
}

View File

@@ -3,11 +3,12 @@ import ComposableArchitecture
struct HomeView: View {
let store: StoreOf<HomeFeature>
let onLogout: () -> Void //
let onLogout: () -> Void
@ObservedObject private var localizationManager = LocalizationManager.shared
@State private var selectedTab: Tab = .feed
var body: some View {
NavigationStack {
GeometryReader { geometry in
ZStack {
// 使 "bg" -
@@ -22,11 +23,15 @@ struct HomeView: View {
ZStack {
switch selectedTab {
case .feed:
NavigationStack {
FeedView(
store: store.scope(state: \.feedState, action: \.feed)
)
store: store.scope(
state: \.feedState,
action: \.feed
),
onShowCreateFeed: {
store.send(.showCreateFeed)
}
)
.transition(.opacity)
case .me:
MeView(onLogout: onLogout)
@@ -47,11 +52,27 @@ struct HomeView: View {
store.send(.onAppear)
}
.sheet(isPresented: Binding(
get: { store.isSettingPresented },
get: { store.withState(\.isSettingPresented) },
set: { _ in store.send(.settingDismissed) }
)) {
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:
//
HomeView(
MainView(
store: Store(
initialState: HomeFeature.State()
initialState: MainFeature.State()
) {
HomeFeature()
},
onLogout: {
//
store.send(.navigateToLogin)
MainFeature()
}
)
}