insanelab.com insanelab.com

November 10, 2019 - Mobile Development

Advanced Xamarin Bindings Case Study – Deep Dive into iOS – Part II

1. Introduction to Advanced Xamarin Bindings for iOS

In the previous article, we briefly described the major impact that software libraries have on web and application development. We also showed you how to utilize Android libraries in Xamarin. This time we are going to show you how to bring an iOS library written in Swift into the Xamarin world!

In fact, the steps shown below will also work for libraries written in Objective-C (which are easier to bind than Swift).

Imagine that you want to implement a Tinder-like clone in Xamarin. The first thing you would do is search for a Card-Like swipe library.
Fortunately, there is a Swift open source library called Koloda (developed by Yalantis) that will help you replicate this very feature.

Tinder-like iOS Application Made Using Xamarin
Swift Tinder like cards library – Koloda, developed by Yalantis
2. Start with .frameworks

To start a Xamarin iOS Bindings project, we need to build a Swift/Objective-C iOS library package and its dependencies. Normally, they are available in .framework format.

We also need to understand how software libraries are distributed across the following programming frameworks/languages:

  • in .NET world, we use NuGet package manager,
  • in Java/Android environment, the most common way to add library is by using a Gradle build system
  • in iOS space, the most common tool is CocoaPads

Fortunately, Koloda is available in CocoaPads, so we do not have to build a library from GitHub manually.

The easiest way to retrieve this library and its .framework dependencies is to create a new iOS Objective-C XCode project. Then, install its dependencies using the CocoaPad command line, see pictures below:

 

iOS Objective C Code Project for Xamarin
Pod Install for Advanced Xamarin Bindings iOS Project
Podfile for Advanced Xamarin Bindings for iOS project

After installation of Pod, we should build a project to get the appropriate frameworks for both the x86 and ARM architectures. It is required step for the matter of fact that we want to get our bound library working on an iOS simulator and a real iPhone device.

Take a note – inside of our Podfile, we added two configuration lines:

  • we set ENABLE_BITCODE flag to NO (as Xamarin.iOS does not support Bitcode)
  • we set Swift version to 4.2 (as the original library, according to project GitHub, is 4.2)

The Pod installer created a new XCode workspace, so just type in the terminal “open objectivec-ios-test-project.xcworkspace.”

After that, we have to build our project for both iOS and Device in Release mode. You can check where your build is in File->Workspace Settings->Derived data.

In the directory /project_build_path_xcode_derived_data/Build/Products, we have two directories – Release-iphoneos and Release-iphonesimulator.

Inside of those directories, we have directories for:

  • Koloda – inside of our library there is a Koloda.framework
  • pop – Koloda library dependency .framework

 

3. Next, Merge .framework Architectures
We have two .frameworks for each library:
  • framework for Release-iphonesimulator (x86/x86_64)
  • framework for Release-iphoneos (ARM64/ARMv7s)

We have to merge those architectures into one .framework file.

Thankfully, this is an easy step that can be done using the lipo tool (preinstalled with Xcode).  You can save commands presented below to a bash script which can be modified to fit your needs.

  1. Copy Release-iphonesimulator/LibraryName.framework to your bindings working directory.
  2. Create a FAT library (merging architectures) with lipo – terminal/command line.lipo -create Release-iphonesimulator/LibraryName.framework/LibraryName Release-iphoneos/LibraryName.framework/LibraryName -output your_bindings_directory/LibraryName.framework/LibraryName
  3. Copy Swift Modules from Release-iphoneos to your bindings framework directory. (applies only to Swift based libraries – not objective-c one!)cp -rf Release-iphoneos/LibraryName.framework/Modules/LibraryName.swiftmodule your_bindings_directory/LibraryName.framework/Modules/LibraryName.swiftmodule

This step is shown in the attached terminal logs. It must be repeated for both Koloda.framework and it’s dependency pop.framework.

MacBook-Pro:Products przemekraciborski$ ls
Release-iphoneos	Release-iphonesimulator
MacBook-Pro:Products przemekraciborski$ cp -rf Release-iphonesimulator/Koloda/Koloda.framework Koloda.framework
MacBook-Pro:Products przemekraciborski$ ls
Koloda.framework	Release-iphoneos	Release-iphonesimulator
MacBook-Pro:Products przemekraciborski$ lipo -create Release-iphonesimulator/Koloda/Koloda.framework/Koloda Release-iphoneos/Koloda/Koloda.framework/Koloda -output Koloda.framework/Koloda 
MacBook-Pro:Products przemekraciborski$ cp -rf Release-iphoneos/Koloda/Koloda.framework/Modules/Koloda.swiftmodule Koloda.framework/Modules/Koloda.swiftmodule
MacBook-Pro:Products przemekraciborski$ cp -rf Release-iphonesimulator/pop/pop.framework pop.framework
MacBook-Pro:Products przemekraciborski$ lipo -create Release-iphonesimulator/pop/pop.framework/pop Release-iphoneos/pop/pop.framework/pop -output pop.framework/pop 

Next, we should also double-check if we have properly created the FAT library (merged frameworks) with a simple lipo -info command.

MacBook-Pro:Products przemekraciborski$ lipo -info Koloda.framework/Koloda 
Architectures in the fat file: Koloda.framework/Koloda are: i386 x86_64 armv7 arm64 
MacBook-Pro:Products przemekraciborski$ lipo -info pop.framework/pop 
Architectures in the fat file: pop.framework/pop are: i386 x86_64 armv7 arm64 

We have confirmed that we have all required architectures, and it supports both simulators (i386, x86_64) and real devices (armv7, arm64).

4. The Final Step Before We Create the Xamarin Bindings Library for iOS

An Advanced Xamarin Bindings iOS project is built from three types of files:

  • ApiDefinition – This maps Objective C concepts like protocols, classes, etc. to the Xamarin world from provided .framework.
  • StructsAndEnums – This maps structures and constants.
  • .framework native reference – This references to the .framework’s we have created in the previous steps. They are used to map Objective-C code into Xamarin.

It would be too difficult to create these files from scratch. Fortunately, the Objective Sharpie tool exists. With this tool, if you provide the framework, you will receive the ApiDefinition and StructsAndEnums as output.

