How to use SwiftUI components in MAUI

Artem Denisov
10 min readDec 4, 2022

--

The standard MAUI component library includes an extensive list of essential elements for creating a user interface. You can use them to develop feature-rich cross-platform applications that look great on iOS and Android devices. But no matter how diverse the standard library is, it is not exhaustive, and sometimes there is a need to create a unique interface element.

In MAUI, you should follow the steps below to create a custom control:

  1. Create the component by the target platform tools (for example, using UIKit or SwiftUI for iOS).
  2. Create the Binding library, which will bind the platform implementation and the .NET code.
  3. Create a .NET control, which will be a projection of the platform component for MAUI applications.

In this article, I will show how to go through all these steps to use modern SwiftUI components as a platform implementation for MAUI controls.

Create the SwiftUI component library

In Xcode, create a new Framework project:

Choose Framework template

Next, let’s configure the framework for further work:

  1. Go to the project file, and in the General tab in the Supported Destinations section, keep only the iPhone.
  2. In the Minimum Deployments section, set the minimum iOS version to 15.5.
  3. Go to the Build Settings tab and type “Build libraries” in the search box. Set Build Libraries for Distribution to Yes. Also, set the Skip Install parameter to No.
SwiftUI_MAUI_Framework configuration

Now delete the default SwiftUI_MAUI_Framework.h header file. We won’t need it. So, the setup is complete, and we can finally add the SwiftUI component we will work with. To do this, add a new file and select the SwiftUI View template.

Choose SwiftUI template

Xcode creates a SwiftUI component template. Since this will be our first component, of course, I named it HelloWorld. Let’s change it a little bit to give the component some personality. I added an orange background under the Label.

HelloWorld SwiftUI component

As you can see in the preview, our changes are applied, and everything works fine.

Unfortunately, at the moment, we can’t bind SwiftUI views directly to the .NET code: MAUI can work only with UIKit classes, so we have to make a UIKit wrapper over the SwiftUI element.

Choose the Swift File template

Create a wrapper class. SwiftUI already has the UIHostingController class, which allows you to display a View on SwiftUI as a UIKit UIView.

@objc public class HelloWorldWrapper: NSObject, ObservableObject {
var swiftUIView: HelloWorldView?
var hostingController: UIHostingController<HelloWorldView>?

@objc public var uiView: UIView? { hostingController?.view }

public override init() {
super.init()

swiftUIView = HelloWorldView()
guard let rootView = swiftUIView else { return }
hostingController = UIHostingController(rootView: rootView)
}

The HelloWorldWrapper class is going to be the bridge between the SwiftUI View and the .NET part of the code. The wrapper class must be a descendant of NSObject in order for .NET to be able to work with it (the ObservableOblect protocol in this example is optional). Since the Swift and C# code are linked through Objective-C, you must mark the elements you want accessible from C# code with the @objc attribute (this can be applied to classes, protocols, methods, or fields).

So, the SwiftUI and UIKit classes are written. Let’s build the project to make sure everything is done without errors. After successfully compiling the project, you need to generate the .xcframework file that the Binding library will interact with.

To create the xcframework, start Terminal, go to the directory where the project file SwiftUI_MAUI_Framework.xcodeproj is, and then run the following commands:

xcodebuild archive \
-scheme SwiftUI_MAUI_Framework \
-archivePath ../XCFrameworks/SwiftMaui-ios.xcarchive \
-sdk iphoneos \
SKIP_INSTALL=NO
xcodebuild archive \
-scheme SwiftUI_MAUI_Framework \
-archivePath ../XCFrameworks/SwiftMaui-sim.xcarchive \
-sdk iphonesimulator \
SKIP_INSTALL=NO
xcodebuild -create-xcframework \
-framework ../XCFrameworks/SwiftMaui-sim.xcarchive/Products/Library/Frameworks/SwiftUI_MAUI_Framework.framework \
-framework ../XCFrameworks/SwiftMaui-ios.xcarchive/Products/Library/Frameworks/SwiftUI_MAUI_Framework.framework \
-output ../XCFrameworks/SwiftUI_MAUI_Framework.xcframework

Once these commands are successfully executed, the XCFrameworks folder will be created, containing two archive files (SwiftMaui-sim.xcarchive for the simulator and SwiftMaui-ios.xcarchive for real devices) as well as the necessary SwiftUI_MAUI_Framework.xcframework directory. This is what you are going to use for the next step.

The xcframework is created

Now the Swift framework is ready, and you can move on to the second stage.

Creation of the Binding Library

Visual Studio has a ready-made template for creating a Binding library. Let’s use it.

Choose the iOS Binding Library template

Set up the created library project. The project contains two files ApiDefinition.cs and StructsAndEnums.cs. Rename the file ApiDefinition.cs to ApiDefinitions.cs. It will be helpful in further automatic generation of this file.

Make sure that after rename, the BuildAction option is set as follows:

  • For file ApiDefinitions.cs — ObjcBindingApiDefinition
  • For file StructsAndEnums.cs — ObjcBindingCoreSource

And the project file .csproj looks as shown in the screenshot.

.csproj file after initial setup

Now add the SwiftUI_MAUI_Framework.xcframework that we created in the first part to the Native References section. Then go to the parameters of the connected framework and set the following options:

  • Frameworks: Foundation UIKit
  • Kind: Framework
  • Smart Link: True
Native library (SwiftUI_MAUI_Framework) properties

The Bindings library needs descriptions of the Swift classes we are going to interact with. Use the Objective Sharpie utility for this (you can download it here). When the utility is installed, you have to go to the XCFrameworks directory in the Terminal and execute the command:

sharpie bind \
-sdk iphoneos \
-output ./ \
-namespace SwiftUI_MAUI_Framework \
-framework ./SwiftUI_MAUI_Framework.xcframework/ios-arm64/SwiftUI_MAUI_Framework.framework

Sharpie analyzes the .xcframework and creates an ApiDefinitions.cs file in the XCFrameworks directory with descriptions of public classes, protocols, methods, and fields that we have marked with the @objc attribute. Replace the ApiDefinitions.cs file in the Bindings library directory with this generated file. Now the content of the file looks like this:

Content of the ApiDefinitions.cs file

Let’s assemble the project and ensure we haven’t made any mistakes. The Bindings library is ready, so let’s move on to the third part.

Creation of a .NET component

In Visual Studio, there is a template for creating a library of MAUI components. Let’s use it.

Choose the .NET MAUI Class Library template

I’m only considering the iOS platform in this article, so after creation, I edited the project file .csproj and left only iOS and Android platforms as targets.

Also, it is necessary to add a reference to the Bindings library that was created in the second part. As a result, my project file looks like this:

MAUI_Library .csproj file after setup

To associate a platform component with an MAUI control, there is a ViewHandler class in MAUI that is used to bind platform code (Swift, in our case) and C# code.

I have created a new class, a descendant of ViewHandler. This class is declared as a partial class in more than one file: the platform-independent part, and file for each platform, to implement platform-specific relationships and platform-specific logic (e.g., iOS and Android).

I have created three files for my handler class:

  • iOSHelloWorldHandler.cs — for iOS
  • AndroidHelloWorldHandler.cs — for Android
  • CommonHelloWorldHandler.cs — for platform-independent part

And the file for the MAUI class HelloWorldControl.cs itself, also in the platform-independent part.

Let’s take a detailed look at the contents of these files.

HelloWorldControl.cs

namespace MAUI_Library;

public class HelloWorldControl : View { }

In my example, the HelloWorldControl class is a simple descendant of Microsoft.Maui.Controls.View. It is used to display the platform SwiftUI View only, so it does not contain any additional code.

CommonHelloWorldHandler.cs

using Microsoft.Maui.Handlers;

namespace MAUI_Library;

public partial class HelloWorldHandler { }

Also, since there is no platform-independent code in this example, the general HelloWorldHandler part does not contain any code, but it is architecturally important that it exists.

AndroidHelloWorldHandler.cs

using Android.Widget;
using Microsoft.Maui.Handlers;
using View = Android.Views.View;

namespace MAUI_Library;

//Implement handler for Android platform here
public partial class HelloWorldHandler: ViewHandler<HelloWorldControl, Android.Views.View> {
public HelloWorldHandler() : base(PropertyMapper) { }

public static IPropertyMapper<HelloWorldControl, HelloWorldHandler> PropertyMapper =
new PropertyMapper<HelloWorldControl, HelloWorldHandler>(ViewHandler.ViewMapper) { };

protected override Android.Views.View CreatePlatformView() {
//it is just a mock view
//create something real for a correct implementation
return new View(null);
}
}

The Android part of the HelloWorldHandler implementation contains minimal workable code to implement inheritance from ViewHandler. But since the Android platform is not the subject of this article, I will not describe it in detail.

iOSHelloWorldHandler.cs

using Microsoft.Maui.Handlers;
using SwiftUI_MAUI_Framework;
using UIKit;

namespace MAUI_Library;

public partial class HelloWorldHandler : ViewHandler<HelloWorldControl, UIView> {
public HelloWorldHandler() : base(PropertyMapper) { }

public static IPropertyMapper<HelloWorldControl, HelloWorldHandler> PropertyMapper =
new PropertyMapper<HelloWorldControl, HelloWorldHandler>(ViewHandler.ViewMapper) { };

protected override UIView CreatePlatformView() {
var wrapper = new HelloWorldWrapper();
return wrapper.UiView;
}
}

The iOS implementation of HelloWorldHandler is also inherited from the generic type ViewHandler<TVirtualView, TPlatformView>, where TVirtualView is the MAUI class that will be the representation for the platform implementation, and TPlatformView is the type of platform UIKit object to bind to the MAUI. In this example:

