On a project my colleague @halkar and I were working on, we came across a puzzling issue. We only discovered this issue after converting our project from the Classic MonoTouch API to the Unified API. On the iOS simulator, our application would fun fine, but when we tried to run it against a real iOS device, we could get an error to the effect of “Native linking failed, undefined Objective-C class …”.
A quick search within our solution revealed no clues, and the subsequent Google search confirmed that EAAccessoryManager is indeed a native Objective-C class. So the class is clearly defined, but our compiler tells us otherwise?
As per the compiler error message, Google has multiple sources to suggest that the solution is to simply add a [Protocol] attribute to the offending C# class. However, here the code wasn’t in our application but in the Xamarin binding library that didn’t get linked to the corresponding Objective-C EAAccessoryManager.
Upon further investigation of the Xamarin.iOS Type Registrar to explain why the application worked in the simulator but not a real device, it turns out that Xamarin uses dynamic registration for simulator builds but static registration for device builds. The reason for this is speed, in that a development machine would have the necessary power to scan classes at runtime, whereas a new binary is always required to deploy to a device. Going down this path, a suggested solution is to use the legacy registrar, but this just results in a different compiler error indicating it is not allowed in a Unified API project.
So where to from here?
Enter the Xamarin Linker
This led to us having a deeper look at the Xamarin.iOS Linker. In short, our project compiled when we selected “Don’t link” for the Linker behaviour option. The default option is “Link SDK assemblies only”, which will mean the Xamarin linker will attempt to exclude all the symbols from the SDK it thinks is unnecessary. Unfortunately, it appeared in our case the static registration analysis was over-zealous. It should be mentioned also that the use of the word “Linker” by Xamarin is slightly counter-intuitive as noted by Adam Kemp and the behaviour is different to the conventional sense of the word. For my own understanding, I found it made more sense if it I substituted the word “Optimize” for “Link” – i.e. pick the “Don’t optimize” option!
But it has to be at least … three times bigger than this!
Please pardon the obscure Zoolander quote, but in practice you will need to consider the size of the generated IPA binary. It really depends on the exact nature of the application as to what the actual size difference would be, but roughly speaking it appears the “Don’t link” option can be 3-5 times larger than the default “Link SDK assemblies only”. App consumers these days are quite unforgiving, and so you really need to do everything in your power to provide a good first impression. This, of course, includes minimising the size of your app and therefore potential installation problems.
As such, the solution we settled for was to explicitly reference the EAAccessoryManager class in our code, allowing us to revert back to the“Link SDK assemblies only” option. This of course is merely a workaround, but it was the most elegant method we came up with to solve our problem. Hopefully Xamarin will address the issue with the Linker soon, but until then this workaround should do the trick.
using ExternalAccessory; ... var sharedAccessoryManager = EAAccessoryManager.SharedAccessoryManager;
Success! Time to go home.
Addendum – Xamarin Support
A big shout out to @kumpera and the Xamarin support team! We had a response within 24 hours after submitting a sample solution to replicate the problem. In our particular case, there were 2 issues that were at the root of the problem, neither of which was a fault with Xamarin. Firstly, a third party library we used did not contain ARM64 code, meaning linking failed when building for this architecture. Secondly, this library also had dependencies on frameworks such as the EAAccessoryManager. Anyway, instead of the workaround originally suggested, a better way to do achieve the same thing is to use the LinkWith attribute.
[assembly: LinkWith (..., Frameworks = "ExternalAccessory")]
Once this attribute was added and we selected ARMV7 as the architecture to build for, we no longer experienced the issue originally described.