We only need to bind the Koloda library with the Objective Sharpie because pop.framework is only a dependency, and we do not need to use it in our C# Xamarin project.

…so let’s bind our project!

MacBook-Pro:Products przemekraciborski$ sharpie bind Koloda.framework/Headers/Koloda-Swift.h -sdk iphoneos12.1 -namespace Xamarin.Bindings.Koloda
Parsing 1 header files...

Binding...
  [write] ApiDefinitions.cs
  [write] StructsAndEnums.cs

Binding Analysis:
  Automated binding is complete, but there are a few APIs which have been flagged with [Verify] attributes. While the entire binding should be audited for best API design practices, look
  more closely at APIs with the following Verify attribute hints:

  ConstantsInterfaceAssociation (235 instances):
    There's no foolproof way to determine with which Objective-C interface an extern variable declaration may be associated. Instances of these are bound as [Field] properties in a partial
    interface into a nearby concrete interface to produce a more intuitive API, possibly eliminating the 'Constants' interface altogether.

  MethodToProperty (219 instances):
    An Objective-C method was bound as a C# property due to convention such as taking no parameters and returning a value (non-void return). Often methods like these should be bound as
    properties to surface a nicer API, but sometimes false-positives can occur and the binding should actually be a method.

  StronglyTypedNSArray (42 instances):
    A native NSArray* was bound as NSObject[]. It might be possible to more strongly type the array in the binding based on expectations set through API documentation (e.g. comments in the
    header file) or by examining the array contents through testing. For example, an NSArray* containing only NSNumber* instances can be bound as NSNumber[] instead of NSObject[].

  PlatformInvoke (5975 instances):
    In general P/Invoke bindings are not as correct or complete as Objective-C bindings (at least currently). You may need to fix up the library name (it defaults to '__Internal') and
    return/parameter types manually to conform to C calling conventionsfor the target platform. You may find you don't even want to expose the C API in your binding, but if you do, you'll
    probably also want to relocate the definition to a more appropriate class and expose a stronger type-safe wrapper. For P/Invoke guidance, see http://www.mono-project.com/docs/advanced/
    pinvoke/.

  InferredFromMemberPrefix (103 instances):
    The name of this originally anonymous declaration was taken from a common prefix of its members.

  Once you have verified a Verify attribute, you should remove it from the binding source code. The presence of Verify attributes intentionally cause build failures.
  
  For more information about the Verify attribute hints above, consult the Objective Sharpie documentation by running 'sharpie docs' or visiting the following URL:

    http://xmn.io/sharpie-docs

Done.

Here we have plugged the Koloda-Swift header file into the objective-sharpie tool. This parsed the header file and generated a C# ApiDefinition output.

Objective Sharpie Doesn’t Work for Me
  • plug-in “-sdk iphoneos12.1” option
    Without the -sdk option the Objective Sharpie often fails during the parse step. You can retrieve a list of installed sdks by using the sharpie xcode -sdks command line. A possible side effect is that generated ApiDefinition/StructsAndEnums classes might be bloated with SDK related classes (like UIView).  You will just need to remove them and leave your library-related classes/methods untouched.
  • invalid objective sharpie version
    Microsoft may have their documentation out-dated. This means they’re pointing to an old Objective Sharpie package (3.4.0), which does not support iphoneos12 SDK and might have bugs. The Sharpie update command will not be working either. So, to get the most recent Objective Sharpie version, check the official Xamarin iOS git repository. Makefile contains a link to the new Objective Sharpie (line 72). The most recent Objective Sharpie version is 3.4.23, which can be downloaded here. I believe this is the reason why the Xamarin team is able to produce iOS bindings with ease.

Using the –sdk iphoneos12.1 option with Objective Sharpie resulted in the ApiDefinition and the StructAndEnums being bloated with iOS Framework related classes.

We have to fix this by leaving only Koloda related classes. It turns out that there are no StructsAndEnum related classes in the Koloda framework and the “cleaned up” ApiDefinition is presented below.

using System;
using Foundation;
using Koloda;

namespace Xamarin.Bindings.Koloda
{
	// @interface DraggableCardView
	[DisableDefaultCtor]
	interface DraggableCardView
	{
		// +(instancetype _Nonnull)new __attribute__((deprecated("-init is unavailable")));
		[Static]
		[Export ("new")]
		DraggableCardView New ();

		// -(instancetype _Nullable)initWithCoder:(NSCoder * _Nonnull)aDecoder __attribute__((objc_designated_initializer));
		[Export ("initWithCoder:")]
		[DesignatedInitializer]
		IntPtr Constructor (NSCoder aDecoder);

		// @property (nonatomic) int frame;
		[Export ("frame")]
		int Frame { get; set; }

		// -(id)gestureRecognizer:(UIGestureRecognizer * _Nonnull)gestureRecognizer shouldReceiveTouch:(UITouch * _Nonnull)touch __attribute__((warn_unused_result));
		[Export ("gestureRecognizer:shouldReceiveTouch:")]
		NSObject GestureRecognizer (UIGestureRecognizer gestureRecognizer, UITouch touch);
	}

	// @interface KolodaView
	interface KolodaView
	{
		// -(void)layoutSubviews;
		[Export ("layoutSubviews")]
		void LayoutSubviews ();

		// -(id)pointInside:(id)point withEvent:(UIEvent * _Nullable)event __attribute__((warn_unused_result));
		[Export ("pointInside:withEvent:")]
		NSObject PointInside (NSObject point, [NullAllowed] UIEvent @event);

		// -(instancetype _Nonnull)initWithFrame:(id)frame __attribute__((objc_designated_initializer));
		[Export ("initWithFrame:")]
		[DesignatedInitializer]
		IntPtr Constructor (NSObject frame);

		// -(instancetype _Nullable)initWithCoder:(NSCoder * _Nonnull)aDecoder __attribute__((objc_designated_initializer));
		[Export ("initWithCoder:")]
		[DesignatedInitializer]
		IntPtr Constructor (NSCoder aDecoder);
	}

	// @interface OverlayView
	interface OverlayView
	{
		// -(instancetype _Nonnull)initWithFrame:(id)frame __attribute__((objc_designated_initializer));
		[Export ("initWithFrame:")]
		[DesignatedInitializer]
		IntPtr Constructor (NSObject frame);

