diff --git a/Tusker/Models/CompositionAttachmentData.swift b/Tusker/Models/CompositionAttachmentData.swift index ab11fd68..577e70f7 100644 --- a/Tusker/Models/CompositionAttachmentData.swift +++ b/Tusker/Models/CompositionAttachmentData.swift @@ -48,13 +48,13 @@ enum CompositionAttachmentData { } } - func getData(completion: @escaping (_ data: Data, _ mimeType: String) -> Void) { + func getData(completion: @escaping (Result<(Data, String), Error>) -> Void) { switch self { case let .image(image): // Export as JPEG instead of PNG, otherweise photos straight from the camera are too large // for Mastodon in its default configuration (max of 10MB). // The quality of 0.8 was chosen completely arbitrarily, it may need to be tuned in the future. - completion(image.jpegData(compressionQuality: 0.8)!, "image/jpeg") + completion(.success((image.jpegData(compressionQuality: 0.8)!, "image/jpeg"))) case let .asset(asset): if asset.mediaType == .image { let options = PHImageRequestOptions() @@ -63,7 +63,10 @@ enum CompositionAttachmentData { options.resizeMode = .none options.isNetworkAccessAllowed = true PHImageManager.default().requestImageDataAndOrientation(for: asset, options: options) { (data, dataUTI, orientation, info) in - guard var data = data, let dataUTI = dataUTI else { fatalError() } + guard var data = data, let dataUTI = dataUTI else { + completion(.failure(.missingData)) + return + } let mimeType: String if dataUTI == "public.heic" { @@ -77,7 +80,7 @@ enum CompositionAttachmentData { mimeType = UTType(dataUTI)!.preferredMIMEType! } - completion(data, mimeType) + completion(.success((data, mimeType))) } } else if asset.mediaType == .video { let options = PHVideoRequestOptions() @@ -100,20 +103,23 @@ enum CompositionAttachmentData { case let .drawing(drawing): let image = drawing.imageInLightMode(from: drawing.bounds, scale: 1) - completion(image.pngData()!, "image/png") + completion(.success((image.pngData()!, "image/png"))) } } - private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Data, String) -> Void) { + private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Result<(Data, String), Error>) -> Void) { session.outputFileType = .mp4 session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4") session.exportAsynchronously { - guard session.status == .completed else { fatalError("video export failed: \(String(describing: session.error))") } + guard session.status == .completed else { + completion(.failure(.export(session.error!))) + return + } do { let data = try Data(contentsOf: session.outputURL!) - completion(data, "video/mp4") + completion(.success((data, "video/mp4"))) } catch { - fatalError("Unable to load video: \(error)") + completion(.failure(.export(error))) } } } @@ -121,6 +127,11 @@ enum CompositionAttachmentData { enum AttachmentType { case image, video } + + enum Error: Swift.Error { + case missingData + case export(Swift.Error) + } } extension PHAsset { diff --git a/Tusker/Screens/Compose/ComposeAttachmentRow.swift b/Tusker/Screens/Compose/ComposeAttachmentRow.swift index a8ed89eb..3254b10e 100644 --- a/Tusker/Screens/Compose/ComposeAttachmentRow.swift +++ b/Tusker/Screens/Compose/ComposeAttachmentRow.swift @@ -97,7 +97,18 @@ struct ComposeAttachmentRow: View { mode = .recognizingText DispatchQueue.global(qos: .userInitiated).async { - self.attachment.data.getData { (data, mimeType) in + self.attachment.data.getData { (result) in + let data: Data + do { + try data = result.get().0 + } catch { + DispatchQueue.main.async { + self.mode = .allowEntry + self.isShowingTextRecognitionFailedAlert = true + self.textRecognitionErrorMessage = error.localizedDescription + } + return + } let handler = VNImageRequestHandler(data: data, options: [:]) let request = VNRecognizeTextRequest { (request, error) in DispatchQueue.main.async { diff --git a/Tusker/Screens/Compose/ComposeView.swift b/Tusker/Screens/Compose/ComposeView.swift index d1c08a8d..9fe304bb 100644 --- a/Tusker/Screens/Compose/ComposeView.swift +++ b/Tusker/Screens/Compose/ComposeView.swift @@ -244,17 +244,17 @@ struct ComposeView: View { private func uploadAttachments(_ completion: @escaping (Result<[Attachment], AttachmentUploadError>) -> Void) { let group = DispatchGroup() - var attachmentDatas = [(Data, String)?]() + var attachmentDataResults = [Result<(Data, String), CompositionAttachmentData.Error>?]() for (index, compAttachment) in draft.attachments.enumerated() { group.enter() - attachmentDatas.append(nil) + attachmentDataResults.append(nil) - compAttachment.data.getData { (data, mimeType) in + compAttachment.data.getData { (result) in postProgress += 1 - attachmentDatas[index] = (data, mimeType) + attachmentDataResults[index] = result group.leave() } } @@ -271,7 +271,15 @@ struct ComposeView: View { // posted status reflects order the user set. // Pleroma does respect the order of the `media_ids` parameter. - for (index, (data, mimeType)) in attachmentDatas.map(\.unsafelyUnwrapped).enumerated() { + let datas: [(Data, String)] + do { + datas = try attachmentDataResults.map { try $0!.get() } + } catch { + completion(.failure(AttachmentUploadError(errors: [error]))) + return + } + + for (index, (data, mimeType)) in datas.enumerated() { group.enter() let compAttachment = draft.attachments[index]