DEV Community

Csaba8472
Csaba8472

Posted on

4 1

Binding Lottie (or any other Swift framework with UI) in MAUI

This post is part of #MAUIUIJuly. It's a great initiative to share helpful content with the community.

What is Lottie?

Lottie is a library for Android, iOS, Web, and Windows that parses Adobe After Effects animations exported as json with Bodymovin and renders them natively on mobile and on the web!
For the first time, designers can create and ship beautiful animations without an engineer painstakingly recreating it by hand.

Why bind?

When I wrote my app which needed Lottie animations, I found LottieXamarin library, but that only works with Xamarin.Forms not with MAUI. I tried to update it, however I've run into multiple issues and at the same time I've realised only older Lottie versions can be binded, because from March of 2019, Lottie is a Swift library, which means it can't be binded easily. With the binding solution I'll show below you can bind the latest version.

The other option is Skottie which was released after I'd already done my Lottie binding. I think Skottie is a viable alternative, based on your project needs can you select the best option.

Getting Started

I have two great resources for binding a library:

Swift binding walkthrough

To bind Lottie I chose slim binding approach, please check the video for the details.

This post would like to give a high level overview how view binding can be done, for the details please check the resources above and LottieMaui repo.

Let's start with Android

LottieProxy

LottieAnimationViewWrapper does what the name suggest. It wraps LottieAnimationView which means, it initializes, exposes the View and makes the caller able to set the animation. The class itself is in an android library.

In the video above there is a script which downloads all the dependencies. Here you can find it, so you can easily paste it in your gradle.

public class LottieAnimationViewWrapper {
private final LottieAnimationView lottieAnimationView;
private final LottieSdkCallback lottieSdkCallback;
private final Context _context;
public LottieAnimationViewWrapper(Context context, LottieSdkCallback sdkCallback){
_context = context;
lottieAnimationView = new LottieAnimationView(context);
lottieAnimationView.setRepeatMode(LottieDrawable.RESTART);
lottieAnimationView.setRepeatCount(LottieDrawable.INFINITE);
lottieSdkCallback = sdkCallback;
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
lottieAnimationView.setLayoutParams(lp);
lottieAnimationView.setFailureListener(result -> {
lottieSdkCallback.onFailure(result.getLocalizedMessage());
});
lottieAnimationView.addAnimatorListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
System.out.println("Animation: "+"start");
}
@Override
public void onAnimationEnd(Animator animation) {
System.out.println("Animation: "+"end");
}
@Override
public void onAnimationCancel(Animator animation) {
System.out.println("Animation: "+"cancel");
}
@Override
public void onAnimationRepeat(Animator animation) {
System.out.println("Animation: " + "repeat");
}
});
}
public View getView() {
return lottieAnimationView;
}
public void setAnimation(String animation){
LottieCompositionFactory.fromAsset(_context, animation).addListener(result -> {
System.out.println("Loaded!!!!!!!!!!!!!! "+result.getBounds() + " " + result.toString());
lottieAnimationView.setComposition(result);
lottieAnimationView.playAnimation();
}).addFailureListener(result -> result.printStackTrace());
}
}

Lottie.Proxy.Binding

Nothing to see here, this is to connect the .net world with the android library. This project simplifies our job with not forcing us to bind 100% of any library, but only what we need (and already wrote in LottieProxy).

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-android</TargetFramework>
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<AndroidAarLibrary Include="Jar\mylibrary-release.aar" />
</ItemGroup>
</Project>

Then make it work on iOS

LottieProxyiOS

On iOS side to keep it clean, I've used Carthage to get Lottie. The wrapping is pretty much the same, the goal is to make sure we can set the animation and get the view.

import Foundation
import UIKit
import Lottie
@objc(LottieAnimationViewWrapper)
public class LottieAnimationViewWrapper: NSObject {
private let animationView = AnimationView()
@objc
public func initAnimation() {
animationView.loopMode = .loop
animationView.contentMode = .scaleAspectFit
}
@objc
public func setAnimation(animation: String) {
animationView.animation = Animation.named(animation)
animationView.play { (finished) in
/// Animation finished
}
}
@objc
public func getView() -> UIView {
return animationView;
}
}

I've borrowed the build script from the Swift binding walkthrough.

LottieProxyiOS.Binding

After you run sharpie you'll get an interface close to this:

using Foundation;
using UIKit;
using ObjCRuntime;
namespace LottieProxyiOS.Binding
{
// @interface LottieAnimationViewWrapper : NSObject
[BaseType(typeof(NSObject))]
interface LottieAnimationViewWrapper
{
// -(void)initAnimation __attribute__((objc_method_family("none")));
[Export("initAnimation")]
void InitAnimation();
// -(void)setAnimationWithAnimation:(NSString * _Nonnull)animation;
[Export("setAnimationWithAnimation:")]
void SetAnimationWithAnimation(string animation);
// -(UIView * _Nonnull)getView __attribute__((warn_unused_result("")));
[Export("getView")]
UIView View { get; }
}
}

The good thing is you have 100% control over what will be binded, so if you run any issue, it's much easier to fix as you would do the binding against the whole Lottie framework.

