Building apps with mobile .NET is great because most of the time it just works. But when it doesn’t, you suddenly need to become an expert in long chains of MSBuild calls.
One of the more complex chains involves creating C# bindings for native libraries. It used to be more straightforward, but with the rapid evolution of the .NET ecosystem and the transition away from Xamarin breaking some old workflows, fresh examples are hard to come by. LLMs do not help much either and keep giving outdated tips.
So, here’s a step-by-step guide on how to create bindings from scratch in .NET 9 and Xcode 16, even if you’ve never done it before. You can get the sources from Github or the package from Nuget.
Start by creating a new binding project for .NET 9:
dotnet new iosbinding -n "Lottie.iOS"
This generates just three files: Lottie.iOS.csproj
, ApiDefinition.cs
, and StructsAndEnums.cs
.
Open Lottie.iOS.csproj
in a text editor. You'll see this essential configuration:
<ItemGroup>
<ObjcBindingApiDefinition Include="ApiDefinition.cs"/>
<ObjcBindingCoreSource Include="StructsAndEnums.cs"/>
</ItemGroup>
Next, add a reference to the native framework (which we'll create shortly):
<ItemGroup>
<NativeReference Include="Lottie-Static.xcframework">
<Kind>Framework</Kind>
<Frameworks>CoreGraphics CoreImage Foundation QuartzCore UIKit</Frameworks>
</NativeReference>
</ItemGroup>
The <Frameworks>
element must list all frameworks that your native library depends on. You can look up these dependencies in the native project in the next step. (Note that in the case of Lottie, CoreAnimation isn't a separate framework in Apple's stack – it's part of QuartzCore.)
First, we need to compile the native library into a format that .NET can consume:
Lottie-iOS
target from the project rootIn Lottie's case, its package dependencies are also static libraries, so no additional configuration is needed. For other frameworks, check their Package.swift
files to understand their dependency structure.
Now build the framework for iOS devices. Run this from the Xcode project root:
xcodebuild archive \
-scheme "Lottie (iOS)" \
-destination 'generic/platform=iOS' \
-archivePath ./build/Lottie \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
You'll also need a simulator version for development:
xcodebuild archive \
-scheme "Lottie (iOS)" \
-destination 'generic/platform=iOS Simulator' \
-archivePath ./build/LottieSim \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
With both architectures ready, create the final XCFramework:
xcodebuild -create-xcframework \
-framework ./build/Lottie.xcarchive/Products/Library/Frameworks/Lottie.framework \
-framework ./build/LottieSim.xcarchive/Products/Library/Frameworks/Lottie.framework \
-output ./build/Lottie-Static.xcframework
The resulting Lottie-Static.xcframework
contains:
Info.plist
describing the framework's librariesios-arm64
directory for device buildsios-arm64_x86_64-simulator
directory for simulator buildsMove this XCFramework to your binding project's root directory.
The binding generation relies on Objective Sharpie, a closed-source tool that's showing its age. According to the .NET iOS/Mac team, a replacement is in the worksб but for now, this is what we’ve got.
Setting up Sharpie requires some specific dependencies:
brew install --cask objectivesharpie
With everything installed, generate the bindings from your project root:
sharpie bind \
--sdk=iphoneos18.1 \
--output=SharpieOutput \
--framework=/path/to/your/project/Lottie-Static.xcframework/ios-arm64/Lottie.framework
Sharpie creates ApiDefinitions.cs
and StructsAndEnums.cs
in the output directory. Move these to your project root, but watch out – the template creates ApiDefinition.cs
(singular), while Sharpie generates ApiDefinitions.cs
(plural). Rename accordingly.
Now comes the manual cleanup. Add your namespace to both files and review the generated code. Sharpie isn't perfect and often includes problematic exports. For instance, it adds initWithCoder:
methods to every class, which conflict with the methods added by the linker. You have to remove these manually.
I also encountered a particularly cryptic issue while binding Lottie: Sharpie exported CAAnimation
as a concrete type instead of the ICAAnimation
interface, causing linker crashes with the error "no type or protocol named 'Microsoft_iOS__CoreAnimation_CAAction'". Thanks to Rolf Bjarne from the .NET iOS team for helping me debug this one.
So, watch out for the types it generates: for most libraries, quickly reviewing ApiDefinition(s).cs
is not a big deal.
Build the release package:
dotnet build Lottie.iOS.csproj -c Release
The NuGet package appears in bin/Release
. If you're not publishing to NuGet, you can reference it locally from your iOS project:
dotnet add package QotoQot.Bindings.Lottie.iOS --source /PATH/TO/YOUR/PROJECT/Lottie.iOS/bin/Release
Caution: if you’re updating your package during development, make sure to bump its version and then re-add it. I forgot about NuGet caching and was puzzled for a while why my app kept crashing even after reinstalling the package.
I also tried referencing the binding project directly in my iOS app, but IDE support was unreliable – JetBrains Rider couldn't resolve the bound classes properly. Creating a Nuget package, even for local use, turned out to be simpler.
That's it, try using
your freshly created native bindings!