feat: 更新日志级别管理及底部导航栏组件化

- 在ContentView中根据编译模式初始化日志级别,确保调试信息的灵活性。
- 在APILogger中使用actor封装日志级别,增强并发安全性。
- 新增通用底部Tab栏组件,优化MainPage中的底部导航逻辑,提升代码可维护性。
- 移除冗余的AppRootView和MainView,简化视图结构,提升代码整洁性。
This commit is contained in:
edwinQQQ
2025-09-18 16:12:18 +08:00
parent 8b4eb9cb7e
commit 90a840c5f3
7 changed files with 295 additions and 297 deletions

View File

@@ -7,12 +7,18 @@ class APILogger {
case basic case basic
case detailed case detailed
} }
#if DEBUG // 使 actor
static let logLevel: LogLevel = .detailed actor Config {
#else static let shared = Config()
static let logLevel: LogLevel = .none #if DEBUG
#endif private var level: LogLevel = .detailed
#else
private var level: LogLevel = .none
#endif
func get() -> LogLevel { level }
func set(_ newLevel: LogLevel) { level = newLevel }
}
private static let logQueue = DispatchQueue(label: "com.yana.api.logger", qos: .utility) private static let logQueue = DispatchQueue(label: "com.yana.api.logger", qos: .utility)
@@ -90,13 +96,13 @@ class APILogger {
body: Data?, body: Data?,
finalHeaders: [String: String]? = nil finalHeaders: [String: String]? = nil
) { ) {
#if DEBUG #if !DEBUG
guard logLevel != .none else { return }
#else
return return
#endif #else
Task {
logQueue.async { let level = await Config.shared.get()
guard level != .none else { return }
logQueue.async {
let timestamp = dateFormatter.string(from: Date()) let timestamp = dateFormatter.string(from: Date())
debugInfoSync("\n🚀 [API Request] [\(timestamp)] ==================") debugInfoSync("\n🚀 [API Request] [\(timestamp)] ==================")
debugInfoSync("📍 Endpoint: \(request.endpoint)") debugInfoSync("📍 Endpoint: \(request.endpoint)")
@@ -106,13 +112,13 @@ class APILogger {
// headers headers headers // headers headers headers
if let headers = finalHeaders, !headers.isEmpty { if let headers = finalHeaders, !headers.isEmpty {
if logLevel == .detailed { if level == .detailed {
debugInfoSync("📋 Final Headers (包括默认 + 自定义):") debugInfoSync("📋 Final Headers (包括默认 + 自定义):")
let masked = maskHeaders(headers) let masked = maskHeaders(headers)
for (key, value) in masked.sorted(by: { $0.key < $1.key }) { for (key, value) in masked.sorted(by: { $0.key < $1.key }) {
debugInfoSync(" \(key): \(value)") debugInfoSync(" \(key): \(value)")
} }
} else if logLevel == .basic { } else if level == .basic {
debugInfoSync("📋 Headers: \(headers.count) 个 headers") debugInfoSync("📋 Headers: \(headers.count) 个 headers")
// headers // headers
let importantHeaders = ["Content-Type", "Accept", "User-Agent", "Authorization"] let importantHeaders = ["Content-Type", "Accept", "User-Agent", "Authorization"]
@@ -141,7 +147,7 @@ class APILogger {
} }
} }
if logLevel == .detailed { if level == .detailed {
let pretty = maskedBodyString(from: body) let pretty = maskedBodyString(from: body)
debugInfoSync("📦 Request Body: \n\(pretty)") debugInfoSync("📦 Request Body: \n\(pretty)")
@@ -149,7 +155,7 @@ class APILogger {
if request.includeBaseParameters { if request.includeBaseParameters {
debugInfoSync("📱 Base Parameters: 已自动注入") debugInfoSync("📱 Base Parameters: 已自动注入")
} }
} else if logLevel == .basic { } else if level == .basic {
let size = body?.count ?? 0 let size = body?.count ?? 0
debugInfoSync("📦 Request Body: \(formatBytes(size))") debugInfoSync("📦 Request Body: \(formatBytes(size))")
@@ -159,18 +165,20 @@ class APILogger {
} }
} }
debugInfoSync("=====================================") debugInfoSync("=====================================")
}
} }
#endif
} }
// MARK: - Response Logging // MARK: - Response Logging
static func logResponse(data: Data, response: HTTPURLResponse, duration: TimeInterval) { static func logResponse(data: Data, response: HTTPURLResponse, duration: TimeInterval) {
#if DEBUG #if !DEBUG
guard logLevel != .none else { return }
#else
return return
#endif #else
Task {
logQueue.async { let level = await Config.shared.get()
guard level != .none else { return }
logQueue.async {
let timestamp = dateFormatter.string(from: Date()) let timestamp = dateFormatter.string(from: Date())
let statusEmoji = response.statusCode < 400 ? "" : "" let statusEmoji = response.statusCode < 400 ? "" : ""
debugInfoSync("\n\(statusEmoji) [API Response] [\(timestamp)] ===================") debugInfoSync("\n\(statusEmoji) [API Response] [\(timestamp)] ===================")
@@ -179,7 +187,7 @@ class APILogger {
debugInfoSync("🔗 URL: \(response.url?.absoluteString ?? "Unknown")") debugInfoSync("🔗 URL: \(response.url?.absoluteString ?? "Unknown")")
debugInfoSync("📏 Data Size: \(formatBytes(data.count))") debugInfoSync("📏 Data Size: \(formatBytes(data.count))")
if logLevel == .detailed { if level == .detailed {
debugInfoSync("📋 Response Headers:") debugInfoSync("📋 Response Headers:")
// headers [String:String] // headers [String:String]
var headers: [String: String] = [:] var headers: [String: String] = [:]
@@ -204,18 +212,20 @@ class APILogger {
} }
} }
debugInfoSync("=====================================") debugInfoSync("=====================================")
}
} }
#endif
} }
// MARK: - Error Logging // MARK: - Error Logging
static func logError(_ error: Error, url: URL?, duration: TimeInterval) { static func logError(_ error: Error, url: URL?, duration: TimeInterval) {
#if DEBUG #if !DEBUG
guard logLevel != .none else { return }
#else
return return
#endif #else
Task {
logQueue.async { let level = await Config.shared.get()
guard level != .none else { return }
logQueue.async {
let timestamp = dateFormatter.string(from: Date()) let timestamp = dateFormatter.string(from: Date())
debugErrorSync("\n❌ [API Error] [\(timestamp)] ======================") debugErrorSync("\n❌ [API Error] [\(timestamp)] ======================")
debugErrorSync("⏱️ Duration: \(String(format: "%.3f", duration))s") debugErrorSync("⏱️ Duration: \(String(format: "%.3f", duration))s")
@@ -229,7 +239,7 @@ class APILogger {
debugErrorSync("🚨 System Error: \(error.localizedDescription)") debugErrorSync("🚨 System Error: \(error.localizedDescription)")
} }
if logLevel == .detailed { if level == .detailed {
if let urlError = error as? URLError { if let urlError = error as? URLError {
debugInfoSync("🔍 URLError Code: \(urlError.code.rawValue)") debugInfoSync("🔍 URLError Code: \(urlError.code.rawValue)")
debugInfoSync("🔍 URLError Localized: \(urlError.localizedDescription)") debugInfoSync("🔍 URLError Localized: \(urlError.localizedDescription)")
@@ -251,22 +261,26 @@ class APILogger {
debugInfoSync("🔍 Full Error: \(error)") debugInfoSync("🔍 Full Error: \(error)")
} }
debugErrorSync("=====================================\n") debugErrorSync("=====================================\n")
}
} }
#endif
} }
// MARK: - Decoded Response Logging // MARK: - Decoded Response Logging
static func logDecodedResponse<T>(_ response: T, type: T.Type) { static func logDecodedResponse<T>(_ response: T, type: T.Type) {
#if DEBUG #if !DEBUG
guard logLevel == .detailed else { return }
#else
return return
#endif #else
Task {
logQueue.async { let level = await Config.shared.get()
guard level == .detailed else { return }
logQueue.async {
let timestamp = dateFormatter.string(from: Date()) let timestamp = dateFormatter.string(from: Date())
debugInfoSync("🎯 [Decoded Response] [\(timestamp)] Type: \(type)") debugInfoSync("🎯 [Decoded Response] [\(timestamp)] Type: \(type)")
debugInfoSync("=====================================\n") debugInfoSync("=====================================\n")
}
} }
#endif
} }
// MARK: - Helper Methods // MARK: - Helper Methods
@@ -279,18 +293,20 @@ class APILogger {
// MARK: - Performance Logging // MARK: - Performance Logging
static func logPerformanceWarning(duration: TimeInterval, threshold: TimeInterval = 5.0) { static func logPerformanceWarning(duration: TimeInterval, threshold: TimeInterval = 5.0) {
#if DEBUG #if !DEBUG
guard logLevel != .none && duration > threshold else { return }
#else
return return
#endif #else
Task {
logQueue.async { let level = await Config.shared.get()
guard level != .none && duration > threshold else { return }
logQueue.async {
let timestamp = dateFormatter.string(from: Date()) let timestamp = dateFormatter.string(from: Date())
debugWarnSync("\n⚠️ [Performance Warning] [\(timestamp)] ============") debugWarnSync("\n⚠️ [Performance Warning] [\(timestamp)] ============")
debugWarnSync("🐌 Request took \(String(format: "%.3f", duration))s (threshold: \(threshold)s)") debugWarnSync("🐌 Request took \(String(format: "%.3f", duration))s (threshold: \(threshold)s)")
debugWarnSync("💡 建议:检查网络条件或优化 API 响应") debugWarnSync("💡 建议:检查网络条件或优化 API 响应")
debugWarnSync("================================================\n") debugWarnSync("================================================\n")
}
} }
#endif
} }
} }

View File

@@ -170,7 +170,14 @@ struct ContentView: View {
let store: StoreOf<LoginFeature> let store: StoreOf<LoginFeature>
let initStore: StoreOf<InitFeature> let initStore: StoreOf<InitFeature>
let configStore: StoreOf<ConfigFeature> let configStore: StoreOf<ConfigFeature>
@State private var selectedLogLevel: APILogger.LogLevel = APILogger.logLevel @State private var selectedLogLevel: APILogger.LogLevel = {
// APILogger.Config
#if DEBUG
return .detailed
#else
return .none
#endif
}()
@State private var selectedTab = 0 @State private var selectedTab = 0
var body: some View { var body: some View {
@@ -188,7 +195,7 @@ struct ContentView: View {
.tag(1) .tag(1)
} }
.onChange(of: selectedLogLevel) { _, selectedLogLevel in .onChange(of: selectedLogLevel) { _, selectedLogLevel in
APILogger.logLevel = selectedLogLevel Task { await APILogger.Config.shared.set(selectedLogLevel) }
} }
} }
} }

