Files
e-party-iOS/yana/MVVM/CreateFeedPage.swift
edwinQQQ 90a840c5f3 feat: 更新日志级别管理及底部导航栏组件化
- 在ContentView中根据编译模式初始化日志级别,确保调试信息的灵活性。
- 在APILogger中使用actor封装日志级别,增强并发安全性。
- 新增通用底部Tab栏组件,优化MainPage中的底部导航逻辑,提升代码可维护性。
- 移除冗余的AppRootView和MainView,简化视图结构,提升代码整洁性。
2025-09-18 16:12:18 +08:00

210 lines
9.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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