From maui-skillz
Creates and updates slim/native iOS bindings for .NET MAUI and .NET iOS projects using Native Library Interop. Guides Swift/Objective-C wrappers, Xcode configuration, C# API generation, and xcframework integration.
npx claudepluginhub redth/maui-skillz --plugin maui-skillzThis skill uses the workspace's default tool permissions.
Activate this skill when the user asks:
Creates and updates slim/native Android bindings for .NET MAUI and .NET for Android using Native Library Interop (NLI). Guides Java/Kotlin wrappers, Gradle config, Maven deps, C# binding generation, AAR/JAR integration.
Guides migration of Xamarin.iOS, Xamarin.Mac, and Xamarin.tvOS native apps to .NET for iOS, macOS, and tvOS. Covers SDK-style projects, MSBuild properties, Info.plist updates, binding libraries, and code signing.
Guides .NET MAUI XAML/C# data bindings: compiled x:DataType, INotifyPropertyChanged/ObservableObject, converters, modes, relative bindings, fallbacks, and MVVM practices.
Share bugs, ideas, or general feedback.
Activate this skill when the user asks:
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.
| 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 |
| 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) |
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
This section shows how to create the entire binding project structure using only command-line tools—no GUI or template cloning required.
Install XcodeGen (generates Xcode projects from YAML):
brew install xcodegen
# 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}
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 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
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
cd macios/native/${BINDING_NAME}
xcodegen generate
cd ../../..
This creates MyBinding.xcodeproj with all the correct build settings.
# 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/
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
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
cd macios/${BINDING_NAME}.MaciOS.Binding
dotnet build
This will:
# 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
If your native library uses CocoaPods dependencies:
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
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
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
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.
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.xcodeprojand Swift Package directories.
Choose the appropriate method for your library's distribution:
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
In Xcode:
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")
]
)
]
)
.xcframework into the Xcode projectCreate 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)
}
}
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] |
// 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 = ""
}
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");
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)
}
}
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>
| 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 |
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/
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
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"
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();
}
}
| 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 |
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
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>
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();
}
}
#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
}
}
#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
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
Review release notes for the native library and update DotnetMyBinding.swift:
# 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"
Compare the new Sharpie output with existing ApiDefinition.cs:
diff ApiDefinition.cs sharpie-output-new/ApiDefinitions.cs
Manually merge:
[Async], [NullAllowed], etc.)dotnet build -c Release
dotnet test # If you have unit tests
Run the sample app to verify functionality.
Causes & Solutions:
<XcodeProject> or <NativeReference>pod install in the native directory# Verify xcframework architectures
lipo -info path/to/Framework.framework/Framework
Causes & Solutions:
public and have @objc<!-- Force load symbols if needed -->
<XcodeProject Include="...">
<SchemeName>MyBinding</SchemeName>
<ForceLoad>true</ForceLoad>
<SmartLink>false</SmartLink>
</XcodeProject>
Causes & Solutions:
ApiDefinition.cs (using UIKit;, etc.)ICAAnimation not CAAnimation)Causes & Solutions:
<NativeReference> entries"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":
--scope=Headers to limit parsingCauses & Solutions:
<ForceLoad>true</ForceLoad> is set@objc(ClassName) annotation is presentCauses & Solutions:
[Export("selector:")] matches Swift @objc(selector:) exactly[Static] attribute is correctCauses & Solutions:
-Wl,-rpath -Wl,@executable_path/FrameworksCauses & Solutions:
DispatchQueue.main.async in Swift for UI updates@escaping - Completion handlers must be @escaping in SwiftIntelliSense shows errors but project compiles: This is expected behavior. Binding projects don't use source generators. The solution:
| 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 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 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>
// 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?>
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:
When assisting with iOS slim bindings, provide:
DotnetMyBinding.swift implementation.csproj and ApiDefinition.cs filesAlways verify:
@objc(ClassName) annotations@objc(selector:) annotations matching Objective-C conventions[Async] attributeNSError for proper propagation