-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathSTImageManager.swift
More file actions
301 lines (268 loc) · 11.2 KB
/
Copy pathSTImageManager.swift
File metadata and controls
301 lines (268 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
//
// STImageManager.swift
// STBaseProject
//
// Created by 寒江孤影 on 2018/3/14.
//
import UIKit
import Photos
import PhotosUI
import AVFoundation
public enum STImageSource {
case camera
case photoLibrary
case simulator
case unknown
public var description: String {
switch self {
case .camera: return "camera"
case .photoLibrary: return "photo_library"
case .simulator: return "simulator"
case .unknown: return "unknown"
}
}
}
public struct STImageManagerModel {
public let image: UIImage
public let imageData: Data
public let fileName: String
public let mimeType: String
public let source: STImageSource
public init(image: UIImage, imageData: Data, fileName: String, mimeType: String, source: STImageSource) {
self.image = image
self.imageData = imageData
self.fileName = fileName
self.mimeType = mimeType
self.source = source
}
}
public struct STImageManagerConfiguration: Sendable {
public var allowsEditing: Bool = true
public var showsCameraControls: Bool = true
public var cameraDevice: UIImagePickerController.CameraDevice = .rear
public var maxFileSize: Int = 300
public var imageFormat: String = "jpeg"
public var compressionQuality: CGFloat = 0.8
public var pickerTitle: String = "选择图片来源"
public var cameraButtonTitle: String = "相机"
public var photoLibraryButtonTitle: String = "照片库"
public var cancelButtonTitle: String = "取消"
public init() {}
}
public enum STImageManagerError: LocalizedError {
case permissionDenied(STImageSource)
case deviceNotAvailable(STImageSource)
case userCancelled
case compressionFailed
case unknown
public var errorDescription: String? {
switch self {
case .permissionDenied(let source):
return "Permission denied for \(source.description)"
case .deviceNotAvailable(let source):
return "Device not available: \(source.description)"
case .userCancelled:
return "User cancelled"
case .compressionFailed:
return "Image compression failed"
case .unknown:
return "Unknown error occurred"
}
}
}
@MainActor
public class STImageManager: NSObject {
public static let shared = STImageManager()
private var configuration: STImageManagerConfiguration = STImageManagerConfiguration()
private var pickerContinuation: CheckedContinuation<STImageManagerModel, Error>?
private var cameraContinuation: CheckedContinuation<STImageManagerModel, Error>?
private var imagePickerController: UIImagePickerController?
private override init() {
super.init()
}
deinit {
self.imagePickerController = nil
}
public func updateConfiguration(_ config: STImageManagerConfiguration) {
self.configuration = config
}
// MARK: - Public async API
public func selectImage(
from viewController: UIViewController,
source: STImageSource = .photoLibrary,
configuration: STImageManagerConfiguration? = nil
) async throws -> STImageManagerModel {
if let config = configuration { self.configuration = config }
switch source {
case .camera:
return try await self.selectFromCamera(from: viewController)
case .photoLibrary:
return try await self.selectFromPhotoLibrary(from: viewController)
case .simulator:
throw STImageManagerError.deviceNotAvailable(.simulator)
case .unknown:
throw STImageManagerError.unknown
}
}
public func showImagePicker(
from viewController: UIViewController,
configuration: STImageManagerConfiguration? = nil
) async throws -> STImageManagerModel {
if let config = configuration { self.configuration = config }
let source: STImageSource = try await withCheckedThrowingContinuation { continuation in
let alert = UIAlertController(title: self.configuration.pickerTitle, message: nil, preferredStyle: .actionSheet)
alert.addAction(UIAlertAction(title: self.configuration.cameraButtonTitle, style: .default) { _ in
continuation.resume(returning: .camera)
})
alert.addAction(UIAlertAction(title: self.configuration.photoLibraryButtonTitle, style: .default) { _ in
continuation.resume(returning: .photoLibrary)
})
alert.addAction(UIAlertAction(title: self.configuration.cancelButtonTitle, style: .cancel) { _ in
continuation.resume(throwing: STImageManagerError.userCancelled)
})
viewController.present(alert, animated: true)
}
return try await self.selectImage(from: viewController, source: source)
}
// MARK: - Private photo library
private func selectFromPhotoLibrary(from viewController: UIViewController) async throws -> STImageManagerModel {
// 取消之前未完成的请求,避免旧 continuation 永远挂起
self.resolvePendingPickerContinuation(with: .userCancelled)
return try await withCheckedThrowingContinuation { continuation in
self.pickerContinuation = continuation
var config = PHPickerConfiguration(photoLibrary: .shared())
config.filter = .images
config.selectionLimit = 1
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
viewController.present(picker, animated: true)
}
}
// MARK: - Private camera
private func selectFromCamera(from viewController: UIViewController) async throws -> STImageManagerModel {
try await self.checkCameraPermission()
// 取消之前未完成的请求,避免旧 continuation 永远挂起
self.resolvePendingCameraContinuation(with: .userCancelled)
return try await withCheckedThrowingContinuation { continuation in
self.cameraContinuation = continuation
let picker = UIImagePickerController()
picker.sourceType = .camera
picker.allowsEditing = self.configuration.allowsEditing
picker.cameraDevice = self.configuration.cameraDevice
picker.showsCameraControls = self.configuration.showsCameraControls
picker.delegate = self
self.imagePickerController = picker
viewController.present(picker, animated: true)
}
}
private func checkCameraPermission() async throws {
guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
throw STImageManagerError.deviceNotAvailable(.camera)
}
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .authorized:
return
case .notDetermined:
let granted: Bool = await withCheckedContinuation { continuation in
AVCaptureDevice.requestAccess(for: .video) { continuation.resume(returning: $0) }
}
if !granted { throw STImageManagerError.permissionDenied(.camera) }
case .denied, .restricted:
throw STImageManagerError.permissionDenied(.camera)
@unknown default:
throw STImageManagerError.unknown
}
}
// MARK: - Continuation 安全清理
private func resolvePendingPickerContinuation(with error: STImageManagerError) {
guard let pending = self.pickerContinuation else { return }
self.pickerContinuation = nil
pending.resume(throwing: error)
}
private func resolvePendingCameraContinuation(with error: STImageManagerError) {
guard let pending = self.cameraContinuation else { return }
self.cameraContinuation = nil
pending.resume(throwing: error)
}
// MARK: - Model 构建(nonisolated 以便在 Task.detached 中调用)
public nonisolated static func buildModel(
from image: UIImage,
source: STImageSource,
config: STImageManagerConfiguration
) throws -> STImageManagerModel {
guard let compressedData = UIImage.smartCompress(image, maxFileSize: config.maxFileSize) else {
throw STImageManagerError.compressionFailed
}
let format = image.getTypeString() ?? config.imageFormat
let timestamp = Date().timeIntervalSince1970
return STImageManagerModel(
image: image,
imageData: compressedData,
fileName: "photo_\(timestamp).\(format)",
mimeType: "image/\(format)",
source: source
)
}
}
// MARK: - PHPickerViewControllerDelegate
extension STImageManager: PHPickerViewControllerDelegate {
public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let result = results.first else {
self.resolvePendingPickerContinuation(with: .userCancelled)
return
}
guard result.itemProvider.canLoadObject(ofClass: UIImage.self) else {
self.resolvePendingPickerContinuation(with: .unknown)
return
}
let continuation = self.pickerContinuation
self.pickerContinuation = nil
let config = self.configuration
result.itemProvider.loadObject(ofClass: UIImage.self) { object, error in
if let error {
continuation?.resume(throwing: error)
return
}
guard let image = object as? UIImage else {
continuation?.resume(throwing: STImageManagerError.unknown)
return
}
Task.detached(priority: .userInitiated) {
do {
let model = try STImageManager.buildModel(from: image, source: .photoLibrary, config: config)
continuation?.resume(returning: model)
} catch {
continuation?.resume(throwing: error)
}
}
}
}
}
// MARK: - UIImagePickerControllerDelegate
extension STImageManager: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
picker.dismiss(animated: true)
let imageKey: UIImagePickerController.InfoKey = self.configuration.allowsEditing ? .editedImage : .originalImage
guard let image = info[imageKey] as? UIImage else {
self.resolvePendingCameraContinuation(with: .unknown)
return
}
let continuation = self.cameraContinuation
self.cameraContinuation = nil
let config = self.configuration
Task.detached(priority: .userInitiated) {
do {
let model = try STImageManager.buildModel(from: image, source: .camera, config: config)
continuation?.resume(returning: model)
} catch {
continuation?.resume(throwing: error)
}
}
}
public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
self.resolvePendingCameraContinuation(with: .userCancelled)
}
}