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:
MyBinding/ ├── macios/ │ ├── native/ │ │ └── MyBinding/ # Xcode project │ │ ├── MyBinding.xcodeproj/ │ │ │ └── project.pbxproj │ │ ├── MyBinding/ │ │ │ └── DotnetMyBinding.swift # Swift wrapper implementation │ │ └── Podfile # If using CocoaPods │ │ └── Package.swift # If using Swift Package Manager │ └── MyBinding.MaciOS.Binding/ │ ├── MyBinding.MaciOS.Binding.csproj │ └── ApiDefinition.cs ├── sample/ │ └── MauiSample/ # Sample MAUI app │ ├── MauiSample.csproj │ └── MainPage.xaml.cs └── README.md
Step-by-step Process
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}
Step 2: Create the Xcode Project with XcodeGen
Create the XcodeGen Project Spec
Create macios/native/${BINDING_NAME}/project.yml :
cat > macios/native/${BINDING_NAME}/project.yml << 'EOF' name: MyBinding options: bundleIdPrefix: com.example deploymentTarget: iOS: "15.0" macOS: "12.0" xcodeVersion: "15.0" generateEmptyDirectories: true
settings: base: MARKETING_VERSION: "1.0.0" CURRENT_PROJECT_VERSION: "1" BUILD_LIBRARY_FOR_DISTRIBUTION: YES SKIP_INSTALL: NO MACH_O_TYPE: staticlib SWIFT_VERSION: "5.0" ENABLE_BITCODE: NO DEFINES_MODULE: YES
targets: MyBinding: type: framework platform: iOS sources: - path: MyBinding type: group settings: base: INFOPLIST_FILE: MyBinding/Info.plist PRODUCT_BUNDLE_IDENTIFIER: com.example.mybinding PRODUCT_NAME: MyBinding TARGETED_DEVICE_FAMILY: "1,2" scheme: gatherCoverageData: false shared: true EOF
Create the Swift Source File
Create the Swift wrapper file:
cat > macios/native/${BINDING_NAME}/${BINDING_NAME}/Dotnet${BINDING_NAME}.swift << 'EOF' import Foundation import UIKit
/// Main binding class exposed to .NET @objc(DotnetMyBinding) public class DotnetMyBinding: NSObject {
/// Initialize the native library
@objc(initialize)
public static func initialize() {
// Initialize your native library here
print("MyBinding initialized")
}
/// Example synchronous method
@objc(getVersion)
public static func getVersion() -> String {
return "1.0.0"
}
/// Example async method with completion handler
@objc(fetchDataWithQuery:completion:)
public static func fetchData(
query: String,
completion: @escaping (String?, NSError?) -> Void
) {
// Simulate async operation
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
completion("Result for: \(query)", nil)
}
}
/// Example view creation
@objc(createViewWithFrame:)
public static func createView(frame: CGRect) -> UIView {
let view = UIView(frame: frame)
view.backgroundColor = .systemBlue
return view
}
} EOF
Create Info.plist
cat > macios/native/${BINDING_NAME}/${BINDING_NAME}/Info.plist << 'EOF' <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>CFBundleDevelopmentRegion</key> <string>$(DEVELOPMENT_LANGUAGE)</string> <key>CFBundleExecutable</key> <string>$(EXECUTABLE_NAME)</string> <key>CFBundleIdentifier</key> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> <key>CFBundleName</key> <string>$(PRODUCT_NAME)</string> <key>CFBundlePackageType</key> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <key>CFBundleShortVersionString</key> <string>$(MARKETING_VERSION)</string> <key>CFBundleVersion</key> <string>$(CURRENT_PROJECT_VERSION)</string> <key>NSPrincipalClass</key> <string></string> </dict> </plist> EOF
Generate the Xcode Project
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/
Step 3: Create the C# Binding Project
Create the Binding .csproj
cat > macios/${BINDING_NAME}.MaciOS.Binding/${BINDING_NAME}.MaciOS.Binding.csproj << 'EOF' <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFrameworks>net9.0-ios;net9.0-maccatalyst</TargetFrameworks> <Nullable>enable</Nullable> <ImplicitUsings>true</ImplicitUsings> <IsBindingProject>true</IsBindingProject>
<!-- Package metadata -->
<PackageId>MyBinding.MaciOS</PackageId>
<Version>1.0.0</Version>
<Authors>Your Name</Authors>
<Description>iOS bindings for MyBinding</Description>
</PropertyGroup>
<!-- Reference the Xcode project --> <ItemGroup> <XcodeProject Include="../native/MyBinding/MyBinding.xcodeproj"> <SchemeName>MyBinding</SchemeName> </XcodeProject> </ItemGroup>
<!-- API definition --> <ItemGroup> <ObjcBindingApiDefinition Include="ApiDefinition.cs" /> </ItemGroup> </Project> EOF
Create Initial ApiDefinition.cs
cat > macios/${BINDING_NAME}.MaciOS.Binding/ApiDefinition.cs << 'EOF' using System; using Foundation; using ObjCRuntime; using UIKit;
namespace MyBinding { // @interface DotnetMyBinding : NSObject [BaseType(typeof(NSObject))] interface DotnetMyBinding { // +(void)initialize; [Static] [Export("initialize")] void Initialize();
// +(NSString * _Nonnull)getVersion;
[Static]
[Export("getVersion")]
string GetVersion();
// +(void)fetchDataWithQuery:(NSString * _Nonnull)query completion:(void (^ _Nonnull)(NSString * _Nullable, NSError * _Nullable))completion;
[Static]
[Export("fetchDataWithQuery:completion:")]
[Async]
void FetchData(string query, Action<string?, NSError?> completion);
// +(UIView * _Nonnull)createViewWithFrame:(CGRect)frame;
[Static]
[Export("createViewWithFrame:")]
UIView CreateView(CGRect frame);
}
} EOF
Step 4: Build and Verify
Build the Binding Project
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:
#!/bin/bash set -e
Configuration
BINDING_NAME="${1:-MyBinding}" BUNDLE_ID_PREFIX="${2:-com.example}" MIN_IOS_VERSION="${3:-15.0}"
echo "Creating iOS binding project: ${BINDING_NAME}"
Check prerequisites
if ! command -v xcodegen &> /dev/null; then echo "Installing xcodegen..." brew install xcodegen fi
Create directory structure
mkdir -p ${BINDING_NAME}/macios/native/${BINDING_NAME}/${BINDING_NAME} mkdir -p ${BINDING_NAME}/macios/${BINDING_NAME}.MaciOS.Binding cd ${BINDING_NAME}
Create XcodeGen project spec
cat > macios/native/${BINDING_NAME}/project.yml << EOF name: ${BINDING_NAME} options: bundleIdPrefix: ${BUNDLE_ID_PREFIX} deploymentTarget: iOS: "${MIN_IOS_VERSION}" macOS: "12.0" xcodeVersion: "15.0" generateEmptyDirectories: true
settings: base: MARKETING_VERSION: "1.0.0" CURRENT_PROJECT_VERSION: "1" BUILD_LIBRARY_FOR_DISTRIBUTION: YES SKIP_INSTALL: NO MACH_O_TYPE: staticlib SWIFT_VERSION: "5.0" ENABLE_BITCODE: NO DEFINES_MODULE: YES
targets: ${BINDING_NAME}: type: framework platform: iOS sources: - path: ${BINDING_NAME} type: group settings: base: INFOPLIST_FILE: ${BINDING_NAME}/Info.plist PRODUCT_BUNDLE_IDENTIFIER: ${BUNDLE_ID_PREFIX}.${BINDING_NAME,,} PRODUCT_NAME: ${BINDING_NAME} TARGETED_DEVICE_FAMILY: "1,2" scheme: gatherCoverageData: false shared: true EOF
Create Swift wrapper
cat > macios/native/${BINDING_NAME}/${BINDING_NAME}/Dotnet${BINDING_NAME}.swift << EOF import Foundation import UIKit
@objc(Dotnet${BINDING_NAME}) public class Dotnet${BINDING_NAME}: NSObject {
@objc(initialize)
public static func initialize() {
print("${BINDING_NAME} initialized")
}
@objc(getVersion)
public static func getVersion() -> String {
return "1.0.0"
}
} EOF
Create Info.plist
cat > macios/native/${BINDING_NAME}/${BINDING_NAME}/Info.plist << 'EOF' <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>CFBundleDevelopmentRegion</key> <string>$(DEVELOPMENT_LANGUAGE)</string> <key>CFBundleExecutable</key> <string>$(EXECUTABLE_NAME)</string> <key>CFBundleIdentifier</key> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> <key>CFBundleName</key> <string>$(PRODUCT_NAME)</string> <key>CFBundlePackageType</key> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <key>CFBundleShortVersionString</key> <string>$(MARKETING_VERSION)</string> <key>CFBundleVersion</key> <string>$(CURRENT_PROJECT_VERSION)</string> </dict> </plist> EOF
Generate Xcode project
cd macios/native/${BINDING_NAME} xcodegen generate cd ../../..
Create binding .csproj
cat > macios/${BINDING_NAME}.MaciOS.Binding/${BINDING_NAME}.MaciOS.Binding.csproj << EOF <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFrameworks>net9.0-ios;net9.0-maccatalyst</TargetFrameworks> <Nullable>enable</Nullable> <ImplicitUsings>true</ImplicitUsings> <IsBindingProject>true</IsBindingProject> <PackageId>${BINDING_NAME}.MaciOS</PackageId> <Version>1.0.0</Version> </PropertyGroup>
<ItemGroup> <XcodeProject Include="../native/${BINDING_NAME}/${BINDING_NAME}.xcodeproj"> <SchemeName>${BINDING_NAME}</SchemeName> </XcodeProject> </ItemGroup>
<ItemGroup> <ObjcBindingApiDefinition Include="ApiDefinition.cs" /> </ItemGroup> </Project> EOF
Create ApiDefinition.cs
cat > macios/${BINDING_NAME}.MaciOS.Binding/ApiDefinition.cs << EOF using System; using Foundation; using ObjCRuntime; using UIKit;
namespace ${BINDING_NAME} { [BaseType(typeof(NSObject))] interface Dotnet${BINDING_NAME} { [Static] [Export("initialize")] void Initialize();
[Static]
[Export("getVersion")]
string GetVersion();
}
} EOF
echo "" echo "✅ Created ${BINDING_NAME} binding project!" echo "" echo "Structure:" find . -type f -name ".swift" -o -name ".cs" -o -name "*.csproj" -o -name "project.yml" | sort echo "" echo "Next steps:" echo " 1. cd ${BINDING_NAME}/macios/${BINDING_NAME}.MaciOS.Binding" echo " 2. dotnet build" echo " 3. Add your native library code to Dotnet${BINDING_NAME}.swift" echo " 4. Update ApiDefinition.cs to match your Swift API"
Save as create-ios-binding.sh and run:
chmod +x create-ios-binding.sh ./create-ios-binding.sh MyAwesomeBinding com.mycompany 15.0
Alternative: Create Xcode Project Manually (Without XcodeGen)
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
Option B: Swift Package Manager
In Xcode:
-
File → Add Package Dependencies
-
Enter the package repository URL
-
Select version rules
-
Add to your target
Or create Package.swift :
// swift-tools-version:5.9 import PackageDescription
let package = Package( name: "MyBinding", platforms: [.iOS(.v15), .macCatalyst(.v15)], products: [ .library(name: "MyBinding", type: .static, targets: ["MyBinding"]) ], dependencies: [ .package(url: "https://github.com/example/SomeLibrary.git", from: "1.0.0") ], targets: [ .target( name: "MyBinding", dependencies: [ .product(name: "SomeLibrary", package: "SomeLibrary") ] ) ] )
Option C: Manual XCFramework
-
Drag the .xcframework into the Xcode project
-
Ensure "Copy items if needed" is checked
-
Add to target's "Frameworks and Libraries" section
-
Set "Embed" to Do Not Embed (for static linking)
Step 6: Implement the Swift Wrapper
Create macios/native/MyBinding/MyBinding/DotnetMyBinding.swift :
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:
@objc(riskyOperationWithCompletion:) public static func riskyOperation(completion: @escaping (Bool, NSError?) -> Void) { do { try TheNativeLibrary.riskyOperation() completion(true, nil) } catch { let nsError = NSError( domain: "DotnetMyBinding", code: (error as NSError).code, userInfo: [ NSLocalizedDescriptionKey: error.localizedDescription, NSUnderlyingErrorKey: error ] ) completion(false, nsError) } }
Step 7: Create the C# Binding Project (If Not Using Script)
If you created the project using the script in Step 1-4, skip to Step 8.
Create macios/MyBinding.MaciOS.Binding/MyBinding.MaciOS.Binding.csproj :
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFrameworks>net9.0-ios;net9.0-maccatalyst</TargetFrameworks> <Nullable>enable</Nullable> <ImplicitUsings>true</ImplicitUsings> <IsBindingProject>true</IsBindingProject>
<!-- Optional: Package metadata for NuGet -->
<PackageId>MyBinding.MaciOS</PackageId>
<Version>1.0.0</Version>
<Authors>Your Name</Authors>
<Description>iOS bindings for MyLibrary</Description>
</PropertyGroup>
<!-- Reference the Xcode project - MSBuild will build it automatically --> <ItemGroup> <XcodeProject Include="../native/MyBinding/MyBinding.xcodeproj"> <SchemeName>MyBinding</SchemeName> <!-- Optional overrides --> <!-- <Configuration>Release</Configuration> --> <!-- <Kind>Framework</Kind> --> <!-- <SmartLink>true</SmartLink> --> </XcodeProject> </ItemGroup>
<!-- If using xcworkspace (CocoaPods), reference it instead --> <!-- <ItemGroup> <XcodeProject Include="../native/MyBinding/MyBinding.xcworkspace"> <SchemeName>MyBinding</SchemeName> </XcodeProject> </ItemGroup> -->
<!-- API definition file --> <ItemGroup> <ObjcBindingApiDefinition Include="ApiDefinition.cs" /> </ItemGroup> </Project>
XcodeProject Properties
Property Description Default
SchemeName
Xcode scheme to build Required
Configuration
Build configuration Release
Kind
Framework or Static
Auto-detected
SmartLink
Enable smart linking true
ForceLoad
Force load all symbols false
Step 8: Build and Generate API Definition
Initial Build
Build the binding project to compile the native framework:
cd macios/MyBinding.MaciOS.Binding dotnet build
This creates the xcframework at:
bin/Debug/net9.0-ios/MyBinding.MaciOS.Binding.resources/MyBindingiOS.xcframework/
Locate the Generated Swift Header
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"
Review and Clean Up Generated Code
The generated ApiDefinition.cs requires cleanup:
using System; using Foundation; using ObjCRuntime; using UIKit;
namespace MyBinding { // @interface DotnetMyBinding : NSObject [BaseType(typeof(NSObject))] interface DotnetMyBinding { // +(void)initializeWithApiKey:(NSString * _Nonnull)apiKey; [Static] [Export("initializeWithApiKey:")] void Initialize(string apiKey);
// +(BOOL)isInitialized;
[Static]
[Export("isInitialized")]
bool IsInitialized { get; }
// +(NSString * _Nonnull)getVersion;
[Static]
[Export("getVersion")]
string GetVersion();
// +(NSString * _Nullable)processDataWithInput:(NSString * _Nonnull)input;
[Static]
[Export("processDataWithInput:")]
[return: NullAllowed]
string ProcessData(string input);
// +(void)fetchDataWithQuery:(NSString * _Nonnull)query
// completion:(void (^ _Nonnull)(NSString * _Nullable, NSError * _Nullable))completion;
[Static]
[Export("fetchDataWithQuery:completion:")]
[Async] // Generates FetchDataAsync method
void FetchData(string query, Action<string?, NSError?> completion);
// +(void)performOperationWithConfig:(NSDictionary * _Nonnull)config
// completion:(void (^ _Nonnull)(NSData * _Nullable, NSError * _Nullable))completion;
[Static]
[Export("performOperationWithConfig:completion:")]
[Async]
void PerformOperation(NSDictionary config, Action<NSData?, NSError?> completion);
// +(UIView * _Nonnull)createViewWithFrame:(CGRect)frame;
[Static]
[Export("createViewWithFrame:")]
UIView CreateView(CGRect frame);
// +(UIView * _Nonnull)createViewWithFrame:(CGRect)frame options:(NSDictionary * _Nonnull)options;
[Static]
[Export("createViewWithFrame:options:")]
UIView CreateView(CGRect frame, NSDictionary options);
// +(void)registerCallbackWithHandler:(void (^ _Nonnull)(NSString * _Nonnull))handler;
[Static]
[Export("registerCallbackWithHandler:")]
void RegisterCallback(Action<string> handler);
// +(void)unregisterCallback;
[Static]
[Export("unregisterCallback")]
void UnregisterCallback();
}
}
Common Cleanup Tasks
Issue Solution
Missing namespace Add namespace MyBinding { ... }
[Verify] attributes Review each, remove after confirming correctness
InitWithCoder constructors Remove - conflicts with linker
Protocol type mismatches Use interface types (e.g., ICAAnimation )
Missing [NullAllowed]
Add for nullable parameters/returns
Completion handlers Add [Async] attribute for async generation
Step 9: Build the Final Binding
cd macios/MyBinding.MaciOS.Binding dotnet build -c Release
Verify the output:
ls -la bin/Release/net9.0-ios/
Should contain: MyBinding.MaciOS.Binding.dll and resources
Step 10: Use in Your MAUI App
Add Project Reference
In your MAUI app's .csproj :
<ItemGroup Condition="$(TargetFramework.Contains('ios')) Or $(TargetFramework.Contains('maccatalyst'))"> <ProjectReference Include="....\macios\MyBinding.MaciOS.Binding\MyBinding.MaciOS.Binding.csproj" /> </ItemGroup>
Initialize in MauiProgram.cs
using Microsoft.Maui.Hosting;
#if IOS || MACCATALYST using MyBinding; #endif
public static class MauiProgram { public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder.UseMauiApp<App>();
#if IOS || MACCATALYST // Initialize the native library DotnetMyBinding.Initialize("your-api-key"); #endif
return builder.Build();
}
}
Use Async APIs
#if IOS || MACCATALYST using MyBinding; #endif
public partial class MainPage : ContentPage { private async void OnFetchClicked(object sender, EventArgs e) { #if IOS || MACCATALYST try { // Using the async version generated by [Async] attribute var result = await DotnetMyBinding.FetchDataAsync("my query"); await DisplayAlert("Success", result ?? "No data", "OK"); } catch (NSErrorException ex) { await DisplayAlert("Error", ex.Error.LocalizedDescription, "OK"); } #endif }
private void OnCreateViewClicked(object sender, EventArgs e)
{
#if IOS || MACCATALYST var nativeView = DotnetMyBinding.CreateView(new CoreGraphics.CGRect(0, 0, 300, 200));
// Add to a MAUI view using a custom handler or platform view
// This requires additional platform-specific integration
#endif } }
Register Callbacks
#if IOS || MACCATALYST protected override void OnAppearing() { base.OnAppearing(); DotnetMyBinding.RegisterCallback((message) => { MainThread.BeginInvokeOnMainThread(() => { StatusLabel.Text = $"Event: {message}"; }); }); }
protected override void OnDisappearing() { base.OnDisappearing(); DotnetMyBinding.UnregisterCallback(); } #endif
Updating Bindings When Native SDK Changes
Step-by-step Update Process
- Update Native Dependency Version
CocoaPods:
Podfile
pod 'FirebaseMessaging', '~> 11.0' # Updated version
cd macios/native/MyBinding pod update
Swift Package Manager: Update version in Xcode's Package Dependencies or Package.swift
Manual XCFramework: Replace the xcframework file with the new version
- Update Swift Wrapper (If Needed)
Review release notes for the native library and update DotnetMyBinding.swift :
-
Add new methods for new APIs
-
Update method signatures for changed APIs
-
Remove deprecated API wrappers
-
Handle any breaking changes
- Regenerate API Definition
Clean and rebuild
cd macios/MyBinding.MaciOS.Binding dotnet clean dotnet build
Regenerate Objective Sharpie output
sharpie bind
--output=sharpie-output-new
--namespace=MyBinding
--sdk=iphoneos18.0
--scope=Headers
"bin/Debug/net9.0-ios/MyBinding.MaciOS.Binding.resources/MyBindingiOS.xcframework/ios-arm64/MyBinding.framework/Headers/MyBinding-Swift.h"
- Diff and Merge Changes
Compare the new Sharpie output with existing ApiDefinition.cs :
diff ApiDefinition.cs sharpie-output-new/ApiDefinitions.cs
Manually merge:
-
Add new method bindings
-
Update changed signatures
-
Remove deleted methods
-
Preserve custom attributes ([Async] , [NullAllowed] , etc.)
- Test the Updated Bindings
dotnet build -c Release dotnet test # If you have unit tests
Run the sample app to verify functionality.
Troubleshooting
Build Errors
"Framework not found" / "Library not found"
Causes & Solutions:
-
XCFramework path incorrect - Verify the path in <XcodeProject> or <NativeReference>
-
Missing architectures - Ensure xcframework includes arm64 (device) and arm64/x86_64 (simulator)
-
CocoaPods not installed - Run pod install in the native directory
Verify xcframework architectures
lipo -info path/to/Framework.framework/Framework
"Undefined symbols for architecture"
Causes & Solutions:
-
Missing linked frameworks - Add system frameworks to Xcode project's "Link Binary with Libraries"
-
Static vs Dynamic mismatch - Ensure consistent linkage (all static or all dynamic)
-
Symbol visibility - Verify Swift classes/methods are public and have @objc
<!-- Force load symbols if needed --> <XcodeProject Include="..."> <SchemeName>MyBinding</SchemeName> <ForceLoad>true</ForceLoad> <SmartLink>false</SmartLink> </XcodeProject>
"No type or protocol named..."
Causes & Solutions:
-
Missing import - Add required imports to ApiDefinition.cs (using UIKit; , etc.)
-
Protocol vs Interface - Use interface types (ICAAnimation not CAAnimation )
-
Namespace mismatch - Verify namespace matches between wrapper and binding
"Duplicate symbol" / "Symbol already defined"
Causes & Solutions:
-
Multiple references to same framework - Check for duplicate <NativeReference> entries
-
Conflicting dependency versions - Resolve CocoaPods/SPM version conflicts
-
InitWithCoder constructor - Remove from ApiDefinition.cs (auto-generated by linker)
Objective Sharpie Errors
"Unable to find SDK":
List available SDKs
sharpie xcode -sdks
Update Xcode command line tools
xcode-select --install sudo xcode-select -s /Applications/Xcode.app
"Parse error in header":
-
Header may use features Sharpie doesn't support
-
Simplify the Swift wrapper to use basic types
-
Use --scope=Headers to limit parsing
Runtime Errors
"Native class hasn't been loaded"
Causes & Solutions:
-
Framework not embedded - Check that native resources are included in app bundle
-
Static library not linked - Verify <ForceLoad>true</ForceLoad> is set
-
Missing Objective-C class registration - Ensure @objc(ClassName) annotation is present
"unrecognized selector sent to instance"
Causes & Solutions:
-
Selector mismatch - Verify [Export("selector:")] matches Swift @objc(selector:) exactly
-
Method signature mismatch - Check parameter count and types match
-
Static vs instance method - Ensure [Static] attribute is correct
"Library not loaded: @rpath/..."
Causes & Solutions:
-
Swift runtime missing - Add linker flags for Swift libraries
-
Framework not embedded - Set "Embed & Sign" in Xcode for dynamic frameworks
-
rpath not set - Add -Wl,-rpath -Wl,@executable_path/Frameworks
Callbacks Not Working
Causes & Solutions:
-
Callback on wrong thread - Use DispatchQueue.main.async in Swift for UI updates
-
Callback garbage collected - Store strong reference to callback handler
-
Missing @escaping
-
Completion handlers must be @escaping in Swift
IntelliSense Issues
IntelliSense shows errors but project compiles: This is expected behavior. Binding projects don't use source generators. The solution:
-
Build the binding project first
-
Reload the solution/project
-
IntelliSense may still show errors - trust the compiler
Quick Reference
ApiDefinition Attributes
Attribute Purpose Example
[BaseType(typeof(NSObject))]
Specifies base class [BaseType(typeof(UIView))]
[Static]
Static method/property [Static] [Export("shared")]
[Export("selector:")]
Objective-C selector [Export("doSomethingWithValue:")]
[Async]
Generate async wrapper On completion handler methods
[NullAllowed]
Nullable parameter/return [return: NullAllowed]
[Protocol]
Objective-C protocol [Protocol] interface IMyDelegate
[Model]
Protocol implementation Combined with [Protocol]
[Abstract]
Required protocol method In protocol interface
[Internal]
Don't expose publicly Hide helper methods
[Wrap("...")]
Wrap with helper Strongly-typed helpers
[Sealed]
Prevent subclassing On final classes
XcodeProject MSBuild Properties
<XcodeProject Include="path/to/Project.xcodeproj"> <SchemeName>MyScheme</SchemeName> <!-- Required: Xcode scheme --> <Configuration>Release</Configuration> <!-- Build configuration --> <Kind>Framework</Kind> <!-- Framework or Static --> <SmartLink>true</SmartLink> <!-- Enable smart linking --> <ForceLoad>false</ForceLoad> <!-- Force load all symbols --> </XcodeProject>
NativeReference MSBuild Properties (Traditional Bindings)
<NativeReference Include="Library.xcframework"> <Kind>Framework</Kind> <!-- Framework or Static --> <Frameworks>Foundation UIKit</Frameworks> <!-- Required Apple frameworks --> <LinkerFlags>-lsqlite3</LinkerFlags> <!-- Additional linker flags --> <SmartLink>true</SmartLink> <!-- Enable smart linking --> <ForceLoad>false</ForceLoad> <!-- Force load all symbols --> <IsCxx>false</IsCxx> <!-- C++ library --> </NativeReference>
Common Swift-to-C# Type Mappings
// Swift // C# ApiDefinition String string String? [NullAllowed] string Bool bool Int / Int32 nint / int Int64 long Double double Float float Data NSData [String: Any] NSDictionary [Any] NSArray URL NSUrl Date NSDate UIView UIView UIImage UIImage CGRect CGRect CGPoint CGPoint CGSize CGSize (Result, Error?) -> Void Action<Result?, NSError?>
Resources
Official Documentation
-
Native Library Interop - .NET Community Toolkit
-
iOS Binding Project Migration
-
Binding Objective-C Libraries
-
Objective Sharpie
Tools
- XcodeGen - Generate Xcode projects from YAML specification
Templates and Examples
-
Maui.NativeLibraryInterop Repository
-
CommunityToolkit.Maui Bindings
Related Skills
-
See docs/ios-bindings-guide.md for comprehensive reference
-
See docs/android-bindings-guide.md for Android bindings
Appendix A: Using the Community Toolkit Template (Alternative)
If you prefer to start from an existing template rather than creating from scratch:
git clone https://github.com/CommunityToolkit/Maui.NativeLibraryInterop cp -r Maui.NativeLibraryInterop/template ./MyBinding cd MyBinding
Rename files and update references
find . -name "NewBinding" -exec bash -c 'mv "$0" "${0//NewBinding/MyBinding}"' {} ; find . -type f ( -name ".cs" -o -name ".csproj" -o -name ".swift" -o -name ".yml" ) | xargs sed -i '' 's/NewBinding/MyBinding/g'
The template includes pre-configured:
-
Xcode project with correct build settings
-
Binding .csproj with XcodeProject reference
-
Sample MAUI app
-
GitHub Actions CI/CD workflows
Output Format
When assisting with iOS slim bindings, provide:
-
Project structure - File/folder layout for the binding
-
Swift wrapper code - Complete DotnetMyBinding.swift implementation
-
Xcode configuration - Build settings and dependency setup
-
C# binding project - .csproj and ApiDefinition.cs files
-
Usage examples - How to call the binding from MAUI/C#
-
Troubleshooting guidance - Common issues and solutions for the specific library
Always verify:
-
Swift classes have @objc(ClassName) annotations
-
Methods have @objc(selector:) annotations matching Objective-C conventions
-
Types are marshallable between Swift and C#
-
Async operations use completion handlers with [Async] attribute
-
Error handling uses NSError for proper propagation