Binding Native Libraries in .NET MAUI 10: iOS Frameworks and Android AAR Bindings (2026)
Bind iOS XCFrameworks and Android AARs in .NET MAUI 10 with Objective Sharpie, Metadata.xml transforms, and a single-NuGet package. Real errors, real fixes.
Binding native libraries in .NET MAUI 10 means wrapping a platform SDK (an iOS .xcframework or an Android .aar) in a C# projection so you can call it from shared code. On iOS, you use an iOS Binding Library project driven by ApiDefinition.cs, and Objective Sharpie generates roughly 80% of that file from headers. On Android, the binding generator consumes the AAR directly, and you patch its rough edges with Metadata.xml. For most 2026 apps, though, the pragmatic answer is Native Library Interop from the Community Toolkit, a slim wrapper approach that beats binding the whole SDK.
Two viable approaches in .NET MAUI 10: traditional bindings (full API surface, more maintenance) or Native Library Interop (thin native wrapper, smaller surface, easier upgrades).
On iOS you must ship an .xcframework. Objective Sharpie's sharpie bind generates ApiDefinition.cs and StructsAndEnums.cs, but 20% of the file needs manual cleanup.
Swift-only SDKs cannot be bound directly. .NET MAUI 10 binds Objective-C headers, so Swift APIs must be exposed via @objc in a wrapper framework.
On Android, keep dependency AARs out of the binding project. Put them in the MAUI app as <AndroidLibrary> items to avoid a Metadata.xml nightmare.
Sign your XCFramework and align SupportedOSPlatformVersion. Mismatched min-OS is the top reason binding NuGets fail on real devices in 2026.
When you actually need a binding
Before you write a single line of ApiDefinition.cs, ask whether you actually need a binding. Honestly, nine times out of ten I see teams reach for a full binding project when a well-written handler, a platform-specific partial class, or a P/Invoke would have done the job in an afternoon. If the SDK you need to call exposes a handful of methods (say a fraud-scoring SDK with two entry points), do not bind it. Write a Swift wrapper on iOS, a Kotlin wrapper on Android, expose those wrappers as a single class, and consume them via native APIs, similar to how you'd wire up Firebase and APNs push notifications in .NET MAUI. That's exactly what Microsoft calls Native Library Interop, and it exists because the classic binding path is painful.
You need a real binding when: (a) the SDK vendor doesn't ship a .NET or MAUI package, (b) you need a large fraction of the SDK's public API in C# (think a full-featured barcode SDK, a payment terminal library, or a scientific instrument driver), and (c) you plan to redistribute the wrapper as a NuGet package for other teams. If only one screen calls one method, skip everything below and use a wrapper. If you're shipping a reusable NuGet, keep reading. This is the guide I wish I'd had when I did my first serious iOS framework binding back in the Xamarin days.
Traditional binding vs. Native Library Interop
Microsoft ships two officially-supported paths for consuming native SDKs from .NET MAUI 10, and picking the right one saves weeks of pain. Traditional bindings project every public symbol in the SDK into C#. Native Library Interop projects only the symbols you chose to expose in a thin native wrapper. The Community Toolkit page for Native Library Interop is blunt about the tradeoff: Microsoft Learn recommends the wrapper approach for most consumer scenarios, and reserves full bindings for cases where you truly need the whole API surface.
Dimension
Traditional Binding
Native Library Interop
API surface exposed
Everything the vendor makes public
Only the methods you expose in the wrapper
iOS effort
Objective Sharpie + hand-editing ApiDefinition.cs
Small Swift @objc wrapper, sharpie against your wrapper
Android effort
Full AAR consumption + Metadata.xml transforms
Small Kotlin wrapper class, no Metadata.xml battles
Vendor SDK upgrade cost
Rebuild binding, fix breakages across whole API
Only breaks if wrapper's methods change
Swift support
Manual wrapper still required
Native. Write wrapper in Swift directly.
Best for
Redistributable NuGets covering full SDKs
App-specific integrations, most 2026 use cases
I switched most of my client projects to Native Library Interop in early 2025 and haven't looked back. The compile-time story is better, the diff you review when the vendor ships an update is smaller, and the trickle of Swift-only SDKs (Stripe Terminal, most Apple sample code, several fintech SDKs) becomes tractable. The Maui.NativeLibraryInterop template gives you an Android Studio project, an Xcode project, and two binding projects ready to build. Clone it, rename the projects, and the scaffolding is done in about ten minutes.
How do I bind an iOS framework in .NET MAUI 10?
To bind an iOS framework in .NET MAUI 10, create an iOS Binding Library project, ship the vendor's SDK as an .xcframework (fat .framework files are no longer accepted for App Store submission), run Objective Sharpie against its Objective-C headers to generate ApiDefinition.cs, then reference the binding project from your MAUI app. Here is the minimal .csproj. Pay attention to the SupportedOSPlatformVersion, which must be less than or equal to the min-OS the vendor's SDK supports:
The Frameworks attribute lists every Apple framework the vendor's SDK depends on. Miss one and you get an unresolved-symbol linker error at MAUI app build time (not at binding build time, which is what makes this so annoying to debug the first time). The vendor's docs should list required frameworks. If they don't, run otool -L Vendor/VendorSDK.xcframework/ios-arm64/VendorSDK.framework/VendorSDK and inspect the output.
# list available SDKs
sharpie xcode -sdks
# generate bindings against iphoneos 18 headers
sharpie bind \
-sdk iphoneos18.0 \
-output ./Generated \
-namespace VendorSDK \
-scope ./Vendor/VendorSDK.xcframework/ios-arm64/VendorSDK.framework/Headers \
./Vendor/VendorSDK.xcframework/ios-arm64/VendorSDK.framework/Headers/VendorSDK.h
Copy Generated/ApiDefinitions.cs to your binding project as ApiDefinition.cs, and Generated/StructsAndEnums.cs as-is. Now open ApiDefinition.cs. Sharpie will have flagged the ~20% of cases it couldn't translate cleanly with [Verify] attributes. Every [Verify] is a call to action: read the vendor's Objective-C header, decide whether the sharpie-generated projection is correct, and either remove the [Verify] attribute (accepting the projection) or rewrite the declaration. Never ship a binding with [Verify] attributes still in it. Compiler warnings will remind you of this.
How do I bind a Swift library in .NET MAUI?
You can't bind a Swift library directly in .NET MAUI 10. The binding generator only understands Objective-C headers. What you can do (and what most 2026 iOS SDKs force you to do) is write a thin Swift wrapper framework that exposes the Swift API to Objective-C via the @objc attribute, then bind that wrapper. This is the only workable path for pure-Swift SDKs like Stripe Terminal, SwiftUI-only components, or anything vendored as a .swiftmodule.
In Xcode, create a new Framework target with Swift as the language and Objective-C compatibility enabled. Then wrap the Swift API:
import Foundation
import VendorSwiftSDK
@objc(MauiVendorClient)
public class MauiVendorClient: NSObject {
private let client: VendorClient
@objc public init(apiKey: String) {
self.client = VendorClient(apiKey: apiKey)
super.init()
}
@objc public func scanBarcode(completion: @escaping (String?, Error?) -> Void) {
client.scan { result in
switch result {
case .success(let payload): completion(payload, nil)
case .failure(let error): completion(nil, error)
}
}
}
}
Two things matter here. First, every class you want to expose must inherit from NSObject and every member must carry @objc(SomeName). Second, avoid Swift-only types (Result, tuples, structs) in method signatures. Projected to Objective-C they become opaque, and you lose the ability to call them from C#. Convert to callbacks, primitives, and NSError. Then build the wrapper to an .xcframework:
BUILD_LIBRARIES_FOR_DISTRIBUTION=YES is not optional. Without it, the framework will only load against the exact Swift compiler version it was built with, which will bite you within one Xcode release cycle. Run sharpie bind against the wrapper's generated MauiVendorClient-Swift.h header (Xcode emits this next to the compiled framework) and you get a clean ApiDefinition.cs. This mirrors the strategy I use for iOS privacy manifests in .NET MAUI 10: always ship the wrapper's PrivacyInfo.xcprivacy inside the XCFramework, not in the MAUI app.
How do I bind an Android AAR in .NET MAUI 10?
To bind an Android AAR in .NET MAUI 10, create an Android Binding Library project, add the vendor's AAR as an <AndroidLibrary> item, and let the binding generator project every public Java class into C#. There's no Objective Sharpie equivalent required. The Android tooling reads bytecode directly and infers signatures. In practice, though, expect to spend most of your time in the Transforms/ folder fixing binding-generator quirks.
The single-biggest footgun on Android is dependency AARs. If the vendor's SDK depends on, say, OkHttp and Kotlin coroutines, do not add those AARs to the binding project. Add them to the consuming MAUI app project as <AndroidLibrary Bind="false"> or as NuGet PackageReferences where wrappers exist. If you drop them into the binding project, the generator tries to project OkHttp and coroutines into C# too, and you'll be staring at 2,000+ binding errors in Metadata.xml for classes you never wanted to call. (I hit this exact bug shipping a payment SDK binding in 2023, and it took two days to unwind.)
Fixing Metadata.xml errors
Metadata.xml is the pressure valve between raw class-parse output and a C#-clean binding. It lives in the Transforms/ folder of an Android binding project and applies XPath-based transformations against api.xml, the intermediate file the generator produces before it emits C#. In .NET MAUI 10 the generator has gotten smarter (Kotlin nullability annotations now project to C# nullable reference types by default), but the following transformation categories still come up on almost every non-trivial binding.
Rename an ill-named Java package to a C#-friendly namespace, remove internal packages you don't want to expose, and rename Java methods that collide with C# keywords:
<metadata>
<attr path="/api/package[@name='com.vendor.sdk']"
name="managedName">Vendor.Sdk</attr>
<!-- Remove a package the vendor exports but you don't want in C# -->
<remove-node path="/api/package[@name='com.vendor.sdk.internal']" />
<!-- Fix a method that returns Object where a specific type is expected -->
<attr path="/api/package[@name='com.vendor.sdk']/class[@name='Client']/method[@name='getConfig']"
name="managedReturn">Vendor.Sdk.ClientConfig</attr>
<!-- Rename a Java 'delete' method that clashes with C# 'delete' keyword -->
<attr path="/api/package[@name='com.vendor.sdk']/class[@name='Store']/method[@name='delete']"
name="managedName">Remove</attr>
<!-- Change accessibility so an internal helper isn't projected as public -->
<attr path="/api/package[@name='com.vendor.sdk']/class[@name='Internal']"
name="visibility">internal</attr>
</metadata>
Whenever dotnet build spits out a BG8* or BG4* error, the fix almost always lives in Metadata.xml. BG8A00 ("Skipping X due to missing dependency") means class-parse couldn't resolve a type. Either add the dependency AAR to the app project so the type resolves at link time, or remove-node the offending class. BG4207 ("Duplicate method") usually means the vendor overloaded a Java method in a way that produces the same C# signature after nullability erasure; attr the collision with managedName to rename one side.
For int-based Java constants, use EnumFields.xml to project them as a proper C# enum instead of loose consts. This is the difference between a binding that feels native in C# and one that feels like translated Java. Consumers of your binding (especially anyone using XAML source generation in .NET MAUI 10) will thank you when they can data-bind against a real enum in XAML.
Packaging both platforms into a single NuGet
When your iOS and Android bindings both work in a sample app, package them together as a single NuGet consumers can add with one PackageReference. The trick is a multi-target library project that references the iOS binding, the Android binding, and (optionally) net-standard shims so non-mobile targets don't error out at restore time.
Run dotnet pack -c Release. The resulting .nupkg contains three lib folders (net10.0, net10.0-ios, net10.0-android) and consumers get the right binaries by TFM. If you also need to ship the raw XCFramework so consumers can inspect it (some enterprise pipelines require it for their own re-signing steps), add a <None Include="..\Vendor.iOS.Binding\Vendor\Vendor.xcframework\**" Pack="true" PackagePath="native\ios\" /> entry.
Debugging unresolved symbols and linker errors
The most common bug I diagnose in shipped bindings is the "works on simulator, crashes on device" class, and the root cause is almost always a mismatched architecture in the XCFramework or a missing dependency in <NativeReference Frameworks="...">. Start with otool -l on the framework binary:
# Confirm architectures included
lipo -info Vendor.xcframework/ios-arm64/Vendor.framework/Vendor
# List load commands, look for MinOS version
otool -l Vendor.xcframework/ios-arm64/Vendor.framework/Vendor | grep -A 3 LC_BUILD_VERSION
# List linked frameworks - every one must be in your Frameworks="..." attribute
otool -L Vendor.xcframework/ios-arm64/Vendor.framework/Vendor
If lipo -info shows only arm64, the framework won't load in the iOS simulator on an Apple Silicon Mac (simulator needs arm64-simulator, which lives in the ios-arm64_x86_64-simulator slice of the XCFramework). If LC_BUILD_VERSION reports a minOS higher than your MAUI project's SupportedOSPlatformVersion, the App Store will accept the build but device installs will fail on older iOS.
On Android, the equivalent debugging story runs through bundletool and zipalign. When your MAUI app builds fine but throws UnsatisfiedLinkError at startup, the vendor SDK ships JNI .so files that your app is stripping out during the R8/D8 linker pass. Fix it in the app project (not the binding project) by adding <AndroidPackageNativeLibraries>True</AndroidPackageNativeLibraries> and confirming the vendor's ABI list (arm64-v8a, x86_64) matches your <AndroidSupportedAbis>. If crashes still slip through in production, wire up crash symbolication with dSYMs and R8 mappings so you can actually read the native frames.
Frequently Asked Questions
Does .NET MAUI 10 support XCFrameworks?
Yes. XCFramework is now the only supported format for iOS Binding Libraries in .NET MAUI 10. Legacy .framework and fat-binary formats are still accepted by the tooling, but the App Store's asset validation will reject them at submission. Ship .xcframework from day one and include both device and simulator slices.
Can I bind an Android AAR that has dependencies?
Yes, but you shouldn't put the dependency AARs in the binding project. Add them to your MAUI app project as <AndroidLibrary Bind="false">, or reference their NuGet wrappers where available. The binding project should only contain the single vendor AAR you actually want to project into C#.
Why does my iOS binding fail with "undefined symbols" at link time?
Almost always because <NativeReference> is missing a required system framework in its Frameworks attribute. Run otool -L against the vendor's framework binary and add every /System/Library/Frameworks/*.framework entry you find to your Frameworks="..." list, space-separated.
Is Native Library Interop better than a full binding?
For most consumer scenarios in 2026, yes. It exposes a smaller, curated API surface, sidesteps Swift-binding pain, and survives vendor SDK upgrades more cleanly. Full bindings are still the right choice when you're distributing a reusable NuGet meant to cover the vendor's whole public API, or when the consuming team needs every class the vendor exposes.
How do I keep binding projects working across .NET MAUI upgrades?
Pin your SupportedOSPlatformVersion, keep a device-tested sample app in the same solution, and rebuild against every .NET MAUI 10 SR release before promoting the binding to production. The Microsoft binding-generator changes between .NET releases are usually source-compatible, but occasionally shift Metadata.xml semantics. The sample app is your regression net.
A working guide to shipping .NET MAUI 10 apps with Fastlane: match-based iOS signing, API-key auth for TestFlight, staged Play Store rollouts, and Fastfile lanes that survive real teams.
Stop the soft keyboard from covering inputs in .NET MAUI 10. Practical Android windowSoftInputMode setup, iOS keyboard avoidance, edge-to-edge Android 15 fixes, and a reusable KeyboardAvoidingView pattern that works across both platforms.
Map stripped .NET MAUI 10 crashes back to source: where dSYMs and R8 mapping.txt land, how to upload to Sentry and Crashlytics, and how to automate it in CI without shipping unsymbolicated builds.