
- 在ContentView中根据编译模式初始化日志级别,确保调试信息的灵活性。 - 在APILogger中使用actor封装日志级别,增强并发安全性。 - 新增通用底部Tab栏组件,优化MainPage中的底部导航逻辑,提升代码可维护性。 - 移除冗余的AppRootView和MainView,简化视图结构,提升代码整洁性。
210 lines
9.4 KiB
Swift
210 lines
9.4 KiB
Swift
import SwiftUI
|
||
|
||
@MainActor
|
||
final class CreateFeedViewModel: ObservableObject {
|
||
@Published var content: String = ""
|
||
@Published var selectedImages: [UIImage] = []
|
||
@Published var isPublishing: Bool = false
|
||
@Published var errorMessage: String? = nil
|
||
// 仅当有文本时才允许发布
|
||
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()
|
||
.onTapGesture {
|
||
// 点击背景收起键盘
|
||
isTextEditorFocused = false
|
||
}
|
||
VStack(spacing: 16) {
|
||
HStack {
|
||
Button(action: onDismiss) {
|
||
Image(systemName: "xmark")
|
||
.foregroundColor(.white)
|
||
.font(.system(size: 18, weight: .medium))
|
||
}
|
||
Spacer()
|
||
Text(LocalizedString("createFeed.title", comment: "Image & Text Publish"))
|
||
.foregroundColor(.white)
|
||
.font(.system(size: 18, weight: .medium))
|
||
Spacer()
|
||
Button(action: publish) {
|
||
if viewModel.isPublishing {
|
||
ProgressView()
|
||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||
} else {
|
||
Text(LocalizedString("createFeed.publish", comment: "Publish"))
|
||
.foregroundColor(.white)
|
||
.font(.system(size: 14, weight: .medium))
|
||
}
|
||
}
|
||
.disabled(!viewModel.canPublish || viewModel.isPublishing)
|
||
.opacity((!viewModel.canPublish || viewModel.isPublishing) ? 0.6 : 1)
|
||
}
|
||
.padding(.horizontal, 16)
|
||
.padding(.top, 12)
|
||
|
||
ZStack(alignment: .topLeading) {
|
||
RoundedRectangle(cornerRadius: 8).fill(Color(hex: 0x1C143A))
|
||
if viewModel.content.isEmpty {
|
||
Text(LocalizedString("createFeed.enterContent", comment: "Enter Content"))
|
||
.foregroundColor(.white.opacity(0.5))
|
||
.padding(.horizontal, 16)
|
||
.padding(.vertical, 12)
|
||
}
|
||
TextEditor(text: $viewModel.content)
|
||
.foregroundColor(.white)
|
||
.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)
|
||
.foregroundColor(.red)
|
||
.font(.system(size: 14))
|
||
}
|
||
|
||
Spacer()
|
||
}
|
||
}
|
||
}
|
||
.navigationBarBackButtonHidden(true)
|
||
}
|
||
|
||
private func publish() {
|
||
viewModel.isPublishing = true
|
||
Task { @MainActor in
|
||
try? await Task.sleep(nanoseconds: 500_000_000)
|
||
viewModel.isPublishing = false
|
||
onDismiss()
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
}
|
||
} |