		// -(instancetype _Nullable)initWithCoder:(NSCoder * _Nonnull)aDecoder __attribute__((objc_designated_initializer));
		[Export ("initWithCoder:")]
		[DesignatedInitializer]
		IntPtr Constructor (NSCoder aDecoder);
	}
}
5. If That Doesn’t Work, Is Objective Sharpie / Xamarin Bindings Broken?

Looking at generated ApiDefinitions, we can clearly see that most of the important types (given GitHub Koloda documentation) are missing.  Thus, you can try to plug-in the ApiDefinitions file into the Advanced Xamarin Bindings for iOS project, and you will see that the output library will have a lot of missing methods/classes/interfaces.

That does not mean that the bindings are broken. We just missed the fact that the Koloda library might not have Objective-C compatibility.

We can only bind Swift classes/types that can be used in Objective-C. This also means that a “native Objective-C” developer cannot use a Koloda library in his project.

Unfortunately, with the Apple Swift release, there is a common problem in the iOS developer’s world. Often, Swift developers can’t use Objective-C libraries and Objective-C developers can’t use Swift libraries.

So, do we have to develop the whole library from scratch?

…Nope! We’ll just try to make the original Swift library Objective-C compatible!

Which Swift Types Are Compatible With Objective-C and Bindable with Xamarin?

The swift types that have the special attributes “@objc” can be consumed in Objective-C projects. Therefore, they are bindable in Xamarin projects.

According to Apple Documentations:

The @objc attribute makes your Swift API available in the Objective-C and the Objective-C runtime.

Also, the swift types that inherit from NSObject (Objective-C types) can be used. This means that most UI-related Swift libraries (unfortunately Koloda is not one) are bindable without any special preparation. This is because most of the time they inherit from something related to view, like UIView or UIViewController, which are indirect subclasses of NSObject.

I have updated the Koloda library to make it Objective-C compatible. I have also updated Swift version to 5.1 to make Xamarin integration easier (more on that later).

You can clone an updated version and review the changes on my GitHub. This was a relatively easy task.  If you have enough experience, it can be done in 15 minutes.

6. Let’s Bind the Forked and Updated Version of Koloda!

As previously mentioned, the updated version is available on GitHub.

All you have to do is clone and build my Koloda fork, and repeat steps #3 and #4 from this article. Just rerun the scripts we have written with a freshly built .framework :).  You do not have to repeat this for the pop framework (as we have only the updated Koloda).

This time everything will work as expected!

The final binding generated class (with removed iOS related UIKit classes) is available below. Take note that this does not build yet!

 

using System;
using CoreGraphics;
using Foundation;
using ObjCRuntime;
using UIKit;

namespace Xamarin.Bindings.Koloda
{
    // @interface DraggableCardView : UIView <UIGestureRecognizerDelegate>
    [BaseType(typeof(UIView))]
    [DisableDefaultCtor]
    interface DraggableCardView : IUIGestureRecognizerDelegate
    {
        // +(instancetype _Nonnull)new __attribute__((deprecated("-init is unavailable")));
        [Static]
        [Export("new")]
        DraggableCardView New();

        // -(instancetype _Nullable)initWithCoder:(NSCoder * _Nonnull)aDecoder __attribute__((objc_designated_initializer));
        [Export("initWithCoder:")]
        [DesignatedInitializer]
        IntPtr Constructor(NSCoder aDecoder);

        // @property (nonatomic) CGRect frame;
        [Export("frame", ArgumentSemantic.Assign)]
        CGRect Frame { get; set; }

        // -(BOOL)gestureRecognizer:(UIGestureRecognizer * _Nonnull)gestureRecognizer shouldReceiveTouch:(UITouch * _Nonnull)touch __attribute__((warn_unused_result));
        [Export("gestureRecognizer:shouldReceiveTouch:")]
        bool GestureRecognizer(UIGestureRecognizer gestureRecognizer, UITouch touch);
    }

    // @interface KolodaView : UIView
    [BaseType(typeof(UIView))]
    interface KolodaView
    {
        // @property (nonatomic) NSInteger countOfVisibleCards;
        [Export("countOfVisibleCards")]
        nint CountOfVisibleCards { get; set; }

        // @property (nonatomic) BOOL isLoop;
        [Export("isLoop")]
        bool IsLoop { get; set; }

        // @property (readonly, nonatomic) NSInteger currentCardIndex;
        [Export("currentCardIndex")]
        nint CurrentCardIndex { get; }

        // @property (readonly, nonatomic) NSInteger countOfCards;
        [Export("countOfCards")]
        nint CountOfCards { get; }

        // @property (nonatomic, weak) id<KolodaViewDataSource> _Nullable dataSource;
        [NullAllowed, Export("dataSource", ArgumentSemantic.Weak)]
        KolodaViewDataSource DataSource { get; set; }

        [Wrap("WeakDelegate")]
        [NullAllowed]
        KolodaViewDelegate Delegate { get; set; }

        // @property (nonatomic, weak) id<KolodaViewDelegate> _Nullable delegate;
        [NullAllowed, Export("delegate", ArgumentSemantic.Weak)]
        NSObject WeakDelegate { get; set; }

        // @property (readonly, nonatomic) BOOL isAnimating;
        [Export("isAnimating")]
        bool IsAnimating { get; }

        // @property (readonly, nonatomic) BOOL isRunOutOfCards;
        [Export("isRunOutOfCards")]
        bool IsRunOutOfCards { get; }

        // -(void)layoutSubviews;
        [Export("layoutSubviews")]
        void LayoutSubviews();

        // -(CGRect)frameForCardAt:(NSInteger)index __attribute__((warn_unused_result));
        [Export("frameForCardAt:")]
        CGRect FrameForCardAt(nint index);

        // -(void)applyAppearAnimationIfNeeded;
        [Export("applyAppearAnimationIfNeeded")]
        void ApplyAppearAnimationIfNeeded();

        // -(void)revertActionWithDirection:(enum SwipeResultDirection)direction;
        [Export("revertActionWithDirection:")]
        void RevertActionWithDirection(SwipeResultDirection direction);

        // -(void)reloadData;
        [Export("reloadData")]
        void ReloadData();

