feat: 更新日志级别管理及底部导航栏组件化
- 在ContentView中根据编译模式初始化日志级别,确保调试信息的灵活性。 - 在APILogger中使用actor封装日志级别,增强并发安全性。 - 新增通用底部Tab栏组件,优化MainPage中的底部导航逻辑,提升代码可维护性。 - 移除冗余的AppRootView和MainView,简化视图结构,提升代码整洁性。
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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: {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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 {
|
||||||
|
@@ -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已创建并保存")
|
|
||||||
// 确保在onAppear中保存store
|
|
||||||
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()
|
|
||||||
//}
|
|
@@ -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.Tab转换为BottomTabView.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.Tab转换为MainFeature.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: {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user