How to Build a NuGet Package with iOS Frameworks for .NET MAUI Development

Building a NuGet package with iOS frameworks for .NET MAUI development is significantly more complicated than for Android. In this article, we will explore the process of adding iOS frameworks to an existing NuGet package that already supports Windows, Linux and Android. Our goal is to create a comprehensive NuGet package that can be utilized for building machine-readable zone (MRZ) recognition applications with .NET console, .NET WinForms, and .NET MAUI.

NuGet Package

https://www.nuget.org/packages/MrzScannerSDK

Tools

  • Xcode

    Build an iOS framework for MRZ recognition.

  • Objective Sharpie

    Generate C# bindings for Objective-C libraries.

  • Visual Studio for Windows or Visual Studio for Mac

    Create an iOS binding library project that references the iOS framework and the C# bindings.

Step 1: Build an iOS Framework for MRZ Recognition Using Xcode

Dynamsoft offers a demo MRZ scanner for iOS, which you can download from GitHub.

Once you have downloaded the source code, navigate to the MRZScannerObjC directory and execute pod install to install all necessary dependencies.

MRZ iOS SDK dependencies

Next, open MRZScannerObjC.xcworkspace in Xcode. This allows you to build and test the MRZ scanner application on an iOS device.

Xcode mrz scanner

The MRZRecognizer subproject is crucial as it loads MRZ model files and offers APIs for recognizing MRZ data from images. To ensure it provides the same APIs as its Android counterpart, you’ll need to add specific Objective-C code in the DynamsoftMRZRecognizer.m file:

- (NSArray<iDLRResult *> *)recognizeMrzFile:(NSString *)fileName error:(NSError * _Nullable __autoreleasing *)error {
    return [self recognizeFile:fileName error:error];
}

- (NSArray<iDLRResult *> *)recognizeMrzBuffer:(iImageData *)imageData error:(NSError * _Nullable __autoreleasing *)error {
    return [self recognizeBuffer:imageData error:error];
}

- (NSArray<iDLRResult *> *)recognizeMrzImage:(UIImage *)image error:(NSError * _Nullable __autoreleasing *)error {
    return [self recognizeImage:image error:error];
}

Finally, switch the Build Configuration to Release and build the MRZRecognizer.framework project.

ios framework build release

Step 2: Generate C# Bindings for Objective-C Libraries with Objective Sharpie

Objective Sharpie serves as a command-line tool designed to generate C# bindings for Objective-C libraries, simplifying the initial phase of the binding process. It works by analyzing the header files of a native library and translating its public API into a set of binding definitions. This process results in the creation of two essential files: ApiDefinition.cs and StructsAndEnums.cs, both of which are integral to the development of the iOS binding library project.

In this step, we aim to generate C# bindings for three specific Objective-C libraries: DynamsoftCore.framework, DynamsoftLabelRecognizer.framework, and MRZRecognizer.framework with the sharpie bind command:

sharpie bind -f xxx.framework -sdk iphoneos16.0

Step 3: Create an iOS Binding Library Project in Visual Studio

In Visual Studio, scaffold a new project with the iOS Binding Library template.

iOS binding library template

Then incorporate the three frameworks (DynamsoftCore.framework, DynamsoftLabelRecognizer.framework, MRZRecognizer.framework) along with the C# bindings (ApiDefinition.cs and StructsAndEnums.cs) generated by Objective Sharpie. The complete csproj file is written as follows:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net7.0-ios</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <AssemblyName>MrzScannerSDK</AssemblyName>
    <IsBindingProject>true</IsBindingProject>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
  </PropertyGroup>

  <ItemGroup>
    <ObjcBindingApiDefinition Include="DynamsoftCore/ApiDefinitions.cs" />
    <ObjcBindingCoreSource Include="DynamsoftCore/StructsAndEnums.cs" />
    <NativeReference Include="DynamsoftCore.framework">
      <Kind>Framework</Kind>
      <Frameworks></Frameworks>
    </NativeReference>
  </ItemGroup>

  <ItemGroup>
    <ObjcBindingApiDefinition Include="DynamsoftLabelRecognizer/ApiDefinitions.cs" />
    <ObjcBindingCoreSource Include="DynamsoftLabelRecognizer/StructsAndEnums.cs" />
    <NativeReference Include="DynamsoftLabelRecognizer.framework">
      <Kind>Framework</Kind>
      <Frameworks></Frameworks>
    </NativeReference>
  </ItemGroup>

  <ItemGroup>
    <ObjcBindingApiDefinition Include="MRZRecognizer/ApiDefinitions.cs" />
    <ObjcBindingCoreSource Include="MRZRecognizer/StructsAndEnums.cs" />
    <NativeReference Include="MRZRecognizer.framework">
      <Kind>Framework</Kind>
      <Frameworks></Frameworks>
    </NativeReference>
  </ItemGroup>

</Project>

Additionally, copy MrzParser.cs, MrzResult.cs, and MrzScanner.cs files from the existing Android binding library project to this new iOS binding library project. Modifications to MrzScanner.cs are required to ensure compatibility with iOS APIs.

using DynamsoftCore;
using Com.Dynamsoft.Dlr;
using MRZRecognizer;
using System.Runtime.InteropServices;
using Foundation;

namespace Dynamsoft
{
    public class MrzScanner
    {
        private DynamsoftMRZRecognizer? recognizer;
        ...

        public class LicenseVerification : LicenseVerificationListener
        {
            public override void LicenseVerificationCallback(bool isSuccess, NSError error)
            {
                if (!isSuccess)
                {
                    System.Console.WriteLine(error.UserInfo);
                }
            }
        }

        public static void InitLicense(string license, object? context = null)
        {
            DynamsoftLicenseManager.InitLicense(license, new LicenseVerification());
        }

        private MrzScanner()
        {
            recognizer = new DynamsoftMRZRecognizer();
        }

        ...

        public static string? GetVersionInfo()
        {
            return DynamsoftLabelRecognizer.Version;
        }

        public Result[]? DetectFile(string filename)
        {
            if (recognizer == null) return null;

            NSError error;
            iDLRResult[]? mrzResult = recognizer.RecognizeMrzFile(filename, out error);
            return GetResults(mrzResult);
        }

        public Result[]? DetectBuffer(byte[] buffer, int width, int height, int stride, ImagePixelFormat format)
        {
            if (recognizer == null) return null;

            IntPtr data = Marshal.AllocHGlobal(buffer.Length);
            Marshal.Copy(buffer, 0, data, buffer.Length);
            NSData converted = NSData.FromBytes(data, (nuint)buffer.Length);

            iImageData imageData = new iImageData()
            {
                Bytes = converted,
                Width = width,
                Height = height,
                Stride = stride,
                Format = (EnumImagePixelFormat)format,
            };
            NSError error;
            iDLRResult[]? mrzResult = recognizer.RecognizeMrzBuffer(imageData, out error);
            Marshal.Release(data);

            return GetResults(mrzResult);
        }

        private Result[]? GetResults(iDLRResult[]? mrzResult)
        {
            if (mrzResult != null && mrzResult[0].LineResults != null)
            {
                iDLRLineResult[] lines = mrzResult[0].LineResults.ToArray();
                Result[] result = new Result[lines.Length];
                for (int i = 0; i < lines.Length; i++)
                {
                    result[i] = new Result()
                    {
                        Confidence = (int)lines[i].Confidence,
                        Text = lines[i].Text ?? "",
                        Points = new int[8]
                        {
                             (int)((NSValue)lines[i].Location.Points[0]).CGPointValue.X,
                             (int)((NSValue)lines[i].Location.Points[0]).CGPointValue.Y,
                             (int)((NSValue)lines[i].Location.Points[1]).CGPointValue.X,
                             (int)((NSValue)lines[i].Location.Points[1]).CGPointValue.Y,
                             (int)((NSValue)lines[i].Location.Points[2]).CGPointValue.X,
                             (int)((NSValue)lines[i].Location.Points[2]).CGPointValue.Y,
                             (int)((NSValue)lines[i].Location.Points[3]).CGPointValue.X,
                             (int)((NSValue)lines[i].Location.Points[3]).CGPointValue.Y
                        }
                    };
                }

                return result;
            }

            return null;
        }
    }
}

The final structure of the iOS binding library project is like this:

iOS binding library structure

With these steps completed, your iOS binding library project is nearly finalized. The subsequent phase involves addressing any build errors and warnings associated with the ApiDefinition.cs and StructsAndEnums.cs files. Manual corrections are essential here, as Objective Sharpie’s automated process might not perfectly map Objective-C libraries to C# bindings. Once these adjustments are made, compiling the project should produce the MrzScannerSDK.dll file.

Step 4: Test the iOS Binding Library in a .NET MAUI Application

Open the .NET MAUI example project located within the example directory. The project was initially set up for Android use. Now we will integrate the iOS binding library into the project and implement iOS-specific modifications:

  1. Modify the Platforms/iOS/Info.plist file by inserting specific keys to secure the necessary permissions.

     <key>NSCameraUsageDescription</key>
     <string>This app is using the camera</string>
     <key>NSPhotoLibraryAddUsageDescription</key>
     <string>This app is saving photo to the library</string>
     <key>NSMicrophoneUsageDescription</key>
     <string>This app needs access to microphone for taking videos.</string>
     <key>NSPhotoLibraryAddUsageDescription</key>
     <string>This app needs access to the photo gallery for picking photos and videos.</string>
     <key>NSPhotoLibraryUsageDescription</key>
     <string>This app needs access to photos gallery for picking photos and videos.</string>
    
  2. In the Platforms/iOS/Program.cs file, call MrzScanner.InitLicense to activate the Dynamsoft Label Recognizer. You can acquire the license key through the Dynamsoft customer portal.

     using ObjCRuntime;
     using UIKit;
     using Dynamsoft;
        
     namespace MauiAndroidMrz
     {
         public class Program
         {
             static void Main(string[] args)
             {
                 MrzScanner.InitLicense("LICENSE-KEY");
                 UIApplication.Main(args, null, typeof(AppDelegate));
             }
         }
     }
    
  3. Conduct a global search for #if ANDROID within the project and replace it with #if ANDROID || IOS to accommodate both platforms.
  4. Finally, compile and execute the .NET MAUI application on an iPhone or iPad to validate the integration and functionality of the iOS binding library.

    .NET MAUI iOS MRZ recognition

Step 5. Create a NuGet Package with iOS Frameworks and C# Bindings

To incorporate the iOS binding library into the existing NuGet package, you’ll need to modify the MrzScannerSDK.nuspec file by adding specific entries for iOS:

<!-- iOS -->
<file src="ios\sdk\bin\Release\net7.0-ios\*.dll" target="lib\net7.0-ios16.1" />
<file src="ios\sdk\bin\Release\net7.0-ios\MrzScannerSDK.resources\**\*.*"
    target="lib\net7.0-ios16.1\MrzScannerSDK.resources" />

After updating the .nuspec file, utilize the nuget pack command to compile a comprehensive NuGet package that supports .NET MAUI development across multiple platforms.

nuget package for windows, linux, android, and iOS

Source Code

https://github.com/yushulx/dotnet-mrz-sdk