
Rejected
“Two rejection reasons. One was my fault. One was almost my fault.”
The email arrived a few days after submission. No drama in the subject line. App Store Connect showed two guideline violations: 2.1(a) and 2.1(b). A crash, and a UI that looked broken.
Apple attaches a crash log to the rejection. I opened it.
Rejection 1: Guideline 2.1(a), the Crash
The crash log named dispatch_assert_queue_fail. The thread: com.apple.launchservices.open-queue. The frame directly above it: a closure inside DiagnosticsViewModel.openApp().
Exception Type: EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000001, 0x00000001xxxxxxxx
Crashed Thread: com.apple.launchservices.open-queue
Thread 7 Crashed:: com.apple.launchservices.open-queue
0 libdispatch.dylib dispatch_assert_queue_fail + 120
1 AppKit -[NSWorkspace _callCompletionHandler:...] + 88
2 Renala closure #1 in DiagnosticsViewModel.openApp() + 64
// ^^^ This is the empty { _, _ in } closure.
// Swift 6 marked it @MainActor.
// It's running on the launch services queue.
// dispatch_assert_queue_fail fires.
The call site looked like this:
NSWorkspace.shared.openApplication(
at: url,
configuration: config,
completionHandler: { _, _ in }
)
An empty closure, passed because the API has a completion handler parameter. The developer equivalent of nodding politely.
Here is what happened. DiagnosticsViewModel is @MainActor. Swift 6 strict concurrency infers that any closure defined in a @MainActor context is itself @MainActor. So {'{'{'}'} _, _ in {'}'} became @MainActor {'{'{'}'} _, _ in {'}'}.
But NSWorkspace.openApplication(at:configuration:completionHandler:) calls the completion handler on com.apple.launchservices.open-queue, not on the main actor. On macOS Sequoia, Apple added a runtime assertion: if a completion handler marked @MainActor runs on a different queue, dispatch_assert_queue_fail fires and the process traps.
macOS Sequoia was not installed on my development machine. Of course it wasn’t.
The fix is one line removed. If the completion handler does nothing, remove it entirely:
NSWorkspace.shared.openApplication(at: url, configuration: config)
No closure, no type annotation, no assertion violation.
The tension is real. Swift 6 made the annotation automatic, and it was technically correct: the closure was @MainActor. But the API calls it on a different queue. Before macOS Sequoia, this mismatch was invisible. After, it crashes. The compiler cannot catch it because the constraint comes from the runtime, not the type system.
The lesson: an empty closure is not free. In Swift 6, even doing nothing has a type, and that type has opinions about where it runs.
The key frames from the crash log, annotated:
Exception Type: EXC_BREAKPOINT
Exception Subtype: SIGTRAP
Thread 0 Crashed:: Dispatch main queue
...
dispatch_assert_queue_fail
...
NSWorkspace.openApplication(at:configuration:completionHandler:)
...
// Fix: omit the completionHandler entirely if unused
Rejection 2: Guideline 2.1(b), the Empty Tip Jar
The second rejection was the more interesting one. The tip jar appeared blank during review. No products, no error. Just an empty sheet where the tip options should be.
The root cause: Product.products(for:) returns an empty array for IAPs with “Waiting for Review” status. The App Store review sandbox cannot see products that have not yet been approved. Your app and its in-app purchases are reviewed together, but the products are invisible to StoreKit until that review passes.
The tips cannot work during the review window. That is not a bug. But an empty sheet with no explanation looks like one. The reviewer sees a UI that appears broken. They flag it under 2.1(b).
The fix: detect when the products array is empty and show a message instead of nothing.
if products.isEmpty {
Text("Tips are not available right now.")
.foregroundStyle(.secondary)
} else {
// tip buttons
}
An empty sheet looks broken. A message looks intentional. The difference between a bug and a feature is sometimes just a label. Both rejections, their root causes, and their fixes:
The Resubmission
Both fixes together. The crash fix removed one line. The tip jar fix added a conditional text view. Neither change touched any other functionality.
Bump version, tag, let the pipeline run. The second review was faster than the first.
On Reading Crash Logs
The structure is consistent: exception type at the top, crashed thread labeled, frames listed with the closest to the crash first. The frame that matters is usually frame 2 or 3, past the system assertion and into your code.
In this case, three data points told the whole story. The frame named the exact closure. The queue name was in the thread label. The assertion name, dispatch_assert_queue_fail, described precisely what went wrong. From there, the cause was clear.
Apple does not include the Swift type annotation in the crash log. That requires reading the source. But the log points to the right location. Reading source from a specific frame is faster than general debugging.
The crash was on a macOS version not installed locally. The fix was a one-line deletion. The cost of not catching it earlier: 3 submission cycles, a few extra weeks of review time.
That cost is what gets you to set up testing on the full range of supported OS versions, or at minimum to spin up a VM before resubmitting. The crash was obvious once the log was in hand. The gap was not having the environment to reproduce it before submission.