diff --git a/yana/APIs/APILogger.swift b/yana/APIs/APILogger.swift index 92193e3..9e45bb5 100644 --- a/yana/APIs/APILogger.swift +++ b/yana/APIs/APILogger.swift @@ -7,12 +7,18 @@ class APILogger { case basic case detailed } - - #if DEBUG - static let logLevel: LogLevel = .detailed - #else - static let logLevel: LogLevel = .none - #endif + + // 使用 actor 封装可变全局状态以保证并发安全 + actor Config { + static let shared = Config() +#if DEBUG + 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) @@ -90,13 +96,13 @@ class APILogger { body: Data?, finalHeaders: [String: String]? = nil ) { - #if DEBUG - guard logLevel != .none else { return } - #else + #if !DEBUG return - #endif - - logQueue.async { + #else + Task { + let level = await Config.shared.get() + guard level != .none else { return } + logQueue.async { let timestamp = dateFormatter.string(from: Date()) debugInfoSync("\n🚀 [API Request] [\(timestamp)] ==================") debugInfoSync("📍 Endpoint: \(request.endpoint)") @@ -106,13 +112,13 @@ class APILogger { // 显示最终的完整 headers(包括默认 headers 和自定义 headers) if let headers = finalHeaders, !headers.isEmpty { - if logLevel == .detailed { + if level == .detailed { debugInfoSync("📋 Final Headers (包括默认 + 自定义):") let masked = maskHeaders(headers) for (key, value) in masked.sorted(by: { $0.key < $1.key }) { debugInfoSync(" \(key): \(value)") } - } else if logLevel == .basic { + } else if level == .basic { debugInfoSync("📋 Headers: \(headers.count) 个 headers") // 只显示重要的 headers 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) debugInfoSync("📦 Request Body: \n\(pretty)") @@ -149,7 +155,7 @@ class APILogger { if request.includeBaseParameters { debugInfoSync("📱 Base Parameters: 已自动注入") } - } else if logLevel == .basic { + } else if level == .basic { let size = body?.count ?? 0 debugInfoSync("📦 Request Body: \(formatBytes(size))") @@ -159,18 +165,20 @@ class APILogger { } } debugInfoSync("=====================================") + } } + #endif } // MARK: - Response Logging static func logResponse(data: Data, response: HTTPURLResponse, duration: TimeInterval) { - #if DEBUG - guard logLevel != .none else { return } - #else + #if !DEBUG return - #endif - - logQueue.async { + #else + Task { + let level = await Config.shared.get() + guard level != .none else { return } + logQueue.async { let timestamp = dateFormatter.string(from: Date()) let statusEmoji = response.statusCode < 400 ? "✅" : "❌" debugInfoSync("\n\(statusEmoji) [API Response] [\(timestamp)] ===================") @@ -179,7 +187,7 @@ class APILogger { debugInfoSync("🔗 URL: \(response.url?.absoluteString ?? "Unknown")") debugInfoSync("📏 Data Size: \(formatBytes(data.count))") - if logLevel == .detailed { + if level == .detailed { debugInfoSync("📋 Response Headers:") // 将 headers 转为 [String:String] 后脱敏 var headers: [String: String] = [:] @@ -204,18 +212,20 @@ class APILogger { } } debugInfoSync("=====================================") + } } + #endif } // MARK: - Error Logging static func logError(_ error: Error, url: URL?, duration: TimeInterval) { - #if DEBUG - guard logLevel != .none else { return } - #else + #if !DEBUG return - #endif - - logQueue.async { + #else + Task { + let level = await Config.shared.get() + guard level != .none else { return } + logQueue.async { let timestamp = dateFormatter.string(from: Date()) debugErrorSync("\n❌ [API Error] [\(timestamp)] ======================") debugErrorSync("⏱️ Duration: \(String(format: "%.3f", duration))s") @@ -229,7 +239,7 @@ class APILogger { debugErrorSync("🚨 System Error: \(error.localizedDescription)") } - if logLevel == .detailed { + if level == .detailed { if let urlError = error as? URLError { debugInfoSync("🔍 URLError Code: \(urlError.code.rawValue)") debugInfoSync("🔍 URLError Localized: \(urlError.localizedDescription)") @@ -251,22 +261,26 @@ class APILogger { debugInfoSync("🔍 Full Error: \(error)") } debugErrorSync("=====================================\n") + } } + #endif } // MARK: - Decoded Response Logging static func logDecodedResponse(_ response: T, type: T.Type) { - #if DEBUG - guard logLevel == .detailed else { return } - #else + #if !DEBUG return - #endif - - logQueue.async { + #else + Task { + let level = await Config.shared.get() + guard level == .detailed else { return } + logQueue.async { let timestamp = dateFormatter.string(from: Date()) debugInfoSync("🎯 [Decoded Response] [\(timestamp)] Type: \(type)") debugInfoSync("=====================================\n") + } } + #endif } // MARK: - Helper Methods @@ -279,18 +293,20 @@ class APILogger { // MARK: - Performance Logging static func logPerformanceWarning(duration: TimeInterval, threshold: TimeInterval = 5.0) { - #if DEBUG - guard logLevel != .none && duration > threshold else { return } - #else + #if !DEBUG return - #endif - - logQueue.async { + #else + Task { + let level = await Config.shared.get() + guard level != .none && duration > threshold else { return } + logQueue.async { let timestamp = dateFormatter.string(from: Date()) debugWarnSync("\n⚠️ [Performance Warning] [\(timestamp)] ============") debugWarnSync("🐌 Request took \(String(format: "%.3f", duration))s (threshold: \(threshold)s)") debugWarnSync("💡 建议:检查网络条件或优化 API 响应") debugWarnSync("================================================\n") + } } + #endif } } diff --git a/yana/ContentView.swift b/yana/ContentView.swift index 6a8c2af..9946185 100644 --- a/yana/ContentView.swift +++ b/yana/ContentView.swift @@ -170,7 +170,14 @@ struct ContentView: View { let store: StoreOf let initStore: StoreOf let configStore: StoreOf - @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 var body: some View { @@ -188,7 +195,7 @@ struct ContentView: View { .tag(1) } .onChange(of: selectedLogLevel) { _, selectedLogLevel in - APILogger.logLevel = selectedLogLevel + Task { await APILogger.Config.shared.set(selectedLogLevel) } } } } diff --git a/yana/MVVM/CommonComponents.swift b/yana/MVVM/CommonComponents.swift index a1fa2f5..cc758dd 100644 --- a/yana/MVVM/CommonComponents.swift +++ b/yana/MVVM/CommonComponents.swift @@ -6,6 +6,89 @@ enum AppImageSource: Equatable { 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: - 背景视图组件 struct LoginBackgroundView: View { var body: some View { @@ -285,4 +368,4 @@ struct CameraPicker: UIViewControllerRepresentable { onTap: {} ) } -} \ No newline at end of file +} diff --git a/yana/MVVM/CreateFeedPage.swift b/yana/MVVM/CreateFeedPage.swift index e24da90..aa5ace7 100644 --- a/yana/MVVM/CreateFeedPage.swift +++ b/yana/MVVM/CreateFeedPage.swift @@ -6,17 +6,31 @@ final class CreateFeedViewModel: ObservableObject { @Published var selectedImages: [UIImage] = [] @Published var isPublishing: Bool = false @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 { @StateObject private var viewModel = CreateFeedViewModel() 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 { GeometryReader { _ in ZStack { - Color(hex: 0x0C0527).ignoresSafeArea() + Color(hex: 0x0C0527) + .ignoresSafeArea() + .onTapGesture { + // 点击背景收起键盘 + isTextEditorFocused = false + } VStack(spacing: 16) { HStack { Button(action: onDismiss) { @@ -58,10 +72,80 @@ struct CreateFeedPage: View { .padding(.horizontal, 12) .padding(.vertical, 8) .scrollContentBackground(.hidden) + .focused($isTextEditorFocused) .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) .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 { 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) + } + } +} \ No newline at end of file diff --git a/yana/MVVM/MainPage.swift b/yana/MVVM/MainPage.swift index 0849d16..dcc51c1 100644 --- a/yana/MVVM/MainPage.swift +++ b/yana/MVVM/MainPage.swift @@ -23,11 +23,25 @@ struct MainPage: View { topRightButton } Spacer() - // 底部导航栏 - bottomTabView - .frame(height: 80) - .padding(.horizontal, 24) - .padding(.bottom, 100) + // 底部导航栏(组件化) + BottomTabBar( + items: [ + TabBarItem(id: MainViewModel.Tab.feed.rawValue, title: MainViewModel.Tab.feed.title, systemIconName: MainViewModel.Tab.feed.iconName), + 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: - 右上角按钮 private var topRightButton: some View { diff --git a/yana/Views/AppRootView.swift b/yana/Views/AppRootView.swift deleted file mode 100644 index 8c24b1a..0000000 --- a/yana/Views/AppRootView.swift +++ /dev/null @@ -1,63 +0,0 @@ -import SwiftUI -import ComposableArchitecture - -struct AppRootView: View { - @State private var isLoggedIn = false - @State private var mainStore: StoreOf? - - 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 { - debugInfoSync("🏗️ AppRootView: 创建新的MainStore实例") - return Store( - initialState: MainFeature.State() - ) { - MainFeature() - } - } -} -// -//#Preview { -// AppRootView() -//} diff --git a/yana/Views/MainView.swift b/yana/Views/MainView.swift deleted file mode 100644 index c0e77cf..0000000 --- a/yana/Views/MainView.swift +++ /dev/null @@ -1,152 +0,0 @@ -import SwiftUI -import ComposableArchitecture - -struct MainView: View { - let store: StoreOf - 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 - @State private var path: [MainFeature.Destination] = [] - init(store: StoreOf) { - 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 - - 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 - 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: {}) - } - } - } - } -}