View File

@@ -6,6 +6,89 @@ enum AppImageSource: Equatable {
case photoLibrary case photoLibrary
} }
// MARK: - Tab
public struct TabBarItem: Identifiable, Equatable {
public let id: String
public let title: String
public let systemIconName: String
public init(id: String, title: String, systemIconName: String) {
self.id = id
self.title = title
self.systemIconName = systemIconName
}
}
struct BottomTabBar: View {
let items: [TabBarItem]
@Binding var selectedId: String
let onSelect: (String) -> Void
var contentPadding: EdgeInsets = EdgeInsets(top: 12, leading: 0, bottom: 12, trailing: 0)
var horizontalPadding: CGFloat = 0
var body: some View {
HStack(spacing: 0) {
ForEach(items) { item in
Button(action: {
selectedId = item.id
onSelect(item.id)
}) {
VStack(spacing: 4) {
Image(systemName: item.systemIconName)
.font(.system(size: 24))
.foregroundColor(selectedId == item.id ? .white : .white.opacity(0.6))
Text(item.title)
.font(.system(size: 12))
.foregroundColor(selectedId == item.id ? .white : .white.opacity(0.6))
}
}
.frame(maxWidth: .infinity)
.padding(contentPadding)
}
}
.padding(.horizontal, horizontalPadding)
.background(LiquidGlassBackground())
.clipShape(Capsule())
.overlay(
Capsule()
.stroke(Color.white.opacity(0.12), lineWidth: 0.5)
)
.shadow(color: Color.black.opacity(0.2), radius: 12, x: 0, y: 6)
.safeAreaInset(edge: .bottom) {
Color.clear.frame(height: 0)
}
}
}
// MARK: - Liquid Glass Background (iOS 26 )
struct LiquidGlassBackground: View {
var body: some View {
Group {
if #available(iOS 26.0, *) {
// iOS 26+使
Rectangle()
.fill(Color.clear)
.glassEffect()
} else
if #available(iOS 17.0, *) {
// iOS 17-25使 +
ZStack {
Rectangle().fill(.ultraThinMaterial)
LinearGradient(
colors: [Color.white.opacity(0.06), Color.clear, Color.white.opacity(0.04)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.blendMode(.softLight)
}
} else {
//
Rectangle()
.fill(Color.black.opacity(0.2))
}
}
}
}
// MARK: - // MARK: -
struct LoginBackgroundView: View { struct LoginBackgroundView: View {
var body: some View { var body: some View {
@@ -285,4 +368,4 @@ struct CameraPicker: UIViewControllerRepresentable {
onTap: {} onTap: {}
) )
} }
} }

