import Foundation struct CapturedCameraMedia: Sendable { let url: URL let kind: ChatMessage.Kind } #if os(iOS) import SwiftUI import UniformTypeIdentifiers import UIKit @MainActor struct SystemPhotoCameraView: UIViewControllerRepresentable { let onMedia: (Result) -> Void let onCancel: () -> Void static var isAvailable: Bool { UIImagePickerController.isSourceTypeAvailable(.camera) } func makeCoordinator() -> Coordinator { Coordinator(onMedia: onMedia, onCancel: onCancel) } func makeUIViewController(context: Context) -> UIImagePickerController { let picker = UIImagePickerController() picker.delegate = context.coordinator picker.sourceType = .camera picker.mediaTypes = [UTType.image.identifier, UTType.movie.identifier] picker.cameraCaptureMode = .photo picker.videoQuality = .typeHigh picker.videoMaximumDuration = 180 picker.allowsEditing = false picker.modalPresentationStyle = .fullScreen return picker } func updateUIViewController( _ uiViewController: UIImagePickerController, context: Context ) {} final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { private let onMedia: (Result) -> Void private let onCancel: () -> Void private var completed = false init( onMedia: @escaping (Result) -> Void, onCancel: @escaping () -> Void ) { self.onMedia = onMedia self.onCancel = onCancel } func imagePickerController( _ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any] ) { guard !completed else { return } completed = true let mediaType = (info[.mediaType] as? String).flatMap { UTType($0) } if mediaType?.conforms(to: .movie) == true, let sourceURL = info[.mediaURL] as? URL { prepareMovie(from: sourceURL) return } guard let image = info[.originalImage] as? UIImage else { onMedia(.failure(SystemCameraError.missingMedia)) return } preparePhoto(image) } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { guard !completed else { return } completed = true onCancel() } private func preparePhoto(_ image: UIImage) { let completion = onMedia DispatchQueue.global(qos: .userInitiated).async { let result: Result do { let url = try Self.destinationURL(extension: "jpg") guard let data = image.jpegData(compressionQuality: 0.9), !data.isEmpty else { throw SystemCameraError.encodingFailed } try data.write(to: url, options: [.atomic]) result = .success(CapturedCameraMedia(url: url, kind: .photo)) } catch { result = .failure(error) } DispatchQueue.main.async { completion(result) } } } private func prepareMovie(from sourceURL: URL) { let completion = onMedia DispatchQueue.global(qos: .userInitiated).async { let result: Result do { let fileExtension = sourceURL.pathExtension.isEmpty ? "mov" : sourceURL.pathExtension let destination = try Self.destinationURL(extension: fileExtension) try FileManager.default.copyItem(at: sourceURL, to: destination) let values = try destination.resourceValues( forKeys: [.fileSizeKey, .isRegularFileKey] ) guard values.isRegularFile == true, (values.fileSize ?? 0) > 0 else { try? FileManager.default.removeItem(at: destination) throw SystemCameraError.emptyMovie } result = .success(CapturedCameraMedia(url: destination, kind: .video)) } catch { result = .failure(error) } DispatchQueue.main.async { completion(result) } } } private static func destinationURL(extension fileExtension: String) throws -> URL { let directory = FileManager.default.temporaryDirectory .appendingPathComponent("LumaCameraMedia", isDirectory: true) try FileManager.default.createDirectory( at: directory, withIntermediateDirectories: true ) return directory.appendingPathComponent( "camera-\(UUID().uuidString).\(fileExtension)" ) } } } private enum SystemCameraError: LocalizedError { case missingMedia case encodingFailed case emptyMovie var errorDescription: String? { switch self { case .missingMedia: return "Камера не вернула фото или видео." case .encodingFailed: return "Не удалось подготовить фотографию к отправке." case .emptyMovie: return "Камера вернула пустой видеофайл. Повторите запись." } } } #endif