Files
e-party-iOS/yana/Views/Components/ImagePickerWithPreview/ImagePickerWithPreviewView.swift
edwinQQQ 17ad000e4b feat: 新增图片源选择功能以增强头像设置体验
- 添加AppImageSource枚举以定义图片源类型(相机和相册)。
- 在AppSettingFeature中新增状态和Action以管理图片源选择。
- 更新AppSettingView以支持图片源选择的ActionSheet和头像选择逻辑。
- 优化ImagePickerWithPreviewView以处理相机和相册选择的取消操作。
2025-07-31 17:29:38 +08:00

183 lines
6.6 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
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)
}
}
}
)
}
}