How To Fix Deep Link/Push Notifications Crashes On Unity iOS (and Why Software Design Matters)
A nasty crash appeared after the update from Unity 2021.3.31f1 to 2021.3.36f1, and the update was required due to the Apple privacy update and the introduction of Privacy manifest files since the support for the privacy manifest was added relatively recently. So you either have already encountered this crash or would encounter it soon if you publish your games to the App Store and use Facebook SDK and deep links or push notifications as the deadline for this update is really close.
Strangely it was reproduced only on iOS 15 and 16, while iOS 17 was okay.
It turned out to be an interesting investigation, so I wanted to share the details with you.
TL;DR
The reported issue: https://issuetracker.unity3d.com/issues/ios-crash-when-opening-app-using-a-deep-link-while-facebook-sdk-is-installed
The stacktrace:
Thread 0, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x35f)
0 UnityFramework`core::StringStorageDefault<char>::assign(char const*, unsigned long) + 28
1 UnityFramework`PlayerSettings::SetAbsoluteURL(core::basic_string<char, core::StringStorageDefault<char> > const&) + 44
2 UnityFramework`UnitySetAbsoluteURL + 100
3 UnityFramework`-[UnityAppController application:openURL:options:] + 192
The fix with a build post processor: https://github.com/facebook/facebook-sdk-for-unity/issues/712#issuecomment-2035022747
Into Details
Facebook SDK has a post-build processor that modifies the generated PBX project and one of the steps is to modify the UnityAppController.mm
which is responsible for the application lifecycle management. And it uses the regex to do this.
It’s interesting how the regex to replace the generated UnityAppController.mm in FB SDK has been sitting there since 2015 and only now fired so badly causing a crash on a subset of devices.
Why The Crash Appeared
It happened due to the UnityAppController.mm
template being updated in one of the newer Unity versions.
Let’s see how the method under discussion didFinishLaunchingWithOptions
from the template for UnityAppController.mm
looked before (Unity 2022.3.9f1):
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { ::printf("-> applicationDidFinishLaunching()\n"); // send notfications #if !PLATFORM_TVOS && !PLATFORM_VISIONOS if ([UIDevice currentDevice].generatesDeviceOrientationNotifications == NO) [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; #endif UnityInitApplicationNoGraphics(UnityDataBundleDir()); [self selectRenderingAPI]; [UnityRenderingView InitializeForAPI: self.renderingAPI]; #if !PLATFORM_VISIONOS if (@available(iOS 13, tvOS 13, *)) _window = [[UIWindow alloc] initWithWindowScene: [self pickStartupWindowScene: application.connectedScenes]]; else _window = [[UIWindow alloc] initWithFrame: [UIScreen mainScreen].bounds]; #else _window = [[UIWindow alloc] init]; #endif _unityView = [self createUnityView]; [DisplayManager Initialize]; _mainDisplay = [DisplayManager Instance].mainDisplay; [_mainDisplay createWithWindow: _window andView: _unityView]; [self createUI]; [self preStartUnity]; // if you wont use keyboard you may comment it out at save some memory [KeyboardDelegate Initialize]; // delay is needed so that the attach managed debugger window would be properly created when OS view is prepared to show it, // otherwise debug window will not appear and will cause application to be in frozen state. "startUnity" method after delay will be called on applicationDidBecomeActive // also this might introduce one black frame between launch screen and unity splash screen, but in most scenarios it will be not visible since the splash screen has black background itself [self performSelector: @selector(startUnity:) withObject: application afterDelay: 0]; return YES; }
As you can see there is no branching and the regex from FB SDK replaced the last line from return YES;
to return NO;
. There is unfortunately no info in the changelogs on why exactly the cold start feature needed this fix, but anyway, let’s look to the newer template for AppController.mm
from Unity 2022.3.20f1:
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { ::printf("-> applicationDidFinishLaunching()\n"); // send notfications #if !PLATFORM_TVOS && !PLATFORM_VISIONOS if ([UIDevice currentDevice].generatesDeviceOrientationNotifications == NO) [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; #endif if ([self isBackgroundLaunchOptions: launchOptions]) return YES; [self initUnityWithApplication: application]; return YES; } - (BOOL)isBackgroundLaunchOptions:(NSDictionary*)launchOptions { if (launchOptions.count == 0) return NO; // launch due to location event, the app likely will stay in background BOOL locationLaunch = [[launchOptions valueForKey: UIApplicationLaunchOptionsLocationKey] boolValue]; if (locationLaunch) return YES; return NO; }
So in the new template, the method is rewritten and now has branching. Also there is a new method isBackgroundLaunchOptions
added after the didFinishLaunchingWithOptions
. Let’s build the XCode project and see what we end up with:
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { ::printf("-> applicationDidFinishLaunching()\n"); // send notfications #if !PLATFORM_TVOS && !PLATFORM_VISIONOS if ([UIDevice currentDevice].generatesDeviceOrientationNotifications == NO) [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; #endif if ([self isBackgroundLaunchOptions: launchOptions]) return YES; [self initUnityWithApplication: application]; return YES; } - (BOOL)isBackgroundLaunchOptions:(NSDictionary*)launchOptions { if (launchOptions.count == 0) return NO; // launch due to location event, the app likely will stay in background BOOL locationLaunch = [[launchOptions valueForKey: UIApplicationLaunchOptionsLocationKey] boolValue]; if (locationLaunch) return YES; return YES; }
So now the regex falsely replaces the last line in isBackgroundLaunchOptions
from NO to YES and this leads to the crash.
It might seem like an easy thing to catch, but not gonna lie it took quite some time to pinpoint.
What makes it harder to investigate is that this crash happened instantly on the app start, so the tool to detect and send crash reports is not initialized yet. Since the Unity version upgrade was done due to the Apple privacy manifest requirement, it also means that a bunch of third-party plugins was updated as well to support it, and the first guess was that there was a conflict between native parts of plugins that affected push notifications or between custom native extensions on top of these plugins like a custom swizzler to handle Braze and Firebase functionality in one app. What is more, it didn’t look like a crash when checking the video with reproduction at first glance. So additional actions were needed to gather all the info and to figure out that it was indeed a crash and what caused it.
The Fix
To fix the crash we need to modify the code modified by Facebook SDK once again. Here is the solution in the form of a build post-process step that also fixes deep link processing: https://github.com/facebook/facebook-sdk-for-unity/issues/712#issuecomment-2035022747
[PostProcessBuild(10000)] public static void IOSBuildPostProcess(BuildTarget target, string pathToBuiltProject) { ... FixUniversalLinksColdStartBugInFacebookSDK(pathToBuiltProject); // Call this function from your IOSBuildPostProcess ... } private static void FixUniversalLinksColdStartBugInFacebookSDK(string path) { string isBackgroundLaunchOptions = @"(?x)(isBackgroundLaunchOptions:\(NSDictionary\*\)launchOptions(?:.*\n)+?\s*return\ )YES(\;\n\})# }"; string fullPath = Path.Combine(path, Path.Combine("Classes", "UnityAppController.mm")); string data = Load(fullPath); data = Regex.Replace( data, isBackgroundLaunchOptions, "$1NO$2"); Save(fullPath, data); static string Load(string fullPath) { string data; FileInfo projectFileInfo = new FileInfo(fullPath); StreamReader fs = projectFileInfo.OpenText(); data = fs.ReadToEnd(); fs.Close(); return data; } static void Save(string fullPath, string data) { System.IO.StreamWriter writer = new System.IO.StreamWriter(fullPath, false); writer.Write(data); writer.Close(); } }
How Does It Relate To The Software Design
This issue is a perfect example of “Unknown Unknowns” discussed in “A Philosophy of Software Design” by John Ousterhout.
Unknown unknowns: The third symptom of complexity is that it is not obvious which pieces of code must be modified to complete a task, or what information a developer must have to carry out the task successfully.
An unknown unknown means that there is something you need to know, but there is no way for you to find out what it is, or even whether there is an issue. You won’t find out about it until bugs appear after you make a change.
“A Philosophy of Software Design” by John Ousterhout
Unknown unknowns increase the complexity of your systems. However, when they occur between two third-party dependencies, tracking and resolving them becomes exponentially more challenging.
While these third-party libraries are obviously out of our control, I would recommend avoiding introducing unknown unknowns in your codebase. There are indeed many design principles that developers debate and argue about, and you might find some of them controversial in nature. However, I consider avoiding unknown unknowns as a principle that should be followed without any argument or controversy, as even small projects can easily suffer from bugs due to breaking this fundamental principle. Neglecting this principle in your codebase can lead to significant issues and headaches down the line, regardless of the size or complexity of your project.
Conclusion
According to the issue tracker it also happens in 2022 and 2023, so now you know what to do if your app starts crashing on iOS while opening via push notifications or deep links after a recent Unity update.
I encourage you to avoid “unknown unknowns” in your code to save you and your peers time in the future especially if your solution will be used by many developers.
Alexey Merzlikin
Experienced game developer and tech lead with a passion for writing educational content about game development and programming. I have over 10 years of industry experience focused on performance optimization, clean code practices, and robust game architecture. I share regular posts on my Telegram channel about applying software engineering best practices to build high-quality games. My goal is to help other game developers level up their skills.
You might also consider subscribing to receive updates about new blog posts via email: