| name | ios-slim-bindings |
| description | 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:
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
BINDING_NAME="MyBinding"
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
ls -la macios/native/${BINDING_NAME}/
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
find bin -name "*.xcframework" -type d
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
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 ../../..
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
BINDING_NAME="${1:-MyBinding}"
BUNDLE_ID_PREFIX="${2:-com.example}"
MIN_IOS_VERSION="${3:-15.0}"
echo "Creating iOS binding project: ${BINDING_NAME}"
if ! command -v xcodegen &> /dev/null; then
echo "Installing xcodegen..."
brew install xcodegen
fi
mkdir -p ${BINDING_NAME}/macios/native/${BINDING_NAME}/${BINDING_NAME}
mkdir -p ${BINDING_NAME}/macios/${BINDING_NAME}.MaciOS.Binding
cd ${BINDING_NAME}
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
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
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
cd macios/native/${BINDING_NAME}
xcodegen generate
cd ../../..
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
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}
swift package init --type library --name ${BINDING_NAME}
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
pod 'FirebaseMessaging', '~> 10.0'
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
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:
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
@objc(DotnetMyBinding)
public class DotnetMyBinding: NSObject {
@objc(initializeWithApiKey:)
public static func initialize(apiKey: String) {
TheNativeLibrary.configure(withApiKey: apiKey)
}
@objc(isInitialized)
public static func isInitialized() -> Bool {
return TheNativeLibrary.isConfigured
}
@objc(getVersion)
public static func getVersion() -> String {
return TheNativeLibrary.version
}
@objc(processDataWithInput:)
public static func processData(input: String) -> String? {
guard let result = TheNativeLibrary.process(input) else {
return nil
}
return result.stringValue
}
@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)
}
}
}
@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)
}
}
}
@objc(createViewWithFrame:)
public static func createView(frame: CGRect) -> UIView {
let nativeView = TheNativeLibrary.createCustomView()
nativeView.frame = frame
return nativeView
}
@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
}
private static var callbackHandler: ((String) -> Void)?
@objc(registerCallbackWithHandler:)
public static func registerCallback(handler: @escaping (String) -> Void) {
callbackHandler = handler
TheNativeLibrary.setEventHandler { event in
callbackHandler?(event.description)
}
}
@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
@objc(ClassName)
public class ClassName: NSObject {
@objc(methodNameWithParam:anotherParam:)
public func methodName(param: String, anotherParam: Int) -> Bool {
}
@objc(staticMethodWithValue:)
public static func staticMethod(value: String) -> String {
}
@objc(propertyName)
public var propertyName: String {
return "value"
}
@objc
public var readWriteProperty: String = ""
}
Completion Handler Pattern
For async operations, use completion handlers that .NET can convert to async/await:
@objc(operationWithInput:completion:)
public static func operation(
input: String,
completion: @escaping (String?, NSError?) -> Void
) {
DispatchQueue.main.async {
completion(result, nil)
completion(nil, error as NSError)
}
}
[Static]
[Export("operationWithInput:completion:")]
[Async]
void Operation(string input, Action<string?, NSError?> completion);
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>
<PackageId>MyBinding.MaciOS</PackageId>
<Version>1.0.0</Version>
<Authors>Your Name</Authors>
<Description>iOS bindings for MyLibrary</Description>
</PropertyGroup>
<ItemGroup>
<XcodeProject Include="../native/MyBinding/MyBinding.xcodeproj">
<SchemeName>MyBinding</SchemeName>
</XcodeProject>
</ItemGroup>
<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 bin -name "*-Swift.h" -type f
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:
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"
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
{
[BaseType(typeof(NSObject))]
interface DotnetMyBinding
{
[Static]
[Export("initializeWithApiKey:")]
void Initialize(string apiKey);
[Static]
[Export("isInitialized")]
bool IsInitialized { get; }
[Static]
[Export("getVersion")]
string GetVersion();
[Static]
[Export("processDataWithInput:")]
[return: NullAllowed]
string ProcessData(string input);
[Static]
[Export("fetchDataWithQuery:completion:")]
[Async]
void FetchData(string query, Action<string?, NSError?> completion);
[Static]
[Export("performOperationWithConfig:completion:")]
[Async]
void PerformOperation(NSDictionary config, Action<NSData?, NSError?> completion);
[Static]
[Export("createViewWithFrame:")]
UIView CreateView(CGRect frame);
[Static]
[Export("createViewWithFrame:options:")]
UIView CreateView(CGRect frame, NSDictionary options);
[Static]
[Export("registerCallbackWithHandler:")]
void RegisterCallback(Action<string> handler);
[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/
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
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
{
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));
#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
1. Update Native Dependency Version
CocoaPods:
pod 'FirebaseMessaging', '~> 11.0'
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
2. 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
3. Regenerate API Definition
cd macios/MyBinding.MaciOS.Binding
dotnet clean
dotnet build
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"
4. 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.)
5. Test the Updated Bindings
dotnet build -c Release
dotnet test
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
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
<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":
sharpie xcode -sdks
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>
<Configuration>Release</Configuration>
<Kind>Framework</Kind>
<SmartLink>true</SmartLink>
<ForceLoad>false</ForceLoad>
</XcodeProject>
NativeReference MSBuild Properties (Traditional Bindings)
<NativeReference Include="Library.xcframework">
<Kind>Framework</Kind>
<Frameworks>Foundation UIKit</Frameworks>
<LinkerFlags>-lsqlite3</LinkerFlags>
<SmartLink>true</SmartLink>
<ForceLoad>false</ForceLoad>
<IsCxx>false</IsCxx>
</NativeReference>
Common Swift-to-C# Type Mappings
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
Tools
- XcodeGen - Generate Xcode projects from YAML specification
Templates and Examples
Related Skills
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
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