View File

@@ -6,17 +6,31 @@ final class CreateFeedViewModel: ObservableObject {
@Published var selectedImages: [UIImage] = [] @Published var selectedImages: [UIImage] = []
@Published var isPublishing: Bool = false @Published var isPublishing: Bool = false
@Published var errorMessage: String? = nil @Published var errorMessage: String? = nil
var canPublish: Bool { !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || !selectedImages.isEmpty } //
var canPublish: Bool { !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
} }
struct CreateFeedPage: View { struct CreateFeedPage: View {
@StateObject private var viewModel = CreateFeedViewModel() @StateObject private var viewModel = CreateFeedViewModel()
let onDismiss: () -> Void let onDismiss: () -> Void
// MARK: - UI State
@FocusState private var isTextEditorFocused: Bool
@State private var isShowingSourceSheet: Bool = false
@State private var isShowingImagePicker: Bool = false
@State private var imagePickerSource: UIImagePickerController.SourceType = .photoLibrary
private let maxCharacters: Int = 500
var body: some View { var body: some View {
GeometryReader { _ in GeometryReader { _ in
ZStack { ZStack {
Color(hex: 0x0C0527).ignoresSafeArea() Color(hex: 0x0C0527)
.ignoresSafeArea()
.onTapGesture {
//
isTextEditorFocused = false
}
VStack(spacing: 16) { VStack(spacing: 16) {
HStack { HStack {
Button(action: onDismiss) { Button(action: onDismiss) {
@@ -58,10 +72,80 @@ struct CreateFeedPage: View {
.padding(.horizontal, 12) .padding(.horizontal, 12)
.padding(.vertical, 8) .padding(.vertical, 8)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.focused($isTextEditorFocused)
.frame(height: 200) .frame(height: 200)
//
VStack { Spacer() }
.overlay(alignment: .bottomTrailing) {
Text("\(viewModel.content.count)/\(maxCharacters)")
.foregroundColor(.white.opacity(0.6))
.font(.system(size: 14))
.padding(.trailing, 8)
.padding(.bottom, 8)
}
} }
.frame(height: 200) .frame(height: 200)
.padding(.horizontal, 20) .padding(.horizontal, 20)
.onChange(of: viewModel.content) { newValue in
//
if newValue.count > maxCharacters {
viewModel.content = String(newValue.prefix(maxCharacters))
}
}
//
HStack(alignment: .top, spacing: 12) {
Button {
isShowingSourceSheet = true
} label: {
ZStack {
RoundedRectangle(cornerRadius: 16)
.fill(Color(hex: 0x1C143A))
.frame(width: 180, height: 180)
Image(systemName: "plus")
.foregroundColor(.white.opacity(0.6))
.font(.system(size: 36, weight: .semibold))
}
}
.buttonStyle(.plain)
//
if !viewModel.selectedImages.isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(viewModel.selectedImages.indices, id: \.self) { index in
Image(uiImage: viewModel.selectedImages[index])
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(RoundedRectangle(cornerRadius: 12))
.clipped()
}
}
}
.frame(height: 180)
}
}
.padding(.horizontal, 20)
.confirmationDialog(LocalizedString("createFeed.chooseSource", comment: "Choose Source"), isPresented: $isShowingSourceSheet, titleVisibility: .visible) {
Button(LocalizedString("createFeed.source.album", comment: "Photo Library")) {
imagePickerSource = .photoLibrary
isShowingImagePicker = true
}
Button(LocalizedString("createFeed.source.camera", comment: "Camera")) {
if UIImagePickerController.isSourceTypeAvailable(.camera) {
imagePickerSource = .camera
isShowingImagePicker = true
}
}
Button(LocalizedString("common.cancel", comment: "Cancel"), role: .cancel) {}
}
.sheet(isPresented: $isShowingImagePicker) {
ImagePicker(sourceType: imagePickerSource) { image in
viewModel.selectedImages.append(image)
}
}
if let error = viewModel.errorMessage { if let error = viewModel.errorMessage {
Text(error) Text(error)
@@ -86,4 +170,41 @@ struct CreateFeedPage: View {
} }
} }
// MARK: - UIKit Image Picker Wrapper
private struct ImagePicker: UIViewControllerRepresentable {
let sourceType: UIImagePickerController.SourceType
let onImagePicked: (UIImage) -> Void
func makeCoordinator() -> Coordinator {
Coordinator(onImagePicked: onImagePicked)
}
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = sourceType
picker.allowsEditing = false
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let onImagePicked: (UIImage) -> Void
init(onImagePicked: @escaping (UIImage) -> Void) {
self.onImagePicked = onImagePicked
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = (info[.originalImage] as? UIImage) ?? (info[.editedImage] as? UIImage) {
onImagePicked(image)
}
picker.dismiss(animated: true)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
}
}
}

View File

@@ -23,11 +23,25 @@ struct MainPage: View {
topRightButton topRightButton
} }
Spacer() Spacer()
// //
bottomTabView BottomTabBar(
.frame(height: 80) items: [
.padding(.horizontal, 24) TabBarItem(id: MainViewModel.Tab.feed.rawValue, title: MainViewModel.Tab.feed.title, systemIconName: MainViewModel.Tab.feed.iconName),
.padding(.bottom, 100) TabBarItem(id: MainViewModel.Tab.me.rawValue, title: MainViewModel.Tab.me.title, systemIconName: MainViewModel.Tab.me.iconName)
],
selectedId: Binding(
get: { viewModel.selectedTab.rawValue },
set: { raw in
if let tab = MainViewModel.Tab(rawValue: raw) {
viewModel.onTabChanged(tab)
}
}
),
onSelect: { _ in }
)
.frame(height: 80)
.padding(.horizontal, 24)
.padding(.bottom, 100)
} }
} }
} }
@@ -83,35 +97,7 @@ struct MainPage: View {
} }
} }
private var bottomTabView: some View { //
HStack(spacing: 0) {
ForEach(MainViewModel.Tab.allCases, id: \.self) { tab in
Button(action: {
viewModel.onTabChanged(tab)
}) {
VStack(spacing: 4) {
Image(systemName: tab.iconName)
.font(.system(size: 24))
.foregroundColor(viewModel.selectedTab == tab ? .white : .white.opacity(0.6))
Text(tab.title)
.font(.system(size: 12))
.foregroundColor(viewModel.selectedTab == tab ? .white : .white.opacity(0.6))
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
}
}
.background(
Rectangle()
.fill(Color.black.opacity(0.3))
.background(.ultraThinMaterial)
)
.safeAreaInset(edge: .bottom) {
Color.clear.frame(height: 0)
}
}
// MARK: - // MARK: -
private var topRightButton: some View { private var topRightButton: some View {

View File

@@ -1,63 +0,0 @@
import SwiftUI
import ComposableArchitecture
struct AppRootView: View {
@State private var isLoggedIn = false
@State private var mainStore: StoreOf<MainFeature>?
var body: some View {
Group {
if isLoggedIn {
if let mainStore = mainStore {
MainView(store: mainStore)
.onAppear {
debugInfoSync("🔄 AppRootView: 使用已存在的MainStore")
}
} else {
// store
let store = createMainStore()
MainView(store: store)
.onAppear {
debugInfoSync("💾 AppRootView: MainStore已创建并保存")
// onAppearstore
DispatchQueue.main.async {
self.mainStore = store
}
}
}
} else {
LoginView(
store: Store(
initialState: LoginFeature.State()
) {
LoginFeature()
},
onLoginSuccess: {
debugInfoSync("🔐 AppRootView: 登录成功准备创建MainStore")
isLoggedIn = true
// store
mainStore = createMainStore()
}
)
}
}
.onAppear {
debugInfoSync("🚀 AppRootView onAppear")
debugInfoSync(" isLoggedIn: \(isLoggedIn)")
debugInfoSync(" mainStore存在: \(mainStore != nil)")
}
}
private func createMainStore() -> StoreOf<MainFeature> {
debugInfoSync("🏗️ AppRootView: 创建新的MainStore实例")
return Store(
initialState: MainFeature.State()
) {
MainFeature()
}
}
}
//
//#Preview {
// AppRootView()
//}

View File

@@ -1,152 +0,0 @@
import SwiftUI
import ComposableArchitecture
struct MainView: View {
let store: StoreOf<MainFeature>
var onLogout: (() -> Void)? = nil
var body: some View {
WithPerceptionTracking {
InternalMainView(store: store)
.onChange(of: store.isLoggedOut) { _, isLoggedOut in
if isLoggedOut {
onLogout?()
}
}
}
}
}
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 {
WithPerceptionTracking {
NavigationStack(path: $path) {
GeometryReader { geometry in
mainContentView(geometry: geometry)
.navigationDestination(for: MainFeature.Destination.self) { destination in
DestinationView(destination: destination, store: self.store)
}
.onChange(of: path) { _, path in
store.send(.navigationPathChanged(path))
}
.onChange(of: store.navigationPath) { _, navigationPath in
if path != navigationPath {
path = navigationPath
}
}
.onAppear {
debugInfoSync("🚀 MainView onAppear")
debugInfoSync(" 当前selectedTab: \(store.selectedTab)")
store.send(.onAppear)
}
}
}
}
}
struct DestinationView: View {
let destination: MainFeature.Destination
let store: StoreOf<MainFeature>
var body: some View {
switch destination {
case .appSetting:
IfLetStore(
store.scope(state: \.appSettingState, action: \.appSettingAction),
then: { store in
WithPerceptionTracking {
AppSettingView(store: store)
}
},
else: { Text("appSettingState is nil") }
)
case .testView:
TestView()
}
}
}
private func mainContentView(geometry: GeometryProxy) -> some View {
WithPerceptionTracking {
ZStack {
//
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.clipped()
.ignoresSafeArea(.all)
//
MainContentView(
store: store,
selectedTab: store.selectedTab
)
.onChange(of: store.selectedTab) { _, newTab in
debugInfoSync("🔄 MainView selectedTab changed: \(newTab)")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.bottom, 80) //
// -
VStack {
Spacer()
BottomTabView(selectedTab: Binding(
get: {
// MainFeature.TabBottomTabView.Tab
let currentTab = store.selectedTab == .feed ? Tab.feed : Tab.me
debugInfoSync("🔍 BottomTabView get: MainFeature.Tab.\(store.selectedTab) → BottomTabView.Tab.\(currentTab)")
return currentTab
},
set: { newTab in
// BottomTabView.TabMainFeature.Tab
let mainTab: MainFeature.Tab = newTab == .feed ? .feed : .other
debugInfoSync("🔍 BottomTabView set: BottomTabView.Tab.\(newTab) → MainFeature.Tab.\(mainTab)")
store.send(.selectTab(mainTab))
}
))
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.padding(.bottom, 100)
.ignoresSafeArea(.keyboard, edges: .bottom)
// API Loading
APILoadingEffectView()
}
}
}
}
struct MainContentView: View {
let store: StoreOf<MainFeature>
let selectedTab: MainFeature.Tab
var body: some View {
WithPerceptionTracking {
let _ = debugInfoSync("📱 MainContentView selectedTab: \(selectedTab)")
let _ = debugInfoSync(" 与store.selectedTab一致: \(selectedTab == store.selectedTab)")
Group {
if selectedTab == .feed {
FeedListView(store: store.scope(
state: \.feedList,
action: \.feedList
))
} else if selectedTab == .other {
MeView(
store: store.scope(
state: \.me,
action: \.me
),
showCloseButton: false // MainView
)
} else {
CustomEmptyView(onRetry: {})
}
}
}
}
}