Skip to content

fix(barcode-scanner): dispatch iOS cancel() cleanup to the main thread#3393

Open
AlexisZankowitch wants to merge 2 commits intotauri-apps:v2from
AlexisZankowitch:fix/ios-cancel-crash-main-thread
Open

fix(barcode-scanner): dispatch iOS cancel() cleanup to the main thread#3393
AlexisZankowitch wants to merge 2 commits intotauri-apps:v2from
AlexisZankowitch:fix/ios-cancel-crash-main-thread

Conversation

@AlexisZankowitch
Copy link
Copy Markdown

@AlexisZankowitch AlexisZankowitch commented Apr 17, 2026

Summary

Wraps the body of BarcodeScannerPlugin.cancel(_:) on iOS in DispatchQueue.main.async so the UIKit teardown runs on the main thread.

Context — please review carefully ⚠️

I'm not an iOS developer. I hit this crash while building a Tauri iOS app and worked through the diagnosis and fix with Claude Opus 4.7. I want to be upfront about my limited native iOS experience so a maintainer familiar with the plugin internals can validate the approach before merging. This code fixed the issue I was facing, so I wanted to share it here.

The fix the agent proposed

Wrap the body of cancel(_:) in DispatchQueue.main.async, mirroring the pattern already used by scan() and openAppSettings()

@objc private func cancel(_ invoke: Invoke) {
  DispatchQueue.main.async { [self] in
    self.invoke?.reject("cancelled")
    self.destroy()
    invoke.resolve()
  }
}

⬇️ Methodology explained in my comment below ⬇️

How I validated the fix

  • Pointed my Tauri app at a local path-dep clone of this repo with the patch applied.
  • Rebuilt and deployed to the same iPhone 13 / iOS 26.4.
  • Tapped scan → camera opened in windowed mode.
  • Tapped my cancel button → the scanner closed cleanly, no crash. The pending scan promise in JS rejected with "cancelled" as expected.
  • Re-tested the happy path: scan → point at a real book barcode → decoded correctly, webView opacity/background restored normally.
  • ⚠️ I would probably need someone else to validate this fix is working fine.

Potential related issues

Issues that could be related, please let me know if I should mention them in the PR title.

Final notes

I reviewed the best I could what the agent proposed and ran some tests and it seems to be working fine for me. That's why I wanted to propose the fix to you guys as well. But I'm perfectly fine if you guys want to take those changes and push it from another PR if you think it's more appropriate. Hopefully I didn't fail your AI policy. If so I apologize and can delete my PR 😅

Signed-off-by: Alexis Zankowitch <a.zankowitch@reply.de>
@AlexisZankowitch AlexisZankowitch requested a review from a team as a code owner April 17, 2026 08:56
@AlexisZankowitch
Copy link
Copy Markdown
Author

Methodology used to diagnose and "fix" the issue

  1. In my app, I added the plugin @tauri-apps/plugin-barcode-scanner and wired a scan icon to my svelte kit app. I then tried it on my actual iPhone (iPhone 13 / iOS 26.4). I am using windowed: true so I could render my own UI (including a cancel button). Tapping the scan icon opened the camera fine and scanning a real barcode worked perfectly. But when I added a cancel button that calls cancel() from JS to close the scanner without scanning anything, tapping it reliably crashed the app every time.

  2. I managed to find some crash report (.ips) from the phone's using the Open Recent Logs button of the Devices and Simulators window from Xcode. Where I saw some: Crash Reporter Key and other words that made me think I could give that file to the LLM to see if it can make sense of it.

  3. I fed the crash report to the agent and asked it to help diagnose. From that point on, it led the analysis.

From my crash report:

Last Exception Backtrace:
0   CoreFoundation                	       0x193103c70 __exceptionPreprocess + 164
1   libobjc.A.dylib               	       0x18fbd9224 objc_exception_throw + 87
2   Foundation                    	       0x190c39f50 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 287
3   UIKitCore                     	       0x198c38198 -[UIView(Internal) _didMoveFromWindow:toWindow:] + 2103
4   UIKitCore                     	       0x198bd55f0 __45-[UIView(Hierarchy) _postMovedFromSuperview:]_block_invoke + 131
5   UIKitCore                     	       0x198bd54f8 -[UIView _postMovedFromSuperview:] + 507
6   UIKitCore                     	       0x198be3f84 __UIViewWasRemovedFromSuperview + 135
7   UIKitCore                     	       0x198be4eb4 -[UIView(Hierarchy) removeFromSuperview] + 255
8   <app>                        	       0x102e97fe0 BarcodeScannerPlugin.dismantleCamera() + 836
9   <app>                        	       0x102e98124 BarcodeScannerPlugin.destroy() + 76
10  <app>                        	       0x102e9ae5c BarcodeScannerPlugin.cancel(_:) + 240
11  <app>                        	       0x102e9aedc @objc BarcodeScannerPlugin.cancel(_:) + 68 //  ⬅️Apparently this is the sweat sugar (from me)
12  <app>                        	       0x102e8a558 closure #1 in PluginManager.invoke(name:invoke:) + 1932
13  <app>                       	       0x102e8ab0c thunk for @escaping @callee_guaranteed @Sendable () -> () + 48
14  libdispatch.dylib             	       0x1cd22e9a8 _dispatch_call_block_and_release + 31
15  libdispatch.dylib             	       0x1cd2481e4 _dispatch_client_callout + 15
16  libdispatch.dylib             	       0x1cd236fb0 _dispatch_lane_serial_drain + 739
17  libdispatch.dylib             	       0x1cd237aac _dispatch_lane_invoke + 391
18  libdispatch.dylib             	       0x1cd241dac _dispatch_root_queue_drain_deferred_wlh + 283
19  libdispatch.dylib             	       0x1cd2416ac _dispatch_workloop_worker_thread + 719
20  libsystem_pthread.dylib       	       0x1f1e303b0 _pthread_wqthread + 291
21  libsystem_pthread.dylib       	       0x1f1e2f8c0 start_wqthread + 7

Diagnosis from Claude Opus 4.7

Looking at BarcodeScannerPlugin.swift, the cancel() handler synchronously mutates UIViews on whichever thread Tauri happens to dispatch plugin commands on — in this case thread 12 with "queue": "ipc", not com.apple.main-thread. UIKit enforces main- thread-only mutations with an internal assertion that aborts the process with SIGABRT:

┌─  UIKit assertion fires here
│   -[NSAssertionHandler handleFailureInMethod:...]
│   -[UIView(Internal) _didMoveFromWindow:toWindow:]
│   -[UIView(Hierarchy) removeFromSuperview]     ← UIKit call off-main
│
│   BarcodeScannerPlugin.dismantleCamera()
│   BarcodeScannerPlugin.destroy()
│   BarcodeScannerPlugin.cancel(_:)              ← our handler
│   @objc BarcodeScannerPlugin.cancel(_:)
│
│   closure #1 in PluginManager.invoke(name:invoke:)
│   _dispatch_call_block_and_release
│   _dispatch_lane_serial_drain
└─  _dispatch_workloop_worker_thread              ← background IPC queue

The other commands in the same file (scan(), openAppSettings()) already wrap their UIKit work in DispatchQueue.main.async. The cancel() handler doesn't — that looks like the bug.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant