Creating a Xamarin Bindings Library for a Custom iOS Control
Xamarin seems to magically allow C# developers the opportunity to create native mobile apps with familiar tools, and you may be wondering how does it really work? Xamarin produces native apps, but how exactly does a native control interact with your C# code? This article will shed some light on those questions by showing how to make a custom native control accessible through Xamarin.iOS and all of the intermediary steps involved.
Setup
There are a number of different tools which we will want to have at our disposal before we get started. We'll want to make sure we have the following items installed before we get started:* The newest version of Xcode and the latest iOS API
- Xcode Command Line Tools
- The newest version of Xarmarin Studio (or a PC with Visual Studio) and the Mono Framework
- Objective Sharpie Xcode can easily be obtained via the Mac Appstore and the newest iOS API's should be included with Xcode (and can always be obtained by updating Xcode to the newest version). The Command Line Tools for Xcode can be installed by invoking the following command in the Terminal application: xcode-select --install Alternatively, you can download the tools from the Apple Developer portal. The newest version of Xamarin can be obtained on their website. Once you have everything downloaded and installed you can get started.
Creating a Universal Static Library
Note: These steps mirror the initial steps of the process of creating a static framework on iOS. If you read our previous blog post covering creating iOS frameworks then this section should seem familiar. The piece that we'll need for this process is a static library that encapsulates our control. We'll create a Cocoa Touch Static Library project in Xcode. Static Library Project Template The project will generically be named Checkbox. After we've taken this step, we can import the code for our control. I'll directly copy the header and implementation files that were part of my custom control class into this project. Once I've copied those files over, I'll be able to generate a static library (denoted by the .a extension). One thing to keep in mind: initially, this file will be built for a single architecture only. The next step is to create a universal library (sometimes called a "fat library") built against multiple architectures. There are a few different processor architectures that we need to be concerned about, including x86_64 and i386 (for the iOS simulator), as well as armv7 and arm64 for the various iPhone and iPad devices that we're targeting. This way, when an app using our framework is deployed, it can be linked to the appropriate bits for the target device. We'll need to add a new target to our application, and select the Aggregate project template in the "Other" category.
Aggregate Project Template I'll name the Aggregate target UniversalLib so that it can be easily identified. Now, we'll need to navigate to the Build Phases for the UniversalLib target so that we can run our own script when we build for this target. Click the "+" at the top of the screen, and select the New Run Script Phase option. You can now expand the Run Script item, which provides the options of entering your own script or specifying the location of a script.
Build Script Entry We'll use the following script to build the universal library. You can either copy this directly into Xcode or create a separate .sh file with this script and simply specify the path.
# define output folder environment variable
UNIVERSAL\_OUTPUTFOLDER=${BUILD\_DIR}/${CONFIGURATION}-universal
# Build Device and Simulator versions
xcodebuild -target ${PROJECT\_NAME} ONLY\_ACTIVE\_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos BUILD\_DIR="${BUILD\_DIR}" BUILD\_ROOT="${BUILD_ROOT}"
xcodebuild -target ${PROJECT\_NAME} ONLY\_ACTIVE\_ARCH=NO -configuration ${CONFIGURATION} -sdk iphonesimulator -arch i386 -arch x86\_64 BUILD\_DIR="${BUILD\_DIR}" BUILD\_ROOT="${BUILD\_ROOT}"
# make sure the output directory exists
mkdir -p "${UNIVERSAL_OUTPUTFOLDER}"
# Create universal binary file using lipo
lipo -create -output "${UNIVERSAL\_OUTPUTFOLDER}/lib${PROJECT\_NAME}.a" "${BUILD\_DIR}/${CONFIGURATION}-iphoneos/lib${PROJECT\_NAME}.a" "${BUILD\_DIR}/${CONFIGURATION}-iphonesimulator/lib${PROJECT\_NAME}.a"
# copy the header files (just for convenience)
cp -R "${BUILD\_DIR}/${CONFIGURATION}-iphoneos/include" "${UNIVERSAL\_OUTPUTFOLDER}/"
open "${UNIVERSAL_OUTPUTFOLDER}"
You may have noticed I mentioned the lipo tool in the script comments. Lipo is used to merge the device and simulator binaries into one library. Once generated, this library can be found in the Derived Data location where Xcode puts its build output. The path for Derived Data folder is inside your user library, in Users//Library/Developer/Xcode/DerivedData. Derived Data Folder in Finder We can verify that the library supports the required architectures with lipo. From a Terminal window we can use the command:
lipo -info libCheckbox.a
The lipo command should report the following: Architectures in the fat file: libXuniListView.a are: armv7 i386 x86_64 arm64
Creating a Bindings Library Project
Now that we have our static control library created we can switch gears and move over to using Xamarin Studio (or Visual Studio) to create a bindings library which will eventually connect our native control to Xamarin. Xamarin has some well written documentation about how the binding process work, but, to explain the process in broad strokes, we will create a bindings library project, modify an "API definition" which specifices how the API for the native control will be exposed in .NET, compile this bindings project to produce a .dll, and finally take the .dll and use it in other Xamarin.iOS projects. Xamarin Studio has a project template which we can use to this end. To create the bindings library:
- Open Xamarin Studio.
- From the File menu, select New -> Solution.
- For the Project template, choose iOS Library category on the left pane and select Bindings Library on the right and click next.
- Name your project Checkbox.
Create a new Bindings Library Project This will create the bindings library project which has an ApiDefinitions.cs file as well as a StructsAndEnums value. These two files are the ones that will need to be edited to properly bind our iOS library. We will only be making edits to the ApiDefinition.cs for this sample which defines how our control's Objective-C API is wrapped in C#. The StructsAndEnums.cs file defines any structures or enumerations that are required by the interfaces and delegates. We'll need to include the static library file which we created earlier into our bindings project. This can be done accomplished by:
Right click on your project and select Add -> Add Files...
Navigate to and Select the Universal Library we created earlier (libCheckbox.a)
The library should be added to your project along with a libCheckbox.linkwith.cs file
The "linkwith" file communicates to Xamarin.iOS how the static library should be handled. If you open the file you'll see how the LinkWith attribute identifies the static library as well as some linker flags.
using ObjCRuntime;
[assembly: LinkWith ("libCheckbox.a", SmartLink = true, ForceLoad = true)]
The next step for us is to modify the ApiDefintions.cs file to define how Xamarin.iOS and our native Objective-C control interact. We can use the tool Objective Sharpie to auto-generate this file for us, and generally the project easier and faster.
Using Objective Sharpie
Objective Sharpie is tool provided by Xamarin to partially automate the process of creating the definitions needed to bind our Objective-C library to C#. This command line tool can be utilized via the Terminal Application on a Mac. Once you have it installed, the are a few commands that you should to be aware of. The help command lists all of the available options:
sharpie -help
The xcode command gives us information about the versions of Xcode and the iOS/Mac APIs that are present on the machine:
sharpie xcode
The build command creates the ApiDefinition.cs and StructsAndEnums.cs files based on the headers present in our libraries:
sharpie bind
To get started, we'll first need to determine which sdks are present on our machine. We can determine this with the following command:
sharpie xcode -sdks
This will return a list of the current SDKs. Now that we've determine what SDKs are present we can use the build command to create the intitial ApiDefinition.cs and StructsAndEnums.cs for our control. Use the following command to build these files:
sharpie bind --output=GCCheckbox --namespace=GCCheckbox --sdk=iphoneos9.2 <path-to-project>/Checkbox/Checkbox/Checkbox.h
The is the directory where the Checkbox Xcode project resides. In this case I only have a single header to import, though you're also capable of using *.h to import all of the headers in that directory. Once Objective Sharpie generates the new ApiDefintion.cs file, you can use it to replace the original ApiDefinition.cs file in your bindings library project. The generated file should look similar to the following:
using Foundation;
using ObjCRuntime;
using UIKit;
namespace GCCheckbox
{
// @interface Checkbox : UIControl
[BaseType (typeof(UIControl))]
interface Checkbox
{
// -(void)setChecked:(BOOL)isChecked;
[Export ("setChecked:")]
void SetChecked (bool isChecked);
// -(void)setEnabled:(BOOL)isEnabled;
[Export ("setEnabled:")]
void SetEnabled (bool isEnabled);
// @property UIColor * checkColor;
[Export ("checkColor", ArgumentSemantic.Assign)]
UIColor CheckColor { get; set; }
// @property UIColor * boxFillColor;
[Export ("boxFillColor", ArgumentSemantic.Assign)]
UIColor BoxFillColor { get; set; }
// @property UIColor * boxBorderColor;
[Export ("boxBorderColor", ArgumentSemantic.Assign)]
UIColor BoxBorderColor { get; set; }
// @property UIFont * labelFont;
[Export ("labelFont", ArgumentSemantic.Assign)]
UIFont LabelFont { get; set; }
// @property UIColor * labelTextColor;
[Export ("labelTextColor", ArgumentSemantic.Assign)]
UIColor LabelTextColor { get; set; }
// @property BOOL isEnabled;
[Export ("isEnabled")]
bool IsEnabled { get; set; }
// @property BOOL isChecked;
[Export ("isChecked")]
bool IsChecked { get; set; }
// @property BOOL showTextLabel;
[Export ("showTextLabel")]
bool ShowTextLabel { get; set; }
// @property (nonatomic, strong) NSString * text;
[Export ("text", ArgumentSemantic.Strong)]
string Text { get; set; }
}
}
It's worth spending a moment to break down the file that we've created. The [BaseType(typeof(UIControl))] at the top of the file alerts Xamarin that this control subclasses UIControl. The [Export] attribute is used to denote the Objective-C method or property which we're binding to, and below it we're expressing the C# API. Our control is relatively simple so there aren't any changes to make here, but for more complicated projects, it's likely that some changes will be required.
Creating a Test Project
We'll create a separate Xamarin.iOS project to test our control. A simple Single View App should suffice for this testing which you can create via this new project:
- Solution.
- For the Project template, choose iOS App category on the left pane and select SingleViewApp on the right and click next.
- Name your project CheckboxTest.
Create a new Single View Project Next, you can import the earlier bindings library project.
- Right click on your project and select Add -> Add Existing Project...
- Navigate to and Select the Bindings Library Project we created earlier
- The project should now appear in the Solution Explorer, but we need to add it as a reference to our CheckTest project
- With CheckTest expanded, right click references and chose the option Edit References...
- Navigate to the Projects Tab
- Click the checkmark next to the Checkbox.csproj project and hit OK
Add a reference to the Bindings Library project This should be sufficient to use the checkbox control in our test application.
Using the Control
Using the controls in Xamarin.iOS should be a familiar process. We'll want to add a using statement for our GCCheckbox namespace to our ViewController so that we don't need to fully qualify everything. The test code for the checkbox control is actually pretty similar at a high level to what we did in Objective-C:
using System;
using GCCheckbox;
using UIKit;
using CoreGraphics;
using Foundation;
namespace CheckTest
{
public partial class ViewController : UIViewController
{
public Checkbox c;
public ViewController (IntPtr handle) : base (handle)
{
}
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
// Perform any additional setup after loading the view, typically from a nib.
c = new Checkbox();
c.IsEnabled = true;
c.Frame = new CGRect (View.Bounds.Size.Width/2 - 100, View.Bounds.Size.Height/2 - 25, 200, 50);
c.Text = "Checked";
c.ShowTextLabel = true;
c.TouchUpInside += HandleTouchUpInside;
View.AddSubview (c);
}
public override void DidReceiveMemoryWarning ()
{
base.DidReceiveMemoryWarning ();
// Release any cached data, images, etc that aren't in use.
}
private void HandleTouchUpInside(object sender, EventArgs e){
if(c.IsChecked == true){
c.Text = "Checked";
}
else{
c.Text = "Unchecked";
}
}
}
}
Note that TouchUpInside has become a C# style event in Xamarin.iOS which definitely eases the learning curve for .NET developers. In Xamarin.iOS, bindings for other delegate functions can also be created to provide these C# style events if desired. Running the application will deliver a familiar result: Xamarin.iOS Checkbox
Conclusion
The process of creating a Xamarin control involves quite a few steps and can become much more complicated as the complexity of your library increases. Objective Sharpie makes the process somewhat easier by automating the creation of the API definition, but more complicated controls require much more fine tuning of this. It is impressive though that Xamarin is able make the process as straightforward as it is given the differences between Objective-C, C#, the iOS APIs, and .NET. Hopefully has provided a clear look into the process of bringing a Native iOS control in Xamarin.iOS.