#5 fix camera capture preview after shooting
- Dismiss the camera fullScreenCover through the SwiftUI binding synchronously in the picker delegate before background file preparation, so UIImagePickerController no longer dismisses itself and desynchronizes the presentation state that the preview sheet waits on. - Keep the archive-sync suspension reserved across the dismissal and surface an explicit error when the captured file fails to prepare. - Add verify.sh invariants for the camera dismissal flow.
This commit is contained in:
1 parent
b54113f0e4
commit
5790f9b2f6
3 files changed
+44
-6
No files matched your search
@@ -336,6 +336,14 @@ grep -q 'let items = pickedMediaItems' Sources/Shared/UI/ChatView.swift || {
|
||||
echo "Gallery staging must run on picker dismissal only"
|
||||
exit 1
|
||||
}
|
||||
grep -q 'onDismissRequest' Sources/Shared/UI/SystemPhotoCameraView.swift || {
|
||||
echo "The camera picker must request dismissal before background preparation"
|
||||
exit 1
|
||||
}
|
||||
grep -q 'onDismissRequest' Sources/Shared/UI/ChatView.swift || {
|
||||
echo "The camera cover must dismiss through the SwiftUI binding"
|
||||
exit 1
|
||||
}
|
||||
grep -q 'ArchiveSyncCursorPolicy.requestPosition' Sources/Shared/XMPP/XMPPService.swift || {
|
||||
echo "MAM reconnects must prefer the durable archive UID over timestamps"
|
||||
exit 1
|
||||
|
||||
@@ -254,13 +254,23 @@ struct ChatView: View {
|
||||
}
|
||||
) {
|
||||
SystemPhotoCameraView(
|
||||
onMedia: { result in
|
||||
onDismissRequest: {
|
||||
// Runs synchronously in the picker delegate while the
|
||||
// camera is still presented: dismiss through the
|
||||
// binding so SwiftUI owns the transition, and reserve
|
||||
// the archive-sync suspension across the dismissal
|
||||
// while the file is prepared in the background.
|
||||
showingPhotoCamera = false
|
||||
photoCameraIsPreparingResult = true
|
||||
},
|
||||
onMedia: { result in
|
||||
switch result {
|
||||
case .success(let media):
|
||||
photoCameraIsPreparingResult = true
|
||||
Task { await stageCapturedMedia(media) }
|
||||
case .failure(let error):
|
||||
photoCameraIsPreparingResult = false
|
||||
releaseArchiveSyncAfterCapture()
|
||||
model.errorMessage = error.localizedDescription
|
||||
}
|
||||
},
|
||||
@@ -1227,11 +1237,12 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func stageImportedFiles(
|
||||
_ urls: [URL],
|
||||
preferredKind: ChatMessage.Kind? = nil
|
||||
) async {
|
||||
guard !urls.isEmpty else { return }
|
||||
) async -> Bool {
|
||||
guard !urls.isEmpty else { return false }
|
||||
isPreparingAttachments = true
|
||||
defer { isPreparingAttachments = false }
|
||||
let prepared = await model.prepareAttachmentDrafts(
|
||||
@@ -1239,10 +1250,11 @@ struct ChatView: View {
|
||||
preferredKind: preferredKind,
|
||||
for: conversation.jid
|
||||
)
|
||||
guard !prepared.isEmpty else { return }
|
||||
guard !prepared.isEmpty else { return false }
|
||||
model.discardAttachmentDrafts(attachmentDrafts)
|
||||
attachmentDrafts = prepared
|
||||
presentAttachmentPreviewAfterPickerDismissal()
|
||||
return true
|
||||
}
|
||||
|
||||
private func stageCapturedMedia(_ media: CapturedCameraMedia) async {
|
||||
@@ -1251,7 +1263,10 @@ struct ChatView: View {
|
||||
photoCameraIsPreparingResult = false
|
||||
releaseArchiveSyncAfterCapture()
|
||||
}
|
||||
await stageImportedFiles([media.url], preferredKind: media.kind)
|
||||
let staged = await stageImportedFiles([media.url], preferredKind: media.kind)
|
||||
if !staged, model.errorMessage == nil {
|
||||
model.errorMessage = "Камера вернула файл, который не удалось подготовить к отправке."
|
||||
}
|
||||
}
|
||||
|
||||
private func presentCamera() {
|
||||
|
||||
@@ -12,6 +12,13 @@ import UIKit
|
||||
|
||||
@MainActor
|
||||
struct SystemPhotoCameraView: UIViewControllerRepresentable {
|
||||
/// Called synchronously on the main thread inside
|
||||
/// `didFinishPickingMediaWithInfo`, before any background preparation.
|
||||
/// The host must flip its `isPresented` binding here so SwiftUI drives
|
||||
/// the dismissal; letting UIImagePickerController dismiss itself leaves
|
||||
/// the fullScreenCover state desynchronized and breaks the follow-up
|
||||
/// preview presentation.
|
||||
let onDismissRequest: () -> Void
|
||||
let onMedia: (Result<CapturedCameraMedia, Error>) -> Void
|
||||
let onCancel: () -> Void
|
||||
|
||||
@@ -20,7 +27,11 @@ struct SystemPhotoCameraView: UIViewControllerRepresentable {
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(onMedia: onMedia, onCancel: onCancel)
|
||||
Coordinator(
|
||||
onDismissRequest: onDismissRequest,
|
||||
onMedia: onMedia,
|
||||
onCancel: onCancel
|
||||
)
|
||||
}
|
||||
|
||||
func makeUIViewController(context: Context) -> UIImagePickerController {
|
||||
@@ -42,14 +53,17 @@ struct SystemPhotoCameraView: UIViewControllerRepresentable {
|
||||
) {}
|
||||
|
||||
final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
||||
private let onDismissRequest: () -> Void
|
||||
private let onMedia: (Result<CapturedCameraMedia, Error>) -> Void
|
||||
private let onCancel: () -> Void
|
||||
private var completed = false
|
||||
|
||||
init(
|
||||
onDismissRequest: @escaping () -> Void,
|
||||
onMedia: @escaping (Result<CapturedCameraMedia, Error>) -> Void,
|
||||
onCancel: @escaping () -> Void
|
||||
) {
|
||||
self.onDismissRequest = onDismissRequest
|
||||
self.onMedia = onMedia
|
||||
self.onCancel = onCancel
|
||||
}
|
||||
@@ -60,6 +74,7 @@ struct SystemPhotoCameraView: UIViewControllerRepresentable {
|
||||
) {
|
||||
guard !completed else { return }
|
||||
completed = true
|
||||
onDismissRequest()
|
||||
|
||||
let mediaType = (info[.mediaType] as? String).flatMap { UTType($0) }
|
||||
if mediaType?.conforms(to: .movie) == true,
|
||||
|
||||
Reference in new issue
Block a user