AgentSkillsCN

macios-binding-sample

创建可运行 .NET 绑定项目 API 的独立 Mac Catalyst 或 iOS 应用示例。适用于验证 C# 绑定针对原生 xcframework 在运行时是否正常工作时使用。涵盖项目搭建、引用绑定项目与原生框架、编写 API 测试代码、处理常见运行时错误,以及调试绑定问题。

SKILL.md
--- frontmatter
name: macios-binding-sample
description: Create sample Mac Catalyst or iOS apps that exercise .NET binding project APIs. Use when you need to verify that C# bindings for native xcframeworks work correctly at runtime. Covers project scaffolding, referencing binding projects and native frameworks, writing API exercise code, handling common runtime errors, and debugging binding issues.

.NET Binding Sample App

Create sample Mac Catalyst or iOS apps to exercise and verify .NET binding project APIs at runtime.

When to Use This Skill

  • After creating binding projects for xcframeworks, to verify they work
  • To create demo/sample apps showcasing bound APIs
  • To debug runtime failures in bindings (selector not found, init crashes, etc.)

Quick Start

1. Create the App Project

bash
# Mac Catalyst app (runs on macOS, uses iOS APIs)
dotnet new maccatalyst -n TestApp -o TestApp

# Or iOS app
dotnet new ios -n TestApp -o TestApp

2. Reference Binding Projects

Edit TestApp.csproj to add project references to your binding projects:

xml
<ItemGroup>
  <ProjectReference Include="../MyFramework.Binding/MyFramework.Binding.csproj" />
</ItemGroup>

3. Add Native Framework References

Critical: Native xcframework references from binding projects do NOT automatically flow to consuming app projects through ProjectReference. You must add them directly:

xml
<ItemGroup>
  <NativeReference Include="../../Frameworks/MyFramework.xcframework">
    <Kind>Framework</Kind>
    <SmartLink>True</SmartLink>
  </NativeReference>
</ItemGroup>

Add one <NativeReference> for each xcframework your bindings depend on, including transitive dependencies.

4. Write API Exercise Code

Put test code in SceneDelegate.cs (Mac Catalyst) or AppDelegate.cs (iOS). Use the WillConnect or FinishedLaunching method:

csharp
using MyFramework;

[Export ("scene:willConnectToSession:options:")]
public void WillConnect (UIScene scene, UISceneSession session,
    UISceneConnectionOptions connectionOptions)
{
    // Test API
    var result = MyClass.SharedInstance;
    Console.WriteLine ($"✅ MyFramework: {result}");
}

5. Build and Run

bash
# Build
dotnet build TestApp/TestApp.csproj

# Run (Mac Catalyst)
open TestApp/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/TestApp.app

# Or run directly to see Console.WriteLine output
./TestApp/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/TestApp.app/Contents/MacOS/TestApp

Note: dotnet run does NOT launch GUI apps. Use open or run the binary directly.

Writing Test Code

Testing Patterns

Structure tests to exercise different API surface areas:

csharp
void RunAllTests ()
{
    TestEnums ();
    TestConstants ();
    TestStaticProperties ();
    TestObjectCreation ();
    TestMethods ();
    TestDelegates ();
    TestNotifications ();
}

void TestEnums ()
{
    // Verify enum values resolve correctly
    var value = MyEnum.OptionA;
    Console.WriteLine ($"✅ Enum: {value} = {(int)value}");
}

void TestConstants ()
{
    // Verify [Field] constants load without linker errors
    var domain = MyConstants.ErrorDomain;
    Console.WriteLine ($"✅ Constant: {domain}");
}

void TestStaticProperties ()
{
    // Verify static properties return non-null
    var version = Settings.SdkVersion;
    Console.WriteLine ($"✅ Version: {version}");
}

void TestObjectCreation ()
{
    // Test constructors — use parameterized constructors for Swift classes
    var obj = new MyClass ("test");
    Console.WriteLine ($"✅ Created: {obj.Name}");
}

void TestMethods ()
{
    // Test instance methods
    var obj = new MyClass ("test");
    obj.Configure (new NSDictionary ());
    Console.WriteLine ("✅ Method call succeeded");
}

What to Test for Each Framework

For each binding project, test at minimum:

  1. One enum value — verifies StructsAndEnums.cs compiles and links
  2. One [Field] constant — verifies library name is correct (not "__Internal")
  3. One static property or method — verifies class resolution at runtime
  4. One object instantiation — verifies constructor binding works
  5. One instance method/property — verifies selector names are correct

Result Reporting Pattern