  • TVirtualView is the HelloWorldControl described above
  • TPlatformView is UIView because the UIHostingController from the first part of the article wraps SwiftUI View into UIView

An inheritance from ViewHandler requires the implementation of three components:

  • Constructor
  • PropertyMapper property, which maps the properties of the platform component and the MAUI control
  • The CreatePlatformView() method, which instantiates the platform component class

In the current example, the platform object has no properties or methods, so the PropertyMapper is empty, as well as the constructor does not execute any specific code. In this case, only the CreatePlatformView method does all the work. As you can see, it creates an instance of the Swift class HelloWorldWrapper, which is a wrapper for the SwiftUI HelloWorldView. And because we can only operate with UIKit classes, we get an instance of UIView from the HelloWorldWrapper, which represents HelloWorldView.

At this point, the entire code of the MAUI library is written, let’s build the library, and it becomes usable in the final applications. How to do this is explained below.

How to use the MAUI library in an MAUI application

As for creating the MAUI library, let’s use the Visual Studio template.

Choose the .NET MAUI App template

Just like when I created the library, I edited the .csproj file and removed the unnecessary platform in order to speed up the building of the application and reduce possible conflicts.

Kept only necessary target platforms in the .csproj file

In order to use a component from the MAUI library, you need to connect both the library itself and the Bindings library. As a result, the project file contains, in my case, the following lines:

The project file with the required references added

As soon as the references to the libraries are added, it is first necessary to assign the class handler that I’m going to use to the application. To do this, add the HelloWorldHandler in the MauiProgram.cs file.

using MAUI_Library;
using Microsoft.Extensions.Logging;

namespace MAUI_App;

public static class MauiProgram {
public static MauiApp CreateMauiApp() {
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
//Add setup handler here
.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler(typeof(HelloWorldControl), typeof(HelloWorldHandler));
});

#if DEBUG
builder.Logging.AddDebug();
#endif

return builder.Build();
}
}

Now the MAUI application knows about the HelloWorldControl class and that you need to use HelloWorldHandler to manipulate it.

Let’s add a link to the MAUI_Library and HelloWorldControl itself to the MainPage.xaml markup file to see it in the application.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:mauiLibrary="clr-namespace:MAUI_Library;assembly=MAUI_Library"
x:Class="MAUI_App.MainPage">

<ScrollView>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center">

<!-- Autogenerated code here -->
<...>

<mauiLibrary:HelloWorldControl/>

</VerticalStackLayout>
</ScrollView>

</ContentPage>
The final markup of the application

Let’s build and run an application to see the SwiftUI HelloWorldView in a run-time MAUI application.

It works!

As you can see, the SwiftUI component I created is displayed on the application’s main screen.

Obviously, this article only covered the concept of using SwiftUI components as a platform implementation of MAUI controls. In the following articles, I am going to show how to create more functional components and how to bind properties and methods from Swift code to C#.

The complete code from this article is on my GitHub.

Articles in this series:

Thanks for reading, see you in future publications. Bye!

Environment specifications:

  • Xcode 14.1 (14B47b)
  • Command Line Tools 14.1 (14B47b)
  • Visual Studio 2022 for Mac Preview 17.5 Preview (17.5 build 437)
  • .NET 7.0.100
  • .NET MAUI workloads iOS/Android 7.0.49/7.0.100
  • macOS Ventura 13.0.1 (22A400)

--

--