        // -(void)swipe:(enum SwipeResultDirection)direction force:(BOOL)force;
        [Export("swipe:force:")]
        void Swipe(SwipeResultDirection direction, bool force);

        // -(void)resetCurrentCardIndex;
        [Export("resetCurrentCardIndex")]
        void ResetCurrentCardIndex();

        // -(UIView * _Nullable)viewForCardAt:(NSInteger)index __attribute__((warn_unused_result));
        [Export("viewForCardAt:")]
        [return: NullAllowed]
        UIView ViewForCardAt(nint index);

        // -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent * _Nullable)event __attribute__((warn_unused_result));
        [Export("pointInside:withEvent:")]
        bool PointInside(CGPoint point, [NullAllowed] UIEvent @event);

        // -(instancetype _Nonnull)initWithFrame:(CGRect)frame __attribute__((objc_designated_initializer));
        [Export("initWithFrame:")]
        [DesignatedInitializer]
        IntPtr Constructor(CGRect frame);

        // -(instancetype _Nullable)initWithCoder:(NSCoder * _Nonnull)aDecoder __attribute__((objc_designated_initializer));
        [Export("initWithCoder:")]
        [DesignatedInitializer]
        IntPtr Constructor(NSCoder aDecoder);
    }

    // @protocol KolodaViewDataSource
    [Protocol, Model]
    interface KolodaViewDataSource
    {
        // @required -(NSInteger)kolodaNumberOfCards:(KolodaView * _Nonnull)koloda __attribute__((warn_unused_result));
        [Abstract]
        [Export("kolodaNumberOfCards:")]
        nint KolodaNumberOfCards(KolodaView koloda);

        // @required -(enum DragSpeed)kolodaSpeedThatCardShouldDrag:(KolodaView * _Nonnull)koloda __attribute__((warn_unused_result));
        [Abstract]
        [Export("kolodaSpeedThatCardShouldDrag:")]
        DragSpeed KolodaSpeedThatCardShouldDrag(KolodaView koloda);

        // @required -(UIView * _Nonnull)koloda:(KolodaView * _Nonnull)koloda viewForCardAt:(NSInteger)index __attribute__((warn_unused_result));
        [Abstract]
        [Export("koloda:viewForCardAt:")]
        UIView Koloda(KolodaView koloda, nint index);

        // @required -(OverlayView * _Nullable)koloda:(KolodaView * _Nonnull)koloda viewForCardOverlayAt:(NSInteger)index __attribute__((warn_unused_result));
        [Abstract]
        [Export("koloda:viewForCardOverlayAt:")]
        [return: NullAllowed]
        OverlayView Koloda(KolodaView koloda, nint index);
    }

    // @protocol KolodaViewDelegate
    [Protocol, Model]
    interface KolodaViewDelegate
    {
        // @required -(NSArray * _Nonnull)koloda:(KolodaView * _Nonnull)koloda allowedDirectionsForIndex:(NSInteger)index __attribute__((warn_unused_result));
        [Abstract]
        [Export("koloda:allowedDirectionsForIndex:")]
        [Verify(StronglyTypedNSArray)]
        NSObject[] Koloda(KolodaView koloda, nint index);

        // @required -(BOOL)koloda:(KolodaView * _Nonnull)koloda shouldSwipeCardAt:(NSInteger)index in:(enum SwipeResultDirection)direction __attribute__((warn_unused_result));
        [Abstract]
        [Export("koloda:shouldSwipeCardAt:in:")]
        bool Koloda(KolodaView koloda, nint index, SwipeResultDirection direction);

        // @required -(void)koloda:(KolodaView * _Nonnull)koloda didSwipeCardAt:(NSInteger)index in:(enum SwipeResultDirection)direction;
        [Abstract]
        [Export("koloda:didSwipeCardAt:in:")]
        void Koloda(KolodaView koloda, nint index, SwipeResultDirection direction);

        // @required -(void)kolodaDidRunOutOfCards:(KolodaView * _Nonnull)koloda;
        [Abstract]
        [Export("kolodaDidRunOutOfCards:")]
        void KolodaDidRunOutOfCards(KolodaView koloda);

        // @required -(void)koloda:(KolodaView * _Nonnull)koloda didSelectCardAt:(NSInteger)index;
        [Abstract]
        [Export("koloda:didSelectCardAt:")]
        void Koloda(KolodaView koloda, nint index);

        // @required -(BOOL)kolodaShouldApplyAppearAnimation:(KolodaView * _Nonnull)koloda __attribute__((warn_unused_result));
        [Abstract]
        [Export("kolodaShouldApplyAppearAnimation:")]
        bool KolodaShouldApplyAppearAnimation(KolodaView koloda);

        // @required -(BOOL)kolodaShouldMoveBackgroundCard:(KolodaView * _Nonnull)koloda __attribute__((warn_unused_result));
        [Abstract]
        [Export("kolodaShouldMoveBackgroundCard:")]
        bool KolodaShouldMoveBackgroundCard(KolodaView koloda);

        // @required -(BOOL)kolodaShouldTransparentizeNextCard:(KolodaView * _Nonnull)koloda __attribute__((warn_unused_result));
        [Abstract]
        [Export("kolodaShouldTransparentizeNextCard:")]
        bool KolodaShouldTransparentizeNextCard(KolodaView koloda);

        // @required -(void)koloda:(KolodaView * _Nonnull)koloda draggedCardWithPercentage:(CGFloat)finishPercentage in:(enum SwipeResultDirection)direction;
        [Abstract]
        [Export("koloda:draggedCardWithPercentage:in:")]
        void Koloda(KolodaView koloda, nfloat finishPercentage, SwipeResultDirection direction);

        // @required -(void)kolodaDidResetCard:(KolodaView * _Nonnull)koloda;
        [Abstract]
        [Export("kolodaDidResetCard:")]
        void KolodaDidResetCard(KolodaView koloda);

        // @required -(CGFloat)kolodaSwipeThresholdRatioMargin:(KolodaView * _Nonnull)koloda __attribute__((warn_unused_result));
        [Abstract]
        [Export("kolodaSwipeThresholdRatioMargin:")]
        nfloat KolodaSwipeThresholdRatioMargin(KolodaView koloda);