csharp
void Test (string framework, string test, Action action)
{
    try {
        action ();
        Console.WriteLine ($"✅ {framework}: {test}");
    } catch (Exception ex) {
        Console.WriteLine ($"❌ {framework}: {test} — {ex.Message}");
    }
}

Common Runtime Errors and Fixes

Selector not found / unrecognized selector sent to instance

The ObjC selector string in [Export ("...")] doesn't match the actual method. Check:

  • Missing colons (:) for parameter separators
  • Capitalization mismatches
  • Method was renamed in Swift-generated header vs actual implementation

unrecognized selector sent to class (class methods)

Swift factory class methods (e.g., +[MyClass classWithName:]) may not exist in the ObjC runtime even though they appear in the -Swift.h header. Use initWith* constructors instead.

Fatal error: Use of unimplemented initializer 'init()'

The Swift class doesn't support default initialization. The binding must have [DisableDefaultCtor]. Fix the binding and rebuild, then use a parameterized constructor in your test:

csharp
// ❌ Crashes
var obj = new SwiftClass ();

// ✅ Use parameterized constructor
var obj = new SwiftClass ("value");

Undefined symbol _OBJC_CLASS_$_ClassName (linker error)

The native xcframework isn't linked. Add <NativeReference> directly to the app project .csproj.

Undefined symbol _ConstantName (linker error)

The [Field] attribute uses wrong library name. Change from "__Internal" to the framework name:

csharp
[Field ("kMyConstant", "MyFramework")]  // ✅ framework name

Could not create an native instance

The binding constructor isn't matching a real ObjC/Swift initializer. Verify the selector in [Export] matches the header exactly.

Project Structure

For a multi-framework binding validation:

code
MySdk/
├── Framework1.Binding/
│   ├── Framework1.Binding.csproj
│   ├── ApiDefinition.cs
│   └── StructsAndEnums.cs
├── Framework2.Binding/
│   ├── Framework2.Binding.csproj
│   ├── ApiDefinition.cs
│   └── StructsAndEnums.cs
├── TestApp/
│   ├── TestApp.csproj          # References all binding projects + xcframeworks
│   ├── AppDelegate.cs
│   ├── SceneDelegate.cs        # Main test code here
│   └── Info.plist
├── Frameworks/
│   ├── Framework1.xcframework
│   └── Framework2.xcframework
└── MySdk.slnx

Multi-TFM Considerations

Binding projects should target both iOS and Mac Catalyst:

xml
<TargetFrameworks>net10.0-ios;net10.0-maccatalyst</TargetFrameworks>

The test app targets one platform:

xml
<!-- Mac Catalyst -->
<TargetFramework>net10.0-maccatalyst</TargetFramework>

<!-- Or iOS -->
<TargetFramework>net10.0-ios</TargetFramework>

Choose the target framework matching your installed .NET workloads and Xcode version:

  • Xcode 26+: requires net10.0-* workloads
  • Xcode 15/16: use net9.0-* workloads (legacy)

Mac Catalyst App Template (SceneDelegate pattern)

Complete SceneDelegate.cs template for a Mac Catalyst test app:

csharp
using Foundation;
using UIKit;

namespace TestApp;

[Register ("SceneDelegate")]
public class SceneDelegate : UIResponder, IUIWindowSceneDelegate
{
    [Export ("window")]
    public UIWindow? Window { get; set; }

    [Export ("scene:willConnectToSession:options:")]
    public void WillConnect (UIScene scene, UISceneSession session,
        UISceneConnectionOptions connectionOptions)
    {
        var windowScene = scene as UIWindowScene;
        if (windowScene == null) return;

        Window = new UIWindow (windowScene);
        var vc = new UIViewController ();
        vc.View!.BackgroundColor = UIColor.SystemBackground;

        var results = new List<string> ();

        // Run tests and collect results
        try {
            // ADD YOUR TESTS HERE
            results.Add ("✅ Test passed");
        } catch (Exception ex) {
            results.Add ($"❌ Test failed: {ex.Message}");
        }

        // Display results
        var label = new UILabel (vc.View.Bounds) {
            Text = string.Join ("\n", results),
            Lines = 0,
            Font = UIFont.GetMonospacedSystemFont (14, UIFontWeight.Regular),
            TextAlignment = UITextAlignment.Left,
            AutoresizingMask = UIViewAutoresizing.All,
        };
        vc.View.AddSubview (label);

        // Also print to console
        foreach (var r in results)
            Console.WriteLine (r);

        Window.RootViewController = vc;
        Window.MakeKeyAndVisible ();
    }
}

References

  • references/test-patterns.md — Detailed patterns for exercising different binding types (enums, delegates, notifications, blocks, async)