Create and update slim/native platform interop bindings for iOS in .NET MAUI and .NET for iOS projects. Guides through creating Swift/Objective-C wrappers, configuring Xcode projects, generating C# API definitions, and integrating native iOS libraries using the Native Library Interop (NLI) approach. Use when asked about iOS bindings, xcframework integration, Swift interop, Objective Sharpie, or bridging native iOS SDKs to .NET.
Create and update slim/native platform interop bindings for iOS in .NET MAUI and .NET for iOS projects. Guides through creating Swift/Objective-C wrappers, configuring Xcode projects, generating C# API definitions, and integrating native iOS libraries using the Native Library Interop (NLI) approach. Use when asked about iOS bindings, xcframework integration, Swift interop, Objective Sharpie, or bridging native iOS SDKs to .NET.
When to use this skill
Activate this skill when the user asks:
How do I create iOS bindings for a native library?
How do I wrap an iOS SDK for use in .NET MAUI?
How do I create slim bindings for iOS?
How do I use Native Library Interop for iOS?
How do I bind a Swift library to .NET?
How do I use Objective Sharpie for iOS bindings?
How do I integrate an xcframework into .NET MAUI?
How do I create a Swift wrapper for a native iOS library?
How do I update iOS bindings when the native SDK changes?
How do I fix iOS binding build errors?
How do I expose native iOS APIs to C#?
How do I handle CocoaPods dependencies in iOS bindings?
How do I handle Swift Package Manager dependencies?
Overview
This skill guides the creation of Native Library Interop (Slim Bindings) for iOS. This modern approach creates a thin native Swift/Objective-C wrapper exposing only the APIs you need from a native iOS library, making bindings easier to create and maintain.
When to Use Slim Bindings vs Traditional Bindings
Scenario
Recommended Approach
Need only a subset of library functionality
Slim Bindings ✓
Easier maintenance when SDK updates
✓
Slim Bindings
Prefer working in Swift/Objective-C for wrapper
Slim Bindings ✓
Better isolation from breaking changes
Slim Bindings ✓
Need entire library API surface
Traditional Bindings
Creating bindings for third-party developers
Traditional Bindings
Already maintaining traditional bindings
Traditional Bindings
Inputs
Parameter
Required
Example
Notes
libraryName
yes
FirebaseMessaging, Lottie
Name of the native iOS library to bind
bindingProjectName
yes
MyBinding.MaciOS
Name for the C# binding project
dependencySource
no
cocoapods, spm, xcframework
How the native library is distributed
targetFrameworks
no
net9.0-ios;net9.0-maccatalyst
Target frameworks (default: latest .NET iOS + Mac Catalyst)
exposedApis
no
List of specific APIs
Which native APIs to expose (helps scope the wrapper)
Project Structure
The recommended project structure for Native Library Interop:
Step 1: Create Project Structure from Command Line
This section shows how to create the entire binding project structure using only command-line tools—no GUI or template cloning required.
Prerequisites
Install XcodeGen (generates Xcode projects from YAML):
brew install xcodegen
Create Directory Structure
# Set your binding name
BINDING_NAME="MyBinding"
# Create the full directory structure
mkdir -p ${BINDING_NAME}/macios/native/${BINDING_NAME}/${BINDING_NAME}
mkdir -p ${BINDING_NAME}/macios/${BINDING_NAME}.MaciOS.Binding
mkdir -p ${BINDING_NAME}/sample/MauiSample
cd ${BINDING_NAME}
cd macios/native/${BINDING_NAME}
xcodegen generate
cd ../../..
This creates MyBinding.xcodeproj with all the correct build settings.
Verify the Generated Project
# List the generated files
ls -la macios/native/${BINDING_NAME}/
# Verify the scheme was created and is shared
ls -la macios/native/${BINDING_NAME}/${BINDING_NAME}.xcodeproj/xcshareddata/xcschemes/
cd macios/${BINDING_NAME}.MaciOS.Binding
dotnet build
This will:
Invoke XcodeBuild to compile the native framework
Create the xcframework
Generate the C# binding assembly
Verify the Build Output
# Check that the xcframework was created
find bin -name "*.xcframework" -type d
# Find the generated Swift header (for updating ApiDefinition.cs later)
find bin -name "*-Swift.h" -type f
Optional: Add CocoaPods Support
If your native library uses CocoaPods dependencies:
Create Podfile
cat > macios/native/${BINDING_NAME}/Podfile << 'EOF'
platform :ios, '15.0'
target 'MyBinding' do
use_frameworks! :linkage => :static
# Add your pods here
# pod 'FirebaseMessaging', '~> 10.0'
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
end
end
end
EOF
Install Pods and Update Project Reference
cd macios/native/${BINDING_NAME}
pod install
cd ../../..
# Update the binding project to use xcworkspace instead of xcodeproj
sed -i '' 's/\.xcodeproj/\.xcworkspace/g' macios/${BINDING_NAME}.MaciOS.Binding/${BINDING_NAME}.MaciOS.Binding.csproj
Complete Script: Create Binding Project
Here's a complete bash script that creates everything:
If you prefer not to use XcodeGen, you can create a minimal Xcode project using plutil and direct file creation. However, this is more complex and error-prone.
Using Swift Package as Alternative
For simpler cases, you can use Swift Package Manager instead of an Xcode project:
cd macios/native
mkdir ${BINDING_NAME}
cd ${BINDING_NAME}
# Initialize Swift package
swift package init --type library --name ${BINDING_NAME}
# The binding project can reference the Package.swift
Then update the binding .csproj to use <XcodeProject> pointing to the directory containing Package.swift.
Note: The <XcodeProject> MSBuild item supports both .xcodeproj and Swift Package directories.
Step 5: Add Native Library Dependencies
Choose the appropriate method for your library's distribution:
Option A: CocoaPods
Create macios/native/MyBinding/Podfile:
platform :ios, '15.0'
target 'MyBinding' do
use_frameworks! :linkage => :static
# Add your native library pod
pod 'FirebaseMessaging', '~> 10.0'
# Add other dependencies as needed
pod 'FirebaseCore'
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
end
end
end
Install dependencies:
cd macios/native/MyBinding
pod install
# After this, open MyBinding.xcworkspace instead of .xcodeproj
import Foundation
import UIKit
import TheNativeLibrary // Import your native library
/// Main binding class exposed to .NET
/// The @objc attribute with explicit name ensures stable Objective-C naming
@objc(DotnetMyBinding)
public class DotnetMyBinding: NSObject {
// MARK: - Initialization
/// Initialize the native library
/// Call this from your .NET app's startup (e.g., MauiProgram.cs)
@objc(initializeWithApiKey:)
public static func initialize(apiKey: String) {
TheNativeLibrary.configure(withApiKey: apiKey)
}
/// Check if the library is initialized
@objc(isInitialized)
public static func isInitialized() -> Bool {
return TheNativeLibrary.isConfigured
}
// MARK: - Synchronous Methods
/// Get a simple value from the native library
@objc(getVersion)
public static func getVersion() -> String {
return TheNativeLibrary.version
}
/// Process data and return result
@objc(processDataWithInput:)
public static func processData(input: String) -> String? {
guard let result = TheNativeLibrary.process(input) else {
return nil
}
return result.stringValue
}
// MARK: - Asynchronous Methods (Completion Handlers)
/// Perform async operation with completion handler
/// .NET can use [Async] attribute to generate async/await version
@objc(fetchDataWithQuery:completion:)
public static func fetchData(
query: String,
completion: @escaping (String?, NSError?) -> Void
) {
TheNativeLibrary.fetch(query: query) { result in
switch result {
case .success(let data):
completion(data.stringValue, nil)
case .failure(let error):
completion(nil, error as NSError)
}
}
}
/// Async method with complex result data
@objc(performOperationWithConfig:completion:)
public static func performOperation(
config: NSDictionary,
completion: @escaping (NSData?, NSError?) -> Void
) {
guard let configDict = config as? [String: Any] else {
let error = NSError(
domain: "DotnetMyBinding",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "Invalid configuration"]
)
completion(nil, error)
return
}
TheNativeLibrary.performOperation(config: configDict) { result in
switch result {
case .success(let data):
completion(data, nil)
case .failure(let error):
completion(nil, error as NSError)
}
}
}
// MARK: - View Creation
/// Create a native view to embed in .NET MAUI
/// Return UIView for cross-platform compatibility
@objc(createViewWithFrame:)
public static func createView(frame: CGRect) -> UIView {
let nativeView = TheNativeLibrary.createCustomView()
nativeView.frame = frame
return nativeView
}
/// Create a configured view with options
@objc(createViewWithFrame:options:)
public static func createView(frame: CGRect, options: NSDictionary) -> UIView {
let config = options as? [String: Any] ?? [:]
let nativeView = TheNativeLibrary.createCustomView(options: config)
nativeView.frame = frame
return nativeView
}
// MARK: - Delegate/Callback Pattern
private static var callbackHandler: ((String) -> Void)?
/// Register a callback for events
/// .NET will pass an Action<string> that gets invoked
@objc(registerCallbackWithHandler:)
public static func registerCallback(handler: @escaping (String) -> Void) {
callbackHandler = handler
TheNativeLibrary.setEventHandler { event in
callbackHandler?(event.description)
}
}
/// Unregister the callback
@objc(unregisterCallback)
public static func unregisterCallback() {
callbackHandler = nil
TheNativeLibrary.setEventHandler(nil)
}
}
Swift Wrapper Design Guidelines
Type Mapping Rules
Only use types that .NET already knows how to marshal:
Swift Type
Objective-C Type
C# Type
String
NSString *
string
Bool
BOOL
bool
Int, Int32
int
int
Int64
long long
long
Double
double
double
Float
float
float
Data
NSData *
NSData
[String: Any]
NSDictionary *
NSDictionary
[Any]
NSArray *
NSArray
UIView
UIView *
UIView
UIImage
UIImage *
UIImage
URL
NSURL *
NSUrl
Custom Class
Must inherit NSObject
Interface with [BaseType]
Required Annotations
// Class: Must be public and have @objc with explicit name
@objc(ClassName)
public class ClassName: NSObject {
// Method: Must be public with @objc selector
@objc(methodNameWithParam:anotherParam:)
public func methodName(param: String, anotherParam: Int) -> Bool {
// Implementation
}
// Static method
@objc(staticMethodWithValue:)
public static func staticMethod(value: String) -> String {
// Implementation
}
// Property (read-only)
@objc(propertyName)
public var propertyName: String {
return "value"
}
// Property (read-write)
@objc
public var readWriteProperty: String = ""
}
Completion Handler Pattern
For async operations, use completion handlers that .NET can convert to async/await:
// Swift
@objc(operationWithInput:completion:)
public static func operation(
input: String,
completion: @escaping (String?, NSError?) -> Void // Result, Error
) {
// Async work...
DispatchQueue.main.async {
completion(result, nil) // Success
// OR
completion(nil, error as NSError) // Failure
}
}
// C# ApiDefinition.cs - Add [Async] for automatic async wrapper
[Static]
[Export("operationWithInput:completion:")]
[Async]
void Operation(string input, Action<string?, NSError?> completion);
// Usage in C#
var result = await DotnetMyBinding.OperationAsync("input");
Error Handling Pattern
Always convert errors to NSError for proper propagation:
After building, find the generated Objective-C header:
# Find the Swift header
find bin -name "*-Swift.h" -type f
# Typical location:
# bin/Debug/net9.0-ios/MyBinding.MaciOS.Binding.resources/
# MyBindingiOS.xcframework/ios-arm64/MyBinding.framework/Headers/MyBinding-Swift.h
Generate ApiDefinition.cs with Objective Sharpie
Install Objective Sharpie if not already installed:
brew install --cask objectivesharpie
Check available iOS SDKs:
sharpie xcode -sdks
Generate bindings:
# Set variables for clarity
HEADER_PATH="bin/Debug/net9.0-ios/MyBinding.MaciOS.Binding.resources/MyBindingiOS.xcframework/ios-arm64/MyBinding.framework/Headers/MyBinding-Swift.h"
SDK_VERSION="iphoneos18.0" # Use your installed SDK version
NAMESPACE="MyBinding"
sharpie bind \
--output=sharpie-output \
--namespace=$NAMESPACE \
--sdk=$SDK_VERSION \
--scope=Headers \
"$HEADER_PATH"