feat: 添加CreateFeed功能及相关视图组件
- 新增CreateFeedView和CreateFeedFeature,支持用户发布图文动态。 - 在FeedView中集成CreateFeedView,允许用户通过加号按钮访问发布界面。 - 实现图片选择和文本输入功能,支持最多9张图片的上传。 - 添加发布API请求模型,处理动态发布逻辑。 - 更新FeedFeature以管理CreateFeedView的显示状态,确保用户体验流畅。 - 完善UI结构分析与执行计划文档,明确开发步骤和技术要点。
This commit is contained in:
79
CreateFeedView-Analysis.md
Normal file
79
CreateFeedView-Analysis.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# CreateFeedView UI 结构分析与执行计划
|
||||
|
||||
## UI 结构分析
|
||||
|
||||
根据设计稿,CreateFeedView 应包含以下UI元素:
|
||||
|
||||
### 1. 顶部导航栏
|
||||
- 左侧:返回按钮
|
||||
- 中间:"图文发布" 标题
|
||||
- 右侧:"发布" 按钮
|
||||
|
||||
### 2. 主要内容区域
|
||||
- 文本输入框:"Enter Content" 占位符,支持多行输入,最大500字符
|
||||
- 字符计数显示:"0/500" 格式
|
||||
- 图片添加区域:
|
||||
- 默认显示一个 "+" 按钮(使用 "add photo" 图片资源)
|
||||
- 支持添加最多9张图片
|
||||
- 图片以网格形式排列
|
||||
- 每张图片可以删除
|
||||
|
||||
### 3. 底部发布按钮
|
||||
- 紫色渐变背景的"发布"按钮
|
||||
- 占据屏幕底部,固定位置
|
||||
|
||||
## 执行计划
|
||||
|
||||
### 第一步:创建 CreateFeedFeature
|
||||
- 定义状态管理结构
|
||||
- 实现文本输入、图片选择、发布等Action
|
||||
- 添加表单验证逻辑
|
||||
- 集成图片选择器
|
||||
|
||||
### 第二步:创建 CreateFeedView
|
||||
- 实现顶部导航栏
|
||||
- 创建文本输入区域
|
||||
- 实现图片选择和展示网格
|
||||
- 添加发布按钮
|
||||
- 应用深色主题样式
|
||||
|
||||
### 第三步:集成到 FeedView
|
||||
- 修改 FeedView 中的加号按钮点击事件
|
||||
- 添加导航到 CreateFeedView 的逻辑
|
||||
- 确保返回时能刷新动态列表
|
||||
|
||||
### 第四步:创建发布API模型
|
||||
- 定义发布动态的请求和响应模型
|
||||
- 添加API端点定义
|
||||
- 实现发布逻辑(模拟或真实API)
|
||||
|
||||
### 第五步:测试和优化
|
||||
- 测试各种输入场景
|
||||
- 验证图片选择和预览功能
|
||||
- 确保UI响应和交互流畅
|
||||
|
||||
## 技术要点
|
||||
|
||||
1. **状态管理**:使用 ComposableArchitecture 模式
|
||||
2. **图片选择**:使用 PhotosUI 框架
|
||||
3. **UI样式**:保持与现有深色主题一致
|
||||
4. **表单验证**:实时字符计数和输入限制
|
||||
5. **导航管理**:使用 NavigationStack 或 sheet 展示
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
yana/
|
||||
├── Features/
|
||||
│ └── CreateFeedFeature.swift # 新建
|
||||
├── Views/
|
||||
│ └── CreateFeedView.swift # 新建
|
||||
├── APIs/
|
||||
│ ├── APIEndpoints.swift # 修改:添加发布端点
|
||||
│ └── DynamicsModels.swift # 修改:添加发布模型
|
||||
└── Assets.xcassets/
|
||||
└── Home/
|
||||
└── add photo.imageset/ # 已存在
|
||||
```
|
||||
|
||||
开始实施第一步:创建 CreateFeedFeature。
|
223
yana/Features/CreateFeedFeature.swift
Normal file
223
yana/Features/CreateFeedFeature.swift
Normal file
@@ -0,0 +1,223 @@
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
import SwiftUI
|
||||
|
||||
// 条件导入 PhotosUI (iOS 16.0+)
|
||||
#if canImport(PhotosUI)
|
||||
import PhotosUI
|
||||
#endif
|
||||
|
||||
@Reducer
|
||||
struct CreateFeedFeature {
|
||||
@ObservableState
|
||||
struct State: Equatable {
|
||||
var content: String = ""
|
||||
var processedImages: [UIImage] = []
|
||||
var isLoading: Bool = false
|
||||
var errorMessage: String? = nil
|
||||
var characterCount: Int = 0
|
||||
|
||||
// iOS 16+ PhotosPicker 支持
|
||||
#if canImport(PhotosUI) && swift(>=5.7)
|
||||
var selectedImages: [PhotosPickerItem] = []
|
||||
#endif
|
||||
|
||||
// iOS 15 UIImagePickerController 支持
|
||||
var showingImagePicker: Bool = false
|
||||
|
||||
|
||||
var canAddMoreImages: Bool {
|
||||
processedImages.count < 9
|
||||
}
|
||||
|
||||
var canPublish: Bool {
|
||||
!content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isLoading
|
||||
}
|
||||
}
|
||||
|
||||
enum Action {
|
||||
case contentChanged(String)
|
||||
case publishButtonTapped
|
||||
case publishResponse(Result<PublishDynamicResponse, Error>)
|
||||
case clearError
|
||||
case dismissView
|
||||
|
||||
// iOS 16+ PhotosPicker Actions
|
||||
#if canImport(PhotosUI) && swift(>=5.7)
|
||||
case photosPickerItemsChanged([PhotosPickerItem])
|
||||
case processPhotosPickerItems([PhotosPickerItem])
|
||||
#endif
|
||||
|
||||
// iOS 15 UIImagePickerController Actions
|
||||
case showImagePicker
|
||||
case hideImagePicker
|
||||
case imageSelected(UIImage)
|
||||
|
||||
case removeImage(Int)
|
||||
}
|
||||
|
||||
@Dependency(\.apiService) var apiService
|
||||
@Dependency(\.dismiss) var dismiss
|
||||
|
||||
var body: some ReducerOf<Self> {
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
case .contentChanged(let newContent):
|
||||
state.content = newContent
|
||||
state.characterCount = newContent.count
|
||||
return .none
|
||||
|
||||
#if canImport(PhotosUI) && swift(>=5.7)
|
||||
case .photosPickerItemsChanged(let items):
|
||||
state.selectedImages = items
|
||||
return .run { send in
|
||||
await send(.processPhotosPickerItems(items))
|
||||
}
|
||||
|
||||
case .processPhotosPickerItems(let items):
|
||||
return .run { [currentImages = state.processedImages] send in
|
||||
var newImages = currentImages
|
||||
|
||||
for item in items {
|
||||
if let data = try? await item.loadTransferable(type: Data.self),
|
||||
let image = UIImage(data: data) {
|
||||
if newImages.count < 9 {
|
||||
newImages.append(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
state.processedImages = newImages
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
case .showImagePicker:
|
||||
state.showingImagePicker = true
|
||||
return .none
|
||||
|
||||
case .hideImagePicker:
|
||||
state.showingImagePicker = false
|
||||
return .none
|
||||
|
||||
case .imageSelected(let image):
|
||||
if state.processedImages.count < 9 {
|
||||
state.processedImages.append(image)
|
||||
}
|
||||
state.showingImagePicker = false
|
||||
return .none
|
||||
|
||||
case .removeImage(let index):
|
||||
guard index < state.processedImages.count else { return .none }
|
||||
state.processedImages.remove(at: index)
|
||||
#if canImport(PhotosUI) && swift(>=5.7)
|
||||
if index < state.selectedImages.count {
|
||||
state.selectedImages.remove(at: index)
|
||||
}
|
||||
#endif
|
||||
return .none
|
||||
|
||||
case .publishButtonTapped:
|
||||
guard state.canPublish else {
|
||||
state.errorMessage = "请输入内容"
|
||||
return .none
|
||||
}
|
||||
|
||||
state.isLoading = true
|
||||
state.errorMessage = nil
|
||||
|
||||
let request = PublishDynamicRequest(
|
||||
content: state.content.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
images: state.processedImages
|
||||
)
|
||||
|
||||
return .run { send in
|
||||
do {
|
||||
let response = try await apiService.request(request)
|
||||
await send(.publishResponse(.success(response)))
|
||||
} catch {
|
||||
await send(.publishResponse(.failure(error)))
|
||||
}
|
||||
}
|
||||
|
||||
case .publishResponse(.success(let response)):
|
||||
state.isLoading = false
|
||||
|
||||
if response.code == 200 {
|
||||
// 发布成功,关闭页面
|
||||
return .send(.dismissView)
|
||||
} else {
|
||||
state.errorMessage = response.message.isEmpty ? "发布失败" : response.message
|
||||
return .none
|
||||
}
|
||||
|
||||
case .publishResponse(.failure(let error)):
|
||||
state.isLoading = false
|
||||
state.errorMessage = error.localizedDescription
|
||||
return .none
|
||||
|
||||
case .clearError:
|
||||
state.errorMessage = nil
|
||||
return .none
|
||||
|
||||
case .dismissView:
|
||||
return .run { _ in
|
||||
await dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 发布动态相关模型
|
||||
|
||||
/// 发布动态请求
|
||||
struct PublishDynamicRequest: APIRequestProtocol {
|
||||
typealias Response = PublishDynamicResponse
|
||||
|
||||
let endpoint: String = "/dynamic/square/publish" // 假设的发布端点
|
||||
let method: HTTPMethod = .POST
|
||||
let includeBaseParameters: Bool = true
|
||||
let queryParameters: [String: String]? = nil
|
||||
let timeout: TimeInterval = 30.0
|
||||
|
||||
let content: String
|
||||
let images: [UIImage]
|
||||
let type: Int // 0: 纯文字, 2: 图片
|
||||
|
||||
init(content: String, images: [UIImage] = []) {
|
||||
self.content = content
|
||||
self.images = images
|
||||
self.type = images.isEmpty ? 0 : 2
|
||||
}
|
||||
|
||||
var bodyParameters: [String: Any]? {
|
||||
var params: [String: Any] = [
|
||||
"content": content,
|
||||
"type": type
|
||||
]
|
||||
|
||||
// 如果有图片,需要转换为base64或上传到服务器
|
||||
if !images.isEmpty {
|
||||
let imageData = images.compactMap { image in
|
||||
image.jpegData(compressionQuality: 0.8)?.base64EncodedString()
|
||||
}
|
||||
params["images"] = imageData
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
||||
/// 发布动态响应
|
||||
struct PublishDynamicResponse: Codable {
|
||||
let code: Int
|
||||
let message: String
|
||||
let data: PublishDynamicData?
|
||||
}
|
||||
|
||||
struct PublishDynamicData: Codable {
|
||||
let dynamicId: Int
|
||||
let publishTime: Int
|
||||
}
|
@@ -13,6 +13,9 @@ struct FeedFeature {
|
||||
|
||||
// 是否已初始化
|
||||
var isInitialized = false
|
||||
|
||||
// CreateFeedView 相关状态
|
||||
var isShowingCreateFeed = false
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
@@ -22,6 +25,11 @@ struct FeedFeature {
|
||||
case momentsResponse(TaskResult<MomentsLatestResponse>)
|
||||
case clearError
|
||||
case retryLoad
|
||||
|
||||
// CreateFeedView 相关 Action
|
||||
case showCreateFeed
|
||||
case dismissCreateFeed
|
||||
case createFeedCompleted
|
||||
}
|
||||
|
||||
@Dependency(\.apiService) var apiService
|
||||
@@ -131,7 +139,20 @@ struct FeedFeature {
|
||||
} else {
|
||||
return .send(.loadMoreMoments)
|
||||
}
|
||||
|
||||
case .showCreateFeed:
|
||||
state.isShowingCreateFeed = true
|
||||
return .none
|
||||
|
||||
case .dismissCreateFeed:
|
||||
state.isShowingCreateFeed = false
|
||||
return .none
|
||||
|
||||
case .createFeedCompleted:
|
||||
state.isShowingCreateFeed = false
|
||||
// 发布完成后刷新动态列表
|
||||
return .send(.loadLatestMoments)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
364
yana/Views/CreateFeedView.swift
Normal file
364
yana/Views/CreateFeedView.swift
Normal file
@@ -0,0 +1,364 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
// 条件导入 PhotosUI (iOS 16.0+)
|
||||
#if canImport(PhotosUI)
|
||||
import PhotosUI
|
||||
#endif
|
||||
|
||||
struct CreateFeedView: View {
|
||||
let store: StoreOf<CreateFeedFeature>
|
||||
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
NavigationView {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// 背景渐变
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
Color(red: 0.1, green: 0.1, blue: 0.2),
|
||||
Color(red: 0.2, green: 0.1, blue: 0.3)
|
||||
]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
.ignoresSafeArea()
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
// 内容输入区域
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// 文本输入框
|
||||
ZStack(alignment: .topLeading) {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.white.opacity(0.1))
|
||||
.frame(minHeight: 120)
|
||||
|
||||
if store.content.isEmpty {
|
||||
Text("Enter Content")
|
||||
.foregroundColor(.white.opacity(0.5))
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
|
||||
TextEditor(text: .init(
|
||||
get: { store.content },
|
||||
set: { store.send(.contentChanged($0)) }
|
||||
))
|
||||
.foregroundColor(.white)
|
||||
.background(Color.clear)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.scrollContentBackground(.hidden)
|
||||
}
|
||||
|
||||
// 字符计数
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("\(store.characterCount)/500")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(
|
||||
store.isCharacterLimitExceeded ? .red : .white.opacity(0.6)
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 20)
|
||||
|
||||
// 图片选择区域
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
if !store.processedImages.isEmpty || store.canAddMoreImages {
|
||||
if #available(iOS 16.0, *) {
|
||||
#if canImport(PhotosUI)
|
||||
ModernImageSelectionGrid(
|
||||
images: store.processedImages,
|
||||
selectedItems: store.selectedImages,
|
||||
canAddMore: store.canAddMoreImages,
|
||||
onItemsChanged: { items in
|
||||
store.send(.photosPickerItemsChanged(items))
|
||||
},
|
||||
onRemoveImage: { index in
|
||||
store.send(.removeImage(index))
|
||||
}
|
||||
)
|
||||
#endif
|
||||
} else {
|
||||
LegacyImageSelectionGrid(
|
||||
images: store.processedImages,
|
||||
canAddMore: store.canAddMoreImages,
|
||||
onAddImage: {
|
||||
store.send(.showImagePicker)
|
||||
},
|
||||
onRemoveImage: { index in
|
||||
store.send(.removeImage(index))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
// 加载状态
|
||||
if store.isLoading {
|
||||
HStack {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
Text("处理图片中...")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
.padding(.top, 10)
|
||||
}
|
||||
|
||||
// 错误提示
|
||||
if let error = store.error {
|
||||
Text(error)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
.padding(.horizontal, 20)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
// 底部安全区域
|
||||
Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100)
|
||||
}
|
||||
}
|
||||
|
||||
// 底部发布按钮
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
store.send(.publishButtonTapped)
|
||||
}) {
|
||||
HStack {
|
||||
if store.isPublishing {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(0.8)
|
||||
Text("发布中...")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
} else {
|
||||
Text("发布")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.background(
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
Color.purple,
|
||||
Color.blue
|
||||
]),
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
)
|
||||
.cornerRadius(25)
|
||||
.disabled(store.isPublishing || (!store.isContentValid && !store.isLoading))
|
||||
.opacity(store.isPublishing || (!store.isContentValid && !store.isLoading) ? 0.6 : 1.0)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, geometry.safeAreaInsets.bottom + 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("图文发布")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(.hidden, for: .navigationBar)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("取消") {
|
||||
store.send(.dismissView)
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("发布") {
|
||||
store.send(.publishButtonTapped)
|
||||
}
|
||||
.foregroundColor(store.isContentValid ? .white : .white.opacity(0.5))
|
||||
.disabled(!store.isContentValid || store.isPublishing)
|
||||
}
|
||||
}
|
||||
}
|
||||
.preferredColorScheme(.dark)
|
||||
.sheet(isPresented: .init(
|
||||
get: { store.showingImagePicker },
|
||||
set: { _ in store.send(.hideImagePicker) }
|
||||
)) {
|
||||
ImagePickerView { image in
|
||||
store.send(.imageSelected(image))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - iOS 16+ 图片选择网格组件
|
||||
#if canImport(PhotosUI)
|
||||
@available(iOS 16.0, *)
|
||||
struct ModernImageSelectionGrid: View {
|
||||
let images: [UIImage]
|
||||
let selectedItems: [PhotosPickerItem]
|
||||
let canAddMore: Bool
|
||||
let onItemsChanged: ([PhotosPickerItem]) -> Void
|
||||
let onRemoveImage: (Int) -> Void
|
||||
|
||||
private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3)
|
||||
|
||||
var body: some View {
|
||||
LazyVGrid(columns: columns, spacing: 8) {
|
||||
// 显示已选择的图片
|
||||
ForEach(Array(images.enumerated()), id: \.offset) { index, image in
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(height: 100)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
|
||||
// 删除按钮
|
||||
Button(action: {
|
||||
onRemoveImage(index)
|
||||
}) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 20))
|
||||
.foregroundColor(.white)
|
||||
.background(Color.black.opacity(0.6))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.padding(4)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加图片按钮
|
||||
if canAddMore {
|
||||
PhotosPicker(
|
||||
selection: .init(
|
||||
get: { selectedItems },
|
||||
set: onItemsChanged
|
||||
),
|
||||
maxSelectionCount: 9,
|
||||
matching: .images
|
||||
) {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color.white.opacity(0.1))
|
||||
.frame(height: 100)
|
||||
.overlay(
|
||||
Image(systemName: "plus")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - iOS 15 兼容图片选择网格组件
|
||||
struct LegacyImageSelectionGrid: View {
|
||||
let images: [UIImage]
|
||||
let canAddMore: Bool
|
||||
let onAddImage: () -> Void
|
||||
let onRemoveImage: (Int) -> Void
|
||||
|
||||
private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3)
|
||||
|
||||
var body: some View {
|
||||
LazyVGrid(columns: columns, spacing: 8) {
|
||||
// 显示已选择的图片
|
||||
ForEach(Array(images.enumerated()), id: \.offset) { index, image in
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(height: 100)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
|
||||
// 删除按钮
|
||||
Button(action: {
|
||||
onRemoveImage(index)
|
||||
}) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 20))
|
||||
.foregroundColor(.white)
|
||||
.background(Color.black.opacity(0.6))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.padding(4)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加图片按钮
|
||||
if canAddMore {
|
||||
Button(action: onAddImage) {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color.white.opacity(0.1))
|
||||
.frame(height: 100)
|
||||
.overlay(
|
||||
Image(systemName: "plus")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIImagePicker 包装器
|
||||
struct ImagePickerView: UIViewControllerRepresentable {
|
||||
let onImageSelected: (UIImage) -> Void
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
func makeUIViewController(context: Context) -> UIImagePickerController {
|
||||
let picker = UIImagePickerController()
|
||||
picker.delegate = context.coordinator
|
||||
picker.sourceType = .photoLibrary
|
||||
picker.allowsEditing = false
|
||||
return picker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
||||
let parent: ImagePickerView
|
||||
|
||||
init(_ parent: ImagePickerView) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||
if let image = info[.originalImage] as? UIImage {
|
||||
parent.onImageSelected(image)
|
||||
}
|
||||
parent.presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
|
||||
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
parent.presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 预览
|
||||
#Preview {
|
||||
CreateFeedView(
|
||||
store: Store(initialState: CreateFeedFeature.State()) {
|
||||
CreateFeedFeature()
|
||||
}
|
||||
)
|
||||
}
|
@@ -22,7 +22,7 @@ struct FeedView: View {
|
||||
|
||||
// 右侧加号按钮
|
||||
Button(action: {
|
||||
// 加号按钮操作
|
||||
store.send(.showCreateFeed)
|
||||
}) {
|
||||
Image("add icon")
|
||||
.frame(width: 36, height: 36)
|
||||
@@ -104,6 +104,20 @@ struct FeedView: View {
|
||||
.onAppear {
|
||||
store.send(.onAppear)
|
||||
}
|
||||
.sheet(isPresented: .init(
|
||||
get: { store.isShowingCreateFeed },
|
||||
set: { _ in store.send(.dismissCreateFeed) }
|
||||
)) {
|
||||
CreateFeedView(
|
||||
store: Store(initialState: CreateFeedFeature.State()) {
|
||||
CreateFeedFeature()
|
||||
}
|
||||
)
|
||||
.onDisappear {
|
||||
// 当CreateFeedView消失时,可能需要刷新数据
|
||||
// 这里可以根据需要添加逻辑
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user