#5 fix camera capture preview after shooting
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s

- 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:
wt committed 2026-08-29 02:29:29 +07:00
1 parent b54113f0e4
commit 5790f9b2f6
3 files changed
+44 -6

No files matched your search

+8
View File
@@ -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
+20 -5
View File
@@ -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() {
+16 -1
View File
@@ -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,