Don't forget to include all the frameworks and you see absolute paths here because relative paths don't always work for me.

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-ios</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
<IsBindingProject>true</IsBindingProject>
</PropertyGroup>
<ItemGroup>
<ObjcBindingApiDefinition Include="ApiDefinition.cs" />
<ObjcBindingCoreSource Include="StructsAndEnums.cs" />
</ItemGroup>
<ItemGroup>
<NativeReference Include="/Users/csabahuszar/Projects/LottieMaui/LottieProxyiOS/VendorFrameworks/swift-framework-proxy/LottieProxyiOS.framework">
<Kind>Framework</Kind>
<Frameworks>Foundation UIKit</Frameworks>
</NativeReference>
</ItemGroup>
<ItemGroup>
<NativeReference Include="/Users/csabahuszar/Projects/LottieMaui/LottieProxyiOS/Carthage/Build/Lottie.xcframework">
<Kind>Framework</Kind>
<Frameworks>Foundation UIKit</Frameworks>
</NativeReference>
</ItemGroup>
</Project>

Common

Lottie.Maui

Okay, we have the Lottie binding, now what? Let's use one of the greatest addition of MAUI, handlers. You'll find in this post the steps for LottieMaui, and please find here the detailed description.

First step is to define our interface:

namespace Lottie.Maui;
public interface ILottieView: IView
{
string Animation { get; set; }
bool Loops { get; }
bool IsPlaying { get; set; }
void PlaybackCompleted();
}
view raw ILottieView hosted with ❤ by GitHub

Then implement the interface:

namespace Lottie.Maui;
public class LottieView : View, ILottieView
{
private string _animation;
public string Animation
{
get => _animation;
set {
_animation = value;
}
}
public bool Loops => true;
private bool _isPlaying;
public bool IsPlaying { get => _isPlaying; set => _isPlaying = value; }
public void PlaybackCompleted()
{
throw new NotImplementedException();
}
}
view raw LottieView hosted with ❤ by GitHub

Then create the LottieHandler with mappers. This is a partial class, the most interesting things will be happening in the Platform folder. Basically, there will be connected the binded C# libraries with our cross-platform app. As you see below I've only implemented what I needed. I wanted to show an animation loop, so I only implemented those parts. Later, anything else can be easily added.

Don't forget to add the handler:

handlers.AddHandler(typeof(ILottieView), typeof(LottieHandler));
Enter fullscreen mode Exit fullscreen mode
#if __IOS__ || MACCATALYST
using PlatformView = UIKit.UIView;
#elif ANDROID
using PlatformView = AndroidX.AppCompat.Widget.AppCompatImageView;
#endif
namespace Lottie.Maui;
public partial class LottieHandler
{
public static readonly PropertyMapper<ILottieView, LottieHandler> Mapper = new(ViewMapper)
{
[nameof(ILottieView.Animation)] = MapAnimation,
[nameof(ILottieView.Loops)] = MapLoops,
[nameof(ILottieView.IsPlaying)] = MapIsPlaying,
};
public static readonly CommandMapper<ILottieView, LottieHandler> CommmandMapper = new()
{
["Reset"] = MapResetAnimation,
};
public LottieHandler() : base(Mapper, CommmandMapper)
{
}
}
view raw LottieHandler hosted with ❤ by GitHub

Lottie.Maui iOS handler

namespace Lottie.Maui;
public partial class LottieHandler : ViewHandler<ILottieView, UIView>
{
LottieAnimationViewWrapper platformView;
private static void MapResetAnimation(LottieHandler handler, ILottieView view, object args)
{
//handler.PlatformView.Progress = 0;
}
private static void MapIsPlaying(LottieHandler handler, ILottieView view)
{
/*
if (view.IsPlaying)
handler.PlatformView.PlayAnimation();
else
handler.PlatformView.PauseAnimation();
*/
}
private static void MapLoops(LottieHandler handler, ILottieView view)
{
// handler.PlatformView.Loop(view.Loops);
}
public static void MapAnimation(LottieHandler handler, ILottieView view)
{
var name = view?.Animation.Split(".")[0];
handler.UpdateAnimation(name);
}
void UpdateAnimation(string name)
{
platformView.SetAnimationWithAnimation(name);
}
protected override UIView CreatePlatformView()
{
platformView = new LottieAnimationViewWrapper();
platformView.InitAnimation();
return platformView.View;
}
}

Lottie.Maui Android handler

namespace Lottie.Maui;
public partial class LottieHandler : ViewHandler<ILottieView, AndroidX.AppCompat.Widget.AppCompatImageView>
{
LottieAnimationView platformView;
private static void MapResetAnimation(LottieHandler handler, ILottieView view, object args)
{
//handler.PlatformView.Progress = 0;
}
private static void MapIsPlaying(LottieHandler handler, ILottieView view)
{
/*
if (view.IsPlaying)
handler.PlatformView.PlayAnimation();
else
handler.PlatformView.PauseAnimation();
*/
}
private static void MapLoops(LottieHandler handler, ILottieView view)
{
// handler.PlatformView.Loop(view.Loops);
}
public static void MapAnimation(LottieHandler handler, ILottieView view)
{
var name = view?.Animation;
handler.UpdateAnimation(name);
}
void UpdateAnimation(string name)
{
platformView.SetAnimation(name);
}
protected override AndroidX.AppCompat.Widget.AppCompatImageView CreatePlatformView()
{
var callback = new LottieSdkCallbackImpl();
platformView = new LottieAnimationView(Context, callback);
return (AndroidX.AppCompat.Widget.AppCompatImageView)platformView.View;
}
}

And last but not least, you can add LottieView to your app, and you'll have the looping animation:

new LottieView {
Animation="logo-loading.json",
HeightRequest = 150,
Margin = new Thickness(0,48,0,0)
}
view raw LottieView hosted with ❤ by GitHub

app animation

If you made it here, you're awesome! I hope this post is going to be helpful.

I mentioned above, I implemented all this for my app, which is a Microsoft To Do client for Apple Watch and Wear OS devices. You can download from the AppStore and from Play Store

You can find all the code here: https://github.com/Csaba8472/LottieMaui

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs