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