
- 添加AppImageSource枚举以定义图片源类型(相机和相册)。 - 在AppSettingFeature中新增状态和Action以管理图片源选择。 - 更新AppSettingView以支持图片源选择的ActionSheet和头像选择逻辑。 - 优化ImagePickerWithPreviewView以处理相机和相册选择的取消操作。
183 lines
6.6 KiB
Swift
183 lines
6.6 KiB
Swift
import SwiftUI
|
||
import ComposableArchitecture
|
||
import PhotosUI
|
||
|
||
public struct ImagePickerWithPreviewView: View {
|
||
let store: StoreOf<ImagePickerWithPreviewReducer>
|
||
let onUpload: ([UIImage]) -> Void
|
||
let onCancel: () -> Void
|
||
|
||
@State private var loadedImages: [UIImage] = []
|
||
@State private var isLoadingImages: Bool = false
|
||
@State private var loadingId: UUID?
|
||
|
||
public init(store: StoreOf<ImagePickerWithPreviewReducer>, onUpload: @escaping ([UIImage]) -> Void, onCancel: @escaping () -> Void) {
|
||
self.store = store
|
||
self.onUpload = onUpload
|
||
self.onCancel = onCancel
|
||
}
|
||
|
||
public var body: some View {
|
||
WithViewStore(store, observe: { $0 }) { viewStore in
|
||
ZStack {
|
||
Color.clear
|
||
}
|
||
.background(.clear)
|
||
.ignoresSafeArea()
|
||
.modifier(CameraSheetModifier(viewStore: viewStore, onCancel: onCancel))
|
||
.modifier(PhotosPickerModifier(viewStore: viewStore, loadedImages: $loadedImages, isLoadingImages: $isLoadingImages, loadingId: $loadingId, onCancel: onCancel))
|
||
.modifier(PreviewCoverModifier(viewStore: viewStore, loadedImages: loadedImages, onUpload: onUpload, loadingId: $loadingId, onCancel: onCancel))
|
||
.modifier(ErrorToastModifier(viewStore: viewStore))
|
||
.onChange(of: viewStore.inner.isLoading) { isLoading in
|
||
if isLoading && loadingId == nil {
|
||
loadingId = APILoadingManager.shared.startLoading()
|
||
} else if !isLoading, let id = loadingId {
|
||
APILoadingManager.shared.finishLoading(id)
|
||
loadingId = nil
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private struct CameraSheetModifier: ViewModifier {
|
||
let viewStore: ViewStoreOf<ImagePickerWithPreviewReducer>
|
||
let onCancel: () -> Void
|
||
func body(content: Content) -> some View {
|
||
content.sheet(isPresented: .init(
|
||
get: { viewStore.inner.showCamera },
|
||
set: { viewStore.send(.inner(.setShowCamera($0))) }
|
||
)) {
|
||
CameraPicker { image in
|
||
if let image = image {
|
||
viewStore.send(.inner(.cameraImagePicked(image)))
|
||
} else {
|
||
// 相机取消,关闭整个视图
|
||
onCancel()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private struct PhotosPickerModifier: ViewModifier {
|
||
let viewStore: ViewStoreOf<ImagePickerWithPreviewReducer>
|
||
@Binding var loadedImages: [UIImage]
|
||
@Binding var isLoadingImages: Bool
|
||
@Binding var loadingId: UUID?
|
||
let onCancel: () -> Void
|
||
|
||
func body(content: Content) -> some View {
|
||
content
|
||
.photosPicker(
|
||
isPresented: .init(
|
||
get: { viewStore.inner.showPhotoPicker },
|
||
set: { show in
|
||
viewStore.send(.inner(.setShowPhotoPicker(show)))
|
||
// 如果相册选择器被关闭且没有选择图片,则关闭整个视图
|
||
if !show && viewStore.inner.selectedPhotoItems.isEmpty {
|
||
onCancel()
|
||
}
|
||
}
|
||
),
|
||
selection: .init(
|
||
get: { viewStore.inner.selectedPhotoItems },
|
||
set: { viewStore.send(.inner(.photoPickerItemsChanged($0))) }
|
||
),
|
||
maxSelectionCount: {
|
||
switch viewStore.inner.selectionMode {
|
||
case .single: return 1
|
||
case .multiple(let max): return max
|
||
}
|
||
}(),
|
||
matching: .images
|
||
)
|
||
.onChange(of: viewStore.inner.selectedPhotoItems) { items in
|
||
guard !items.isEmpty else { return }
|
||
isLoadingImages = true
|
||
loadedImages = []
|
||
let group = DispatchGroup()
|
||
var tempImages: [UIImage] = []
|
||
for item in items {
|
||
group.enter()
|
||
item.loadTransferable(type: Data.self) { result in
|
||
defer { group.leave() }
|
||
if let data = try? result.get(), let uiImage = UIImage(data: data) {
|
||
DispatchQueue.main.async {
|
||
tempImages.append(uiImage)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
DispatchQueue.global().async {
|
||
group.wait()
|
||
DispatchQueue.main.async {
|
||
loadedImages = tempImages
|
||
isLoadingImages = false
|
||
viewStore.send(.inner(.setLoading(false)))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private struct PreviewCoverModifier: ViewModifier {
|
||
let viewStore: ViewStoreOf<ImagePickerWithPreviewReducer>
|
||
let loadedImages: [UIImage]
|
||
let onUpload: ([UIImage]) -> Void
|
||
@Binding var loadingId: UUID?
|
||
let onCancel: () -> Void
|
||
func body(content: Content) -> some View {
|
||
content.fullScreenCover(isPresented: .init(
|
||
get: { viewStore.inner.showPreview },
|
||
set: { _ in }
|
||
)) {
|
||
ImagePreviewView(
|
||
images: .constant(previewImages),
|
||
currentIndex: .init(
|
||
get: { viewStore.inner.previewIndex },
|
||
set: { viewStore.send(.inner(.setPreviewIndex($0))) }
|
||
),
|
||
onConfirm: {
|
||
viewStore.send(.inner(.previewConfirm))
|
||
onUpload(previewImages)
|
||
},
|
||
onCancel: {
|
||
viewStore.send(.inner(.previewCancel))
|
||
onCancel()
|
||
}
|
||
)
|
||
}
|
||
}
|
||
private var previewImages: [UIImage] {
|
||
if let camera = viewStore.inner.cameraImage {
|
||
return [camera]
|
||
}
|
||
if !loadedImages.isEmpty {
|
||
return loadedImages
|
||
}
|
||
return []
|
||
}
|
||
}
|
||
|
||
private struct ErrorToastModifier: ViewModifier {
|
||
let viewStore: ViewStoreOf<ImagePickerWithPreviewReducer>
|
||
func body(content: Content) -> some View {
|
||
content.overlay(
|
||
Group {
|
||
if let error = viewStore.inner.errorMessage {
|
||
VStack {
|
||
Spacer()
|
||
Text(error)
|
||
.foregroundColor(.red)
|
||
.padding()
|
||
.background(Color.white)
|
||
.cornerRadius(12)
|
||
.padding(.bottom, 40)
|
||
}
|
||
}
|
||
}
|
||
)
|
||
}
|
||
}
|