diff --git a/.github/workflows/ui-tests-critical.yml b/.github/workflows/ui-tests-critical.yml
index 74ce0adaab7..8ca8080e7c9 100644
--- a/.github/workflows/ui-tests-critical.yml
+++ b/.github/workflows/ui-tests-critical.yml
@@ -48,4 +48,28 @@ jobs:
xcode: "16.2"
command:
- fastlane_command: ui_critical_tests_ios_swiftui_envelope
- - fastlane_command: ui_critical_tests_ios_swiftui_crash
+
+ run-swiftui-crash-test:
+ name: Run SwiftUI Crash Test
+ runs-on: macos-15
+ steps:
+ - uses: actions/checkout@v4
+
+ - run: ./scripts/ci-select-xcode.sh 16.2
+
+ - run: make init-ci-build
+ - run: make xcode-ci
+
+ - name: Boot simulator
+ run: ./scripts/ci-boot-simulator.sh
+
+ - name: Run SwiftUI Crash Test
+ run: |
+ ./TestSamples/SwiftUICrashTest/test-crash-and-relaunch.sh --screenshots-dir "swiftui-crash-test-screenshots"
+
+ - name: Upload SwiftUI Crash Test Screenshots
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: swiftui-crash-test-screenshots
+ path: swiftui-crash-test-screenshots
diff --git a/.gitignore b/.gitignore
index a97011acd39..46435c2e3b1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -96,6 +96,7 @@ Samples/visionOS-Swift/visionOS-Swift.xcodeproj
Samples/watchOS-Swift/watchOS-Swift.xcodeproj
Samples/SentrySampleShared/SentrySampleShared.xcodeproj
TestSamples/SwiftUITestSample/SwiftUITestSample.xcodeproj
+TestSamples/SwiftUICrashTest/SwiftUICrashTest.xcodeproj
Sentry.xcframework*
Sentry-Dynamic.xcframework*
diff --git a/Makefile b/Makefile
index e43c79f7e10..9f7836db3c2 100644
--- a/Makefile
+++ b/Makefile
@@ -188,3 +188,4 @@ xcode-ci:
xcodegen --spec Samples/visionOS-Swift/visionOS-Swift.yml
xcodegen --spec Samples/watchOS-Swift/watchOS-Swift.yml
xcodegen --spec TestSamples/SwiftUITestSample/SwiftUITestSample.yml
+ xcodegen --spec TestSamples/SwiftUICrashTest/SwiftUICrashTest.yml
diff --git a/Sentry.xcworkspace/contents.xcworkspacedata b/Sentry.xcworkspace/contents.xcworkspacedata
index 659478e10a7..dab599420f3 100644
--- a/Sentry.xcworkspace/contents.xcworkspacedata
+++ b/Sentry.xcworkspace/contents.xcworkspacedata
@@ -59,5 +59,8 @@
+
+
diff --git a/TestSamples/SwiftUICrashTest/SwiftUICrashTest.xcconfig b/TestSamples/SwiftUICrashTest/SwiftUICrashTest.xcconfig
new file mode 100644
index 00000000000..f8ad7182821
--- /dev/null
+++ b/TestSamples/SwiftUICrashTest/SwiftUICrashTest.xcconfig
@@ -0,0 +1,29 @@
+#include "../SwiftUITestSample/Shared/Config/Architectures.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/BuildOptions.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/Deployment.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/Linking.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/Localization.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/Packaging.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/SearchPaths.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/Signing.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/Versioning.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/CodeGeneration.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/ClangLanguage.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/ClangCppLanguage.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/ClangModules.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/ClangObjCLanguage.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/ClangPreprocessing.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/ClangWarnings.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/ClangWarningsCpp.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/ClangWarningsObjC.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/AssetCatalog.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/ClangAnalyzer.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/Swift.xcconfig"
+#include "../SwiftUITestSample/Shared/Config/Metal.xcconfig"
+
+PRODUCT_NAME = SwiftUICrashTest
+PRODUCT_BUNDLE_IDENTIFIER = io.sentry.tests.SwiftUICrashTest
+GENERATE_INFOPLIST_FILE = YES
+
+SUPPORTED_PLATFORMS = iphoneos iphonesimulator
+MARKETING_VERSION = 1
diff --git a/TestSamples/SwiftUICrashTest/SwiftUICrashTest.yml b/TestSamples/SwiftUICrashTest/SwiftUICrashTest.yml
new file mode 100644
index 00000000000..b95cbb5865f
--- /dev/null
+++ b/TestSamples/SwiftUICrashTest/SwiftUICrashTest.yml
@@ -0,0 +1,30 @@
+name: SwiftUICrashTest
+createIntermediateGroups: true
+generateEmptyDirectories: true
+configs:
+ Debug: debug
+ Release: release
+projectReferences:
+ Sentry:
+ path: ../../Sentry.xcodeproj
+fileGroups:
+ - SwiftUICrashTest.yml
+options:
+ bundleIdPrefix: io.sentry
+targets:
+ SwiftUICrashTest:
+ type: application
+ platform: auto
+ dependencies:
+ - target: Sentry/Sentry
+ sources:
+ - SwiftUICrashTest
+ configFiles:
+ Debug: SwiftUICrashTest.xcconfig
+ Release: SwiftUICrashTest.xcconfig
+
+schemes:
+ SwiftUICrashTest:
+ build:
+ targets:
+ SwiftUICrashTest: all
diff --git a/TestSamples/SwiftUICrashTest/SwiftUICrashTest/Assets.xcassets/AccentColor.colorset/Contents.json b/TestSamples/SwiftUICrashTest/SwiftUICrashTest/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 00000000000..0afb3cf0eec
--- /dev/null
+++ b/TestSamples/SwiftUICrashTest/SwiftUICrashTest/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors": [
+ {
+ "idiom": "universal"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/TestSamples/SwiftUICrashTest/SwiftUICrashTest/Assets.xcassets/AppIcon.appiconset/Contents.json b/TestSamples/SwiftUICrashTest/SwiftUICrashTest/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000000..c70a5bff185
--- /dev/null
+++ b/TestSamples/SwiftUICrashTest/SwiftUICrashTest/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,35 @@
+{
+ "images": [
+ {
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "tinted"
+ }
+ ],
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/TestSamples/SwiftUICrashTest/SwiftUICrashTest/Assets.xcassets/Contents.json b/TestSamples/SwiftUICrashTest/SwiftUICrashTest/Assets.xcassets/Contents.json
new file mode 100644
index 00000000000..74d6a722cf3
--- /dev/null
+++ b/TestSamples/SwiftUICrashTest/SwiftUICrashTest/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/TestSamples/SwiftUICrashTest/SwiftUICrashTest/ContentView.swift b/TestSamples/SwiftUICrashTest/SwiftUICrashTest/ContentView.swift
new file mode 100644
index 00000000000..7c39b655063
--- /dev/null
+++ b/TestSamples/SwiftUICrashTest/SwiftUICrashTest/ContentView.swift
@@ -0,0 +1,18 @@
+import Sentry
+import SwiftUI
+
+struct ContentView: View {
+ var body: some View {
+ VStack {
+ Image(systemName: "globe")
+ .imageScale(.large)
+ .foregroundStyle(.tint)
+ Text("Hello, world!")
+ }
+ .padding()
+ }
+}
+
+#Preview {
+ ContentView()
+}
diff --git a/TestSamples/SwiftUICrashTest/SwiftUICrashTest/SwiftUICrashTestApp.swift b/TestSamples/SwiftUICrashTest/SwiftUICrashTest/SwiftUICrashTestApp.swift
new file mode 100644
index 00000000000..aa219a34116
--- /dev/null
+++ b/TestSamples/SwiftUICrashTest/SwiftUICrashTest/SwiftUICrashTestApp.swift
@@ -0,0 +1,25 @@
+import Sentry
+import SwiftUI
+
+@main
+struct SwiftUICrashTestApp: App {
+
+ init() {
+ SentrySDK.start { options in
+ options.dsn = "https://6cc9bae94def43cab8444a99e0031c28@o447951.ingest.sentry.io/5428557"
+ }
+
+ let userDefaultsKey = "crash-on-launch"
+ if UserDefaults.standard.bool(forKey: userDefaultsKey) {
+
+ UserDefaults.standard.removeObject(forKey: userDefaultsKey)
+ SentrySDK.crash()
+ }
+ }
+
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ }
+ }
+}
diff --git a/TestSamples/SwiftUICrashTest/test-crash-and-relaunch.sh b/TestSamples/SwiftUICrashTest/test-crash-and-relaunch.sh
new file mode 100755
index 00000000000..a6c67ae4223
--- /dev/null
+++ b/TestSamples/SwiftUICrashTest/test-crash-and-relaunch.sh
@@ -0,0 +1,138 @@
+#!/bin/bash
+
+set -euo pipefail
+
+# Launches the SwiftUI Crash Test app and validates that it crashes and relaunches correctly.
+# This test run requires one booted simulator to work. So make sure to boot one simulator before
+# running this script.
+
+# Background:
+# XCTest isn't built for crashing during tests. Instead of using XCTest to press a button and
+# let a test app crash, we now use UserDefaults to tell the test app to crash during launch.
+# We then simply launch the app again via `xcrun simctl launch` and wait to see if it keeps
+# running. This is basically the same as the testCrash of the SwiftUITestSample without using
+# XCTests.
+
+
+BUNDLE_ID="io.sentry.tests.SwiftUICrashTest"
+USER_DEFAULT_KEY="crash-on-launch"
+DEVICE_ID="booted"
+SCREENSHOTS_DIR="test-crash-and-relaunch-simulator-screenshots"
+
+usage() {
+ echo "Usage: $0"
+ echo " -s|--screenshots-dir
Screenshots directory (default: test-crash-and-relaunch-simulator-screenshots)"
+ echo " -h|--help Show this help message"
+ exit 1
+}
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -s|--screenshots-dir)
+ SCREENSHOTS_DIR="$2"
+ shift 2
+ ;;
+ -h|--help)
+ usage
+ ;;
+ *)
+ echo "Unknown option: $1"
+ usage
+ ;;
+ esac
+done
+
+# Echo with timestamp
+log() {
+ echo "[$(date '+%H:%M:%S')] $1"
+}
+
+# Take screenshot with timestamp and custom name
+take_simulator_screenshot() {
+ local name="$1"
+
+ # Create screenshots directory if it doesn't exist
+ mkdir -p "$SCREENSHOTS_DIR"
+
+ # Generate timestamp-based filename with custom name
+ timestamp=$(date '+%H%M%S')
+ screenshot_name="$SCREENSHOTS_DIR/${timestamp}_${name}.png"
+
+ # Take screenshot
+ xcrun simctl io booted screenshot "$screenshot_name" 2>/dev/null || true
+}
+
+log "Removing previous screenshots directory."
+rm -rf "$SCREENSHOTS_DIR"
+
+log "Starting crash test and relaunch test."
+log "This test crashes the app and validates that it can relaunch after a crash without crashing again."
+
+log "🔨 Building SwiftUI Crash Test app for simulator 🔨"
+
+xcodebuild -workspace Sentry.xcworkspace \
+ -scheme SwiftUICrashTest \
+ -destination "platform=iOS Simulator,name=iPhone 16" \
+ -derivedDataPath DerivedData \
+ -configuration Debug \
+ CODE_SIGNING_REQUIRED=NO \
+ build 2>&1 | tee raw-build.log | xcbeautify
+
+log "Installing app on simulator."
+xcrun simctl install $DEVICE_ID DerivedData/Build/Products/Debug-iphonesimulator/SwiftUICrashTest.app
+
+take_simulator_screenshot "after-install"
+
+log "Terminating app if running."
+xcrun simctl terminate $DEVICE_ID $BUNDLE_ID 2>/dev/null || true
+
+# Phase 1: Let the app crash
+
+log "Setting crash flag."
+xcrun simctl spawn $DEVICE_ID defaults write $BUNDLE_ID $USER_DEFAULT_KEY -bool true
+
+log "Launching app with expected crash."
+xcrun simctl launch $DEVICE_ID $BUNDLE_ID
+
+# Check every 100ms for 5 seconds if the app is still running.
+for i in {1..50}; do
+ if xcrun simctl listapps $DEVICE_ID | grep "$BUNDLE_ID" | grep -q "Running"; then
+ sleep 0.1
+ else
+ log "✅ App crashed as expected after $(echo "scale=1; $i * 0.1" | bc) seconds."
+ break
+ fi
+
+ if [ "$i" -eq 50 ]; then
+ log "❌ App is still running after 5 seconds but it should have crashed instead."
+ exit 1
+ fi
+done
+
+take_simulator_screenshot "after-crash"
+
+# Phase 2: Test normal operation
+
+log "Removing crash flag..."
+xcrun simctl spawn $DEVICE_ID defaults delete $BUNDLE_ID $USER_DEFAULT_KEY
+
+log "Relaunching app after crash."
+xcrun simctl launch $DEVICE_ID $BUNDLE_ID
+
+take_simulator_screenshot "after-crash-check"
+
+log "Waiting for 5 seconds to check if the app is still running."
+sleep 5
+
+take_simulator_screenshot "after-crash-check-after-sleep"
+
+if xcrun simctl spawn booted launchctl list | grep "$BUNDLE_ID"; then
+ log "✅ App is still running"
+else
+ log "❌ App is not running"
+ exit 1
+fi
+
+log "✅ Test completed successfully."
+exit 0
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 92b9e059575..b6ac1adadeb 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -200,14 +200,6 @@ platform :ios do
)
end
- lane :ui_critical_tests_ios_swiftui_crash do
- run_ui_tests(
- scheme: "SwiftUITestSampleCrash",
- result_bundle_name: "ui_critical_tests_ios_swiftui_crash",
- device: "iPhone 16 (18.5)"
- )
- end
-
lane :ui_critical_tests_ios_swiftui_envelope do
run_ui_tests(
scheme: "SwiftUITestSampleEnvelope",