        // @required -(void)koloda:(KolodaView * _Nonnull)koloda didShowCardAt:(NSInteger)index;
        [Abstract]
        [Export("koloda:didShowCardAt:")]
        void Koloda(KolodaView koloda, nint index);

        // @required -(BOOL)koloda:(KolodaView * _Nonnull)koloda shouldDragCardAt:(NSInteger)index __attribute__((warn_unused_result));
        [Abstract]
        [Export("koloda:shouldDragCardAt:")]
        bool Koloda(KolodaView koloda, nint index);

        // @required -(void)kolodaPanBegan:(KolodaView * _Nonnull)koloda card:(DraggableCardView * _Nonnull)card;
        [Abstract]
        [Export("kolodaPanBegan:card:")]
        void KolodaPanBegan(KolodaView koloda, DraggableCardView card);

        // @required -(void)kolodaPanFinished:(KolodaView * _Nonnull)koloda card:(DraggableCardView * _Nonnull)card;
        [Abstract]
        [Export("kolodaPanFinished:card:")]
        void KolodaPanFinished(KolodaView koloda, DraggableCardView card);
    }

    // @interface OverlayView : UIView
    [BaseType(typeof(UIView))]
    interface OverlayView
    {
        // -(instancetype _Nonnull)initWithFrame:(CGRect)frame __attribute__((objc_designated_initializer));
        [Export("initWithFrame:")]
        [DesignatedInitializer]
        IntPtr Constructor(CGRect frame);

        // -(instancetype _Nullable)initWithCoder:(NSCoder * _Nonnull)aDecoder __attribute__((objc_designated_initializer));
        [Export("initWithCoder:")]
        [DesignatedInitializer]
        IntPtr Constructor(NSCoder aDecoder);
    }
}
using System;
using ObjCRuntime;

namespace Xamarin.Bindings.Koloda
{
    [Native]
    public enum DragSpeed : nint
    {
        Slow = 0,
        Moderate = 1,
        Default = 2,
        Fast = 3
    }

    [Native]
    public enum SwipeResultDirection : nint
    {
        Left = 0,
        Right = 1,
        Up = 2,
        Down = 3,
        TopLeft = 4,
        TopRight = 5,
        BottomLeft = 6,
        BottomRight = 7
    }
}
7. How to Make the Project Build? Fixing Objective Sharpie Generated Code

We need to slightly adjust this code to build and completely bind this library.

As for StructsAndEnum.cs, this is relatively easy because every native enum should inherit from “long” or “ulong.”

For ApiDefinitions, we should:

  • Remove any [Verify] attributes because they instruct us to improve quality of bindings here (perhaps by returning a more specific object instead of NSObject)
  • Remove the method names duplication and improve the quality of the bindings by introducing more “human naming” 🙂
  • Remove constructor-like methods that take NSCoder as a method parameter. Those kinds of constructors are automatically generated by the bindings project.

Those are updated classes that *almost* build 😉

using System;
using CoreGraphics;
using Foundation;
using ObjCRuntime;
using UIKit;

namespace Xamarin.Bindings.Koloda
{
    // @interface DraggableCardView : UIView <UIGestureRecognizerDelegate>
    [BaseType(typeof(UIView))]
    [DisableDefaultCtor]
    interface DraggableCardView : IUIGestureRecognizerDelegate
    {
        // +(instancetype _Nonnull)new __attribute__((deprecated("-init is unavailable")));
        [Static]
        [Export("new")]
        DraggableCardView New();

        // @property (nonatomic) CGRect frame;
        [Export("frame", ArgumentSemantic.Assign)]
        CGRect Frame { get; set; }

        // -(BOOL)gestureRecognizer:(UIGestureRecognizer * _Nonnull)gestureRecognizer shouldReceiveTouch:(UITouch * _Nonnull)touch __attribute__((warn_unused_result));
        [Export("gestureRecognizer:shouldReceiveTouch:")]
        bool GestureRecognizer(UIGestureRecognizer gestureRecognizer, UITouch touch);
    }

    // @interface KolodaView : UIView
    [BaseType(typeof(UIView))]
    interface KolodaView
    {
        // @property (nonatomic) NSInteger countOfVisibleCards;
        [Export("countOfVisibleCards")]
        nint CountOfVisibleCards { get; set; }

        // @property (nonatomic) BOOL isLoop;
        [Export("isLoop")]
        bool IsLoop { get; set; }

        // @property (readonly, nonatomic) NSInteger currentCardIndex;
        [Export("currentCardIndex")]
        nint CurrentCardIndex { get; }

        // @property (readonly, nonatomic) NSInteger countOfCards;
        [Export("countOfCards")]
        nint CountOfCards { get; }

        // @property (nonatomic, weak) id<KolodaViewDataSource> _Nullable dataSource;
        [NullAllowed, Export("dataSource", ArgumentSemantic.Weak)]
        KolodaViewDataSource DataSource { get; set; }

        [Wrap("WeakDelegate")]
        [NullAllowed]
        KolodaViewDelegate Delegate { get; set; }

        // @property (nonatomic, weak) id<KolodaViewDelegate> _Nullable delegate;
        [NullAllowed, Export("delegate", ArgumentSemantic.Weak)]
        NSObject WeakDelegate { get; set; }

        // @property (readonly, nonatomic) BOOL isAnimating;
        [Export("isAnimating")]
        bool IsAnimating { get; }

        // @property (readonly, nonatomic) BOOL isRunOutOfCards;
        [Export("isRunOutOfCards")]
        bool IsRunOutOfCards { get; }

        // -(void)layoutSubviews;
        [Export("layoutSubviews")]
        void LayoutSubviews();

        // -(CGRect)frameForCardAt:(NSInteger)index __attribute__((warn_unused_result));
        [Export("frameForCardAt:")]
        CGRect FrameForCardAt(nint index);

        // -(void)applyAppearAnimationIfNeeded;
        [Export("applyAppearAnimationIfNeeded")]
        void ApplyAppearAnimationIfNeeded();

        // -(void)revertActionWithDirection:(enum SwipeResultDirection)direction;
        [Export("revertActionWithDirection:")]
        void RevertActionWithDirection(SwipeResultDirection direction);

