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) } } }