        // -(void)reloadData;
        [Export("reloadData")]
        void ReloadData();

        // -(void)swipe:(enum SwipeResultDirection)direction force:(BOOL)force;
        [Export("swipe:force:")]
        void Swipe(SwipeResultDirection direction, bool force);

        // -(void)resetCurrentCardIndex;
        [Export("resetCurrentCardIndex")]
        void ResetCurrentCardIndex();

        // -(UIView * _Nullable)viewForCardAt:(NSInteger)index __attribute__((warn_unused_result));
        [Export("viewForCardAt:")]
        [return: NullAllowed]
        UIView ViewForCardAt(nint index);

        // -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent * _Nullable)event __attribute__((warn_unused_result));
        [Export("pointInside:withEvent:")]
        bool PointInside(CGPoint point, [NullAllowed] UIEvent @event);

        // -(instancetype _Nonnull)initWithFrame:(CGRect)frame __attribute__((objc_designated_initializer));
        [Export("initWithFrame:")]
        [DesignatedInitializer]
        IntPtr Constructor(CGRect frame);
    }

    // @protocol KolodaViewDataSource
    [Protocol, Model]
    interface KolodaViewDataSource
    {
        // @required -(NSInteger)kolodaNumberOfCards:(KolodaView * _Nonnull)koloda __attribute__((warn_unused_result));
        [Abstract]
        [Export("kolodaNumberOfCards:")]
        nint KolodaNumberOfCards(KolodaView koloda);

        // @required -(enum DragSpeed)kolodaSpeedThatCardShouldDrag:(KolodaView * _Nonnull)koloda __attribute__((warn_unused_result));
        [Abstract]
        [Export("kolodaSpeedThatCardShouldDrag:")]
        DragSpeed KolodaSpeedThatCardShouldDrag(KolodaView koloda);

        // @required -(UIView * _Nonnull)koloda:(KolodaView * _Nonnull)koloda viewForCardAt:(NSInteger)index __attribute__((warn_unused_result));
        [Abstract]
        [Export("koloda:viewForCardAt:")]
        UIView ViewForCardAt(KolodaView koloda, nint index);

        // @required -(OverlayView * _Nullable)koloda:(KolodaView * _Nonnull)koloda viewForCardOverlayAt:(NSInteger)index __attribute__((warn_unused_result));
        [Abstract]
        [Export("koloda:viewForCardOverlayAt:")]
        [return: NullAllowed]
        OverlayView ViewForCardOverlayAt(KolodaView koloda, nint index);
    }

    // @protocol KolodaViewDelegate
    [Protocol, Model]
    interface KolodaViewDelegate
    {
        // @required -(NSArray * _Nonnull)koloda:(KolodaView * _Nonnull)koloda allowedDirectionsForIndex:(NSInteger)index __attribute__((warn_unused_result));
        [Abstract]
        [Export("koloda:allowedDirectionsForIndex:")]
       // [Verify(StronglyTypedNSArray)]
        NSObject[] GetAllowedDirectionsForIndex(KolodaView koloda, nint index);

        // @required -(BOOL)koloda:(KolodaView * _Nonnull)koloda shouldSwipeCardAt:(NSInteger)index in:(enum SwipeResultDirection)direction __attribute__((warn_unused_result));
        [Abstract]
        [Export("koloda:shouldSwipeCardAt:in:")]
        bool ShouldSwipeCardAt(KolodaView koloda, nint index, SwipeResultDirection direction);

        // @required -(void)koloda:(KolodaView * _Nonnull)koloda didSwipeCardAt:(NSInteger)index in:(enum SwipeResultDirection)direction;
        [Abstract]
        [Export("koloda:didSwipeCardAt:in:")]
        void DidSwipeCardAt(KolodaView koloda, nint index, SwipeResultDirection direction);

        // @required -(void)kolodaDidRunOutOfCards:(KolodaView * _Nonnull)koloda;
        [Abstract]
        [Export("kolodaDidRunOutOfCards:")]
        void KolodaDidRunOutOfCards(KolodaView koloda);

        // @required -(void)koloda:(KolodaView * _Nonnull)koloda didSelectCardAt:(NSInteger)index;
        [Abstract]
        [Export("koloda:didSelectCardAt:")]
        void DidSelectCardAt(KolodaView koloda, nint index);

        // @required -(BOOL)kolodaShouldApplyAppearAnimation:(KolodaView * _Nonnull)koloda __attribute__((warn_unused_result));
        [Abstract]
        [Export("kolodaShouldApplyAppearAnimation:")]
        bool KolodaShouldApplyAppearAnimation(KolodaView koloda);

        // @required -(BOOL)kolodaShouldMoveBackgroundCard:(KolodaView * _Nonnull)koloda __attribute__((warn_unused_result));
        [Abstract]
        [Export("kolodaShouldMoveBackgroundCard:")]
        bool KolodaShouldMoveBackgroundCard(KolodaView koloda);

        // @required -(BOOL)kolodaShouldTransparentizeNextCard:(KolodaView * _Nonnull)koloda __attribute__((warn_unused_result));
        [Abstract]
        [Export("kolodaShouldTransparentizeNextCard:")]
        bool KolodaShouldTransparentizeNextCard(KolodaView koloda);

        // @required -(void)koloda:(KolodaView * _Nonnull)koloda draggedCardWithPercentage:(CGFloat)finishPercentage in:(enum SwipeResultDirection)direction;
        [Abstract]
        [Export("koloda:draggedCardWithPercentage:in:")]
        void DraggedCardWithPercentage(KolodaView koloda, nfloat finishPercentage, SwipeResultDirection direction);

        // @required -(void)kolodaDidResetCard:(KolodaView * _Nonnull)koloda;
        [Abstract]
        [Export("kolodaDidResetCard:")]
        void KolodaDidResetCard(KolodaView koloda);

        // @required -(CGFloat)kolodaSwipeThresholdRatioMargin:(KolodaView * _Nonnull)koloda __attribute__((warn_unused_result));
        [Abstract]
        [Export("kolodaSwipeThresholdRatioMargin:")]
        nfloat KolodaSwipeThresholdRatioMargin(KolodaView koloda);

        // @required -(void)koloda:(KolodaView * _Nonnull)koloda didShowCardAt:(NSInteger)index;
        [Abstract]
        [Export("koloda:didShowCardAt:")]
        void DidShowCardAt(KolodaView koloda, nint index);

        // @required -(BOOL)koloda:(KolodaView * _Nonnull)koloda shouldDragCardAt:(NSInteger)index __attribute__((warn_unused_result));
        [Abstract]
        [Export("koloda:shouldDragCardAt:")]
        bool ShouldDragCardAt(KolodaView koloda, nint index);

        // @required -(void)kolodaPanBegan:(KolodaView * _Nonnull)koloda card:(DraggableCardView * _Nonnull)card;
        [Abstract]
        [Export("kolodaPanBegan:card:")]
        void KolodaPanBegan(KolodaView koloda, DraggableCardView card);

        // @required -(void)kolodaPanFinished:(KolodaView * _Nonnull)koloda card:(DraggableCardView * _Nonnull)card;
        [Abstract]
        [Export("kolodaPanFinished:card:")]
        void KolodaPanFinished(KolodaView koloda, DraggableCardView card);
    }

    // @interface OverlayView : UIView
    [BaseType(typeof(UIView))]
    interface OverlayView
    {
        // -(instancetype _Nonnull)initWithFrame:(CGRect)frame __attribute__((objc_designated_initializer));
        [Export("initWithFrame:")]
        [DesignatedInitializer]
        IntPtr Constructor(CGRect frame); 
    }
}
using System;
using ObjCRuntime;

namespace Xamarin.Bindings.Koloda
{
    [Native]
    public enum DragSpeed : long
    {
        Slow = 0,
        Moderate = 1,
        Default = 2,
        Fast = 3
    }

    [Native]
    public enum SwipeResultDirection : long
    {
        Left = 0,
        Right = 1,
        Up = 2,
        Down = 3,
        TopLeft = 4,
        TopRight = 5,
        BottomLeft = 6,
        BottomRight = 7
    }
}
8. Still Fails with Type Not Found!

When we start to build the updated code, we still get two build errors. The KolodaViewDataSource and the KolodaViewDelegate types are missing!

When we dig through the bindings’ generated classes (obj folder), we can see that those types exist but with slightly different names like – KolodaViewDataSourceWrapper. 

Those classes are so-called class protocol in Swift. They do not inherit from the NSObject directly (but they are Obj-C compatibile), so they cannot be created directly by the Bindings Generator. Remember, we can use only NSObject based in Xamarin.

As a workaround, Xamarin generates a IKolodaViewDataSourceINativeObject interface and a managed marshal wrapper called KolodaViewDataSourceWrapper that is indeed a NSObject implementation.

This, unfortunately, forces us to rely on weakly typed properties. We have to change strongly typed KolodaViewDataSource/KolodaViewDelegate properties in ApiDefinitions.cs to the weakly typed – NSObject.

The updated KolodaView class (ApiDefinitions.cs) is presented below.

// @interface KolodaView : UIView
    [BaseType(typeof(UIView))]
    interface KolodaView
    {
        // @property (nonatomic) NSInteger countOfVisibleCards;
        [Export("countOfVisibleCards")]
        nint CountOfVisibleCards { get; set; }

        // @property (nonatomic) BOOL isLoop;
        [Export("isLoop")]
        bool IsLoop { get; set; }

        // @property (readonly, nonatomic) NSInteger currentCardIndex;
        [Export("currentCardIndex")]
        nint CurrentCardIndex { get; }

        // @property (readonly, nonatomic) NSInteger countOfCards;
        [Export("countOfCards")]
        nint CountOfCards { get; }

        // @property (nonatomic, weak) id<KolodaViewDataSource> _Nullable dataSource;
        [NullAllowed, Export("dataSource", ArgumentSemantic.Weak)]
        // We had to change that to NSObject
        NSObject DataSource { get; set; }

        /*
        [Wrap("WeakDelegate")]
        [NullAllowed]
        KolodaViewDelegate Delegate { get; set; }

        We have to remove this as it just wraps weakly typed delegate.
        */

        // @property (nonatomic, weak) id<KolodaViewDelegate> _Nullable delegate;
        [NullAllowed, Export("delegate", ArgumentSemantic.Weak)]
        NSObject WeakDelegate { get; set; }

        // @property (readonly, nonatomic) BOOL isAnimating;
        [Export("isAnimating")]
        bool IsAnimating { get; }

        // @property (readonly, nonatomic) BOOL isRunOutOfCards;
        [Export("isRunOutOfCards")]
        bool IsRunOutOfCards { get; }

        // -(void)layoutSubviews;
        [Export("layoutSubviews")]
        void LayoutSubviews();

        // -(CGRect)frameForCardAt:(NSInteger)index __attribute__((warn_unused_result));
        [Export("frameForCardAt:")]
        CGRect FrameForCardAt(nint index);

        // -(void)applyAppearAnimationIfNeeded;
        [Export("applyAppearAnimationIfNeeded")]
        void ApplyAppearAnimationIfNeeded();

        // -(void)revertActionWithDirection:(enum SwipeResultDirection)direction;
        [Export("revertActionWithDirection:")]
        void RevertActionWithDirection(SwipeResultDirection direction);

        // -(void)reloadData;
        [Export("reloadData")]
        void ReloadData();

        // -(void)swipe:(enum SwipeResultDirection)direction force:(BOOL)force;
        [Export("swipe:force:")]
        void Swipe(SwipeResultDirection direction, bool force);

        // -(void)resetCurrentCardIndex;
        [Export("resetCurrentCardIndex")]
        void ResetCurrentCardIndex();

        // -(UIView * _Nullable)viewForCardAt:(NSInteger)index __attribute__((warn_unused_result));
        [Export("viewForCardAt:")]
        [return: NullAllowed]
        UIView ViewForCardAt(nint index);

        // -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent * _Nullable)event __attribute__((warn_unused_result));
        [Export("pointInside:withEvent:")]
        bool PointInside(CGPoint point, [NullAllowed] UIEvent @event);

        // -(instancetype _Nonnull)initWithFrame:(CGRect)frame __attribute__((objc_designated_initializer));
        [Export("initWithFrame:")]
        [DesignatedInitializer]
        IntPtr Constructor(CGRect frame);
    }
9. Lets Polish Our Bindings!

Now, when everything builds, we can slightly improve the quality of our generated bindings.

  • We had one type decorated with [Verify(StronglyTypedNSArray], which means that we can improve this method by providing “stronger type.” Unfortunately, this is not the case with this project because the Swift class returns NSArray here.
  • We have removed strong types from the ApiDefinitions to make the project build. We can partially restore this “strong typing” by providing some kind of C# Extension method.

Below, I have presented the final cleanup adjustments to the library bindings.

 

using Foundation;
using Xamarin.Bindings.Koloda;

namespace Koloda
{
    public static class KolodaViewExtensions
    {
        public static void SetDataSource(this KolodaView kolodaView, IKolodaViewDataSource kolodaViewDataSource)
        {
            kolodaView.DataSource = kolodaViewDataSource as NSObject;
        }

        public static void SetDelegate(this KolodaView kolodaView, IKolodaViewDelegate kolodaViewDelegate)
        {
            kolodaView.WeakDelegate = kolodaViewDelegate as NSObject;
        }
    }
}
10. Time to Integrate the Binding Library in Our Xamarin.iOS Project

Now that we have our bindings library ready, we can plug into our sample project.  Just add a reference to the Xamarin.iOS bindings library we created in the previous steps.

Take notice that every application using the Swift bindings library has to add the appropriate SwiftSupport nuget packages.

If you are curious on which Swift libraries Koloda library is dependent on, we can use the otool utility.

This step is required for proper before Swift 5 but can be omitted when you are targeting Swift 5+. Swift Support NuGet package for 5+ automatically adds all required dependencies.

MacBook-Pro:Bindings przemekraciborski$ otool -l Koloda.framework/Koloda | grep libswift
         name @rpath/libswiftCore.dylib (offset 24)
         name @rpath/libswiftCoreFoundation.dylib (offset 24)
         name @rpath/libswiftCoreGraphics.dylib (offset 24)
         name @rpath/libswiftCoreImage.dylib (offset 24)
         name @rpath/libswiftDarwin.dylib (offset 24)
         name @rpath/libswiftDispatch.dylib (offset 24)
         name @rpath/libswiftFoundation.dylib (offset 24)
         name @rpath/libswiftMetal.dylib (offset 24)
         name @rpath/libswiftObjectiveC.dylib (offset 24)
         name @rpath/libswiftQuartzCore.dylib (offset 24)
         name @rpath/libswiftUIKit.dylib (offset 24)

After we retrieve the list of Swift libraries that was used by Koloda, we just need to confirm which version of Swift was used.

The quickest way is to do this is to check the first line of the Swift Header file. Then, add the appropriate NuGets (one package Xamarin.Swift for Swift 5+, multiple packages named Xamarin.Swift4… for Swift 4)

MacBook-Pro:Bindings przemekraciborski$ vi Koloda.framework/Headers/Koloda-Swift.h 

// Generated by Apple Swift version 5.1 (swiftlang-1100.0.270.13 clang-1100.0.33.7)
...
How Swift to Xamarin integration differs between Swift 4 and 5 ?

Xamarin integration is done via Xamarin.Swift library created by Flash3001.

  • If you are using pre-Swift5 library you need to have matching XCode and Swift version installed on your OSX and/or build machine (this is almost impossible to achieve these days so I never use Swift 4+ libraries anymore)
  • Swift 4 libraries require you to check dependency list via otool manually – afterwards you have to install multiple Xamarin.Swift4 packages with version equal to your Swift version, any issue here will result in app crashing on startup
  • Xamarin.Swift4 does not work well with new NetStandard project scheme – it might not work if you are using <PackageReference> instead of good, old packages.config (there are some workarounds but I prefer going with Swift 5 anyway)
  • Every app which consume Swift library needs to have SwiftSupport folder with used swift libraries inside final IPA – or your application will not pass Apple AppStore Review process
  • If you are Xamarin.Swift4 packages consumer – you have to add SwiftSupport folder manually
  • If you target Swift 5 and use Xamarin.Swift – this will be automatically done for you via build task (by Xamarin.Swift package).

 

Briefly – don’t use Swift version less than 5 – it is too much troublesome/time consuming.

10. Sample app does not build anyway!

When we try to build our sample app integrated with Xamarin.Bindings.Koloda that we have created in previous steps, we will receive bunch errors (see below screenshots)

Errors states that Xamarin/Mono compiler could not find objective-c classes that we have linked.
Indeed – we can only bind Objective-C related types – however we have our Swift classes marked as @objc compatible – what is wrong here!?

The reason is simple – by marking Swift class/protocol as @objc compatible XCode/Swift compiler automatically generates some kind of Objective-C Bridge headers.

Inside this header – each Swift type with @objc attribute gets registered/assigned into the Objective-C world.

Let’s try to figure out how Swift types are named by checking Swift Bridge header which is located in: Koloda.Framework/Header/Koloda-Swift.h

Alright – we can see that Swift types are registered through SWIFT_CLASS “suffix/attribute”.

We just have to instruct Xamarin Bindings Generator how Swift classes are registered.

You need to use “Name” property inside of each interface attribute – and sample app will build and run.

Working sample and bindings project is available on my github.

11. Final

This is the comprehensive how to use Swift libraries inside Xamarin world. With this knowledge you should be able to build and consume any Swift/Objective-C library you want to.

With proper practice it will not be difficult anymore – you will be able to bind most of Swift libraries in ~15 minutes.

… and by the way – if you have strived to the finish of this article – go ahead and read one bonus advice below 🙂

How to diagnose iOS app startup/runtime crashes that does not have debug stacktrace?

This is an often case that iOS app crash on runtime (especially when using Xamarin Swift Bindings library) and we do not have any debug stacktrace.

To figure out what caused that we can use OSX built-in tool called… “Console” 🙂

I have presented a screenshot when KolodaSample crashed due to “wrong achitecture”. The problem lied in fact that I have added incorrect Xamarin.Swift nuget package into the project.

Take note – that you are also able to retrieve crashlogs from your real device via Console app (when iPhone connected) or directly through iPhone app details.

What is your challenge?

Tell us with any means provided. We'd love to hear from you!