DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • How to Make a Picture-in-Picture Feature in iOS App Using AVFoundation
  • HLS Streaming With AVKit and Swift UI for iOS and tvOS
  • Vision AI on Apple Silicon: A Practical Guide to MLX-VLM
  • Feature Flag Framework in Salesforce Using LaunchDarkly

Trending

  • The Cypress Edge: Next-Level Testing Strategies for React Developers
  • How to Convert XLS to XLSX in Java
  • Why Documentation Matters More Than You Think
  • Optimize Deployment Pipelines for Speed, Security and Seamless Automation
  1. DZone
  2. Coding
  3. Frameworks
  4. Setting up iOS Framework for Unity

Setting up iOS Framework for Unity

Running iOS framework from Unity using interoperability between Swift and C. Expanding the usage of Swift in Unity and exploring its limitations.

By 
Max Kalik user avatar
Max Kalik
·
Apr. 06, 23 · Tutorial
Likes (11)
Comment
Save
Tweet
Share
3.8K Views

Join the DZone community and get the full member experience.

Join For Free

Part 1: Launch UIViewController from Unity

I won’t waste your time on a long introduction about the technologies that I will describe here. You most likely already work as an iOS Engineer or Game Developer, which means you probably have some questions regarding the topic of this article. So, let’s get started.

This article is split into two parts. In the first part, you will learn how to launch a simple UIViewController from Unity. We will force C# to understand Swift. In the second part, we will try to expand the usage of Swift in Unity and explore its limitations.

All right, here we go.

Prerequisites

  • macOS Monterey
  • Xcode 13 (or 14)
  • Unity LTS 2021.3
  • iOS Device (with at least iOS14 or higher)

iOS Framework for Unity

iOS Framework

Open Xcode and create a new Swift project — framework.

Swift project — framework

Swift project — framework

The name of the project is up to you. I named it SwiftCodeKit. It’s going to be a simple framework with one view controller. We need only three files:

SwiftCodeKit

In SwiftCodeKitViewController, let’s create a simple UIViewController — with a button at the center of the view.

Swift
 
final class SwiftCodeKitViewController: UIViewController {

    private lazy var button: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("Press me", for: .normal)
        button.backgroundColor = .white
        button.layer.cornerRadius = 20
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(onTap), for: .touchUpInside)
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .orange
        
        view.addSubview(button)
        
        NSLayoutConstraint.activate([
            button.widthAnchor.constraint(equalToConstant: 150),
            button.heightAnchor.constraint(equalToConstant: 50),
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
    
    @objc
    func onTap() {
        dismiss(animated: true)
    }
}


In SwiftCodeKit.swift we will collect all our public methods. Having this public class will allow you to test your framework in iOS projects. The first one will bestart(). This method presents our SwiftCodeKitViewController.

Swift
 
public class SwiftCodeKit {
    public static func start() {
        DispatchQueue.main.async {
            if let rootViewController = UIApplication.shared.windows.first?.rootViewController {
                let viewController = SwiftCodeKitViewController()
                rootViewController.present(viewController, animated: true)
            }
        }
    }
}


The third file SwiftCodeKitBridging.swift will contain a list of public exposed methods with C declaration notation @_cdecl. These methods should be stored only in a non-local scope. First, let’s create a method: startSwiftCodeKitController().

Swift
 
@_cdecl("startSwiftCodeKitController")
public func startSwiftCodeKitController() {
    SwiftCodeKit.start()
}


The attribute @_cdecl is not documented. For now, you only have to know that this attribute exposes a Swift function to C. Further, we will experiment a little bit with this attribute and will figure out the limitation.

Building Framework

In your Xcode project status of the toolbar, choose Any iOS Device (arm64) destination. Yes, we are going to build only for iOS devices, not for Simulators. Now, you need only tap Command + B.

Any iOS Device (arm64)

Any iOS device (arm64) destination

To get our baked framework, we need to go to Derived data and take it from there. You should open the Derived data folder from Xcode: Go to Xcode Preferences (cmd + ,), then open Locations and tap on the arrow at the end of the Derived data path.

In the Derived data folder, find your project and use this path:
Build / Products / Debug-iphoneos / SwiftCodeKit.framework

Derived data with SwiftCodeKit project

Derived data with SwiftCodeKit project

Unity Project

It’s time to set up our Unity project. Open Unity and create a project; I called it UnityClientProject.

UnityClientProject

Creating a Unity Project

In the Unity project, you will find folder assets. Drag your SwiftCodeKit.framework and drop it into the Assetsfolder.

SwiftCodeKit.framework in Assets / iOS folder

SwiftCodeKit.framework in Assets/iOS folder 

Regarding UI in Unity, we are going to make a simple button. This button will summon our SwiftCodeKit View Controller. Right-click on Hierarchy, choose UI, and then Button.

Button in Unity project

Button in Unity project

Also, in the hierarchy, we have to make another object for our button. I call it ButtonController. In the inspector the ButtonController, add a component/new script and name it ButtonControllerScript.cs. Open this file.

Actually, we need only one public method: OnPressButton(). But before that, we have to import System.Runtime.InteropServices. This will help us to recognize our methods from the SwiftCodeKit.framework. If you remember, we have only one public function: startSwiftCodeKitController(), so let’s import it using DllImport.

C#
 
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;

public class ButtonControllerScript : MonoBehaviour
{
    [DllImport("__Internal")]
    private static extern void startSwiftCodeKitController();

    public void OnPressButton()
    {
        startSwiftCodeKitController();
    }
}


(ButtonController.cs handles method from iOS using Runtime.InteropServices)

Building Unity Project

We are ready to build our iOS project from Unity. Open File/Build Settings and change the Platform to iOS. Then press Build.

Unity build settings

After pressing build, it will automatically run an Xcode project. This project is generated by Unity, and there you will see only one Framework called UnityFramework.

A couple of problems you can stumble upon at this moment: the first one is you have to sign this project, and the second one is the bitcode error — this can only happen if you use Xcode 14. It’s not such a big deal. Let’s just disable bitcode in this project and run it again.

Xcode Build settings

Xcode Build settings

On your iPhone, you will see your Unity app. The first blue screen with a button — this is the Unity part and the orange presented screen is your framework!

Unity Button presents a view controller implemented in Swift

Unity Button presents a View Controller implemented in Swift

Wrapping up of Part 1

Let’s quickly go through what we did:

  1. Prepared a Framework with a public method for iOS and for C# using @_cdecl attribute.
  2. Created a simple Unity project with a button at the center.
  3. Added our Framework to the assets of the Unity project.
  4. Prepared the ButtonController object with C# script where we imported our method using System.Runtime.InteropServices
  5. Built an iOS project from Unity.

As you can see, these are basic steps that give you an opportunity to understand how it works. In part two (see below), we will try to make this project closer to real usage. We will experiment with @_cdecl attribute and explore the limitations.


Part 2: Exposing Swift Functions to C# in Unity

This article is written in two parts. In the second part, we will experiment with @_cdeclto understand the limitations of interoperability of Swift and C#. Let’s get started.

If you read the first part of this article, you should remember the SwiftCodeKitBridging.swift file:

Swift
 
@_cdecl("startSwiftCodeKitController")
public func startSwiftCodeKitController() {
    SwiftCodeKit.start()
}


In this file, we store a list of public methods for exposing, using @_cdecl attribute. As you can see, we already know how to call some functions from C#. But obviously, it’s not so usable because in real iOS applications, we use functions with parameters, and our functions return some values. Moreover, we use closures, delegates, and so on. How do expose this code to the Unity project?

Let’s resume what we are gonna try today:

  • Function with parameters
  • Function returns value
  • Function with closure

iOS Framework Update

I prepared a small update of our SwiftCodeKitViewController.

Swift
 
final class SwiftCodeKitViewController: UIViewController {

    private lazy var label: UILabel = {
        let label = UILabel()
        label.textColor = .black
        label.font = .systemFont(ofSize: 128, weight: .bold)
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    private lazy var stepper: UIStepper = {
        let stepper = UIStepper()
        stepper.value = 5
        stepper.translatesAutoresizingMaskIntoConstraints = false
        stepper.addTarget(self, action: #selector(stepperChanged), for: .valueChanged)
        return stepper
    }()
    
    private lazy var button: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("Done", for: .normal)
        button.backgroundColor = .black.withAlphaComponent(0.1)
        button.setTitleColor(UIColor.black, for: .normal)
        button.layer.cornerRadius = 8
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(onTap), for: .touchUpInside)
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        setupViews()
    }
    
    func setupViews() {
        view.backgroundColor = .white
        label.text = String(stepper.value)
        
        view.addSubview(button)
        view.addSubview(stepper)
        view.addSubview(label)

        NSLayoutConstraint.activate([
            button.widthAnchor.constraint(equalToConstant: 92),
            button.heightAnchor.constraint(equalToConstant: 50),
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40),
            
            stepper.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            stepper.bottomAnchor.constraint(equalTo: button.topAnchor, constant: -20),
            
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
    
    @objc
    func onTap() {
        dismiss(animated: true)
    }
    
    @objc
    func stepperChanged(_ sender: UIStepper) {
        label.text = String(sender.value)
    }
}


Let's add two more views: UILabel and UIStepper. Using stepper control, we can increment and decrement a value and set it to the label.

SiwftCodeKit UI

It will be enough to experiment with real UI. Let’s take a look at our public class:

Swift
 
public class SwiftCodeKit {
    
    private static let viewController = SwiftCodeKitViewController()
    
    public static func start() {
        DispatchQueue.main.async {
            if let rootViewController = UIApplication.shared.windows.first?.rootViewController {
                rootViewController.present(viewController, animated: true)
            }
        }
    }
    
    public static func configure(min: Double, max: Double) {
        viewController.configureStepper(min: min, max: max)
    }
    
    public static func configure(value: Double) {
        viewController.configureStepper(value: value)
    }
    
    public static func getValue() -> Double {
        viewController.stepperValue
    }
    
    public static func getVersion() -> String {
        return "Swift Code Kit 0.0.1"
    }
}

// MARK: Delegates
public extension SwiftCodeKit {
    static var swiftCodeKitDidStart: (() -> Void)?
    static var swiftCodeKitDidFinish: (() -> Void)?
    static var swiftCodeKitStepperValueDidChange: ((Double) -> Void)?
}


As you can see, this class was extended with methods that we are going to use in C#. The most interesting part is the Delegates — they are represented by closures. Let’s connect all these methods to our UI. In viewDidLoad, add our first delegate method SwiftCodeKit.swiftCodeKitDidStart?().

Here are other methods that can be used in extensions:

Swift
 
extension SwiftCodeKitViewController {
    @objc
    func onTap() {
        dismiss(animated: true)
        SwiftCodeKit.swiftCodeKitDidFinish?()
    }
    
    @objc
    func stepperChanged(_ sender: UIStepper) {
        label.text = String(sender.value)
        SwiftCodeKit.swiftCodeKitStepperValueDidChange?(sender.value)
    }
}

extension SwiftCodeKitViewController {
    func configureStepper(min: Double, max: Double) {
        stepper.minimumValue = min
        stepper.maximumValue = max
    }
    
    func configureStepper(value: Double) {
        stepper.value = value
    }
    
    var stepperValue: Double {
        stepper.value
    }
}


iOS Framework Bridging Methods

All our public methods should be recognized from C#. I suggest walking through them one by one.

Here’s the function with parameters:

Swift
 
@_cdecl("swiftCodeKitConfigureMinMax")
public func swiftCodeKitConfigureMinMax(min: Double, max: Double) {
    SwiftCodeKit.configure(min: min, max: max)
}

@_cdecl("swiftCodeKitConfigureValue")
public func swiftCodeKitConfigureValue(value: Double) {
    SwiftCodeKit.configure(value: value)
}


The function returns a value (Double):

Swift
 
@_cdecl("swiftCodeKitGetValue")
public func swiftCodeKitGetValue() -> Double {
    SwiftCodeKit.getValue()
}


The function returns a string. Here you can notice a difference. Specifically for this kind of data, we need to allocate memory and pass our string to strdup() — this method will duplicate the string for UnsafePointer().

Swift
 
@_cdecl("swiftCodeKitGetVersion")
public func swiftCodeKitGetVersion() -> UnsafePointer<CChar>? {
    let string = strdup(SwiftCodeKit.getVersion())
    return UnsafePointer(string)
}


Ok, now about Delegates: As you can see, they are functions with escaping closures. But besides escaping closure, it has an interesting add-on called @convention(c).

There is a short description from docs.swift.org:

"Apply this attribute to the type of a function to indicate its calling conventions. The c argument indicates a C function reference. The function value carries no context and uses the C calling convention."

Swift
 
@_cdecl("setSwiftCodeKitDidStart")
public func setSwiftCodeKitDidStart(delegate: @convention(c) @escaping () -> Void) {
    SwiftCodeKit.swiftCodeKitDidStart = delegate
}

@_cdecl("setSwiftCodeKitDidFinish")
public func setSwiftCodeKitDidFinish(delegate: @convention(c) @escaping () -> Void) {
    SwiftCodeKit.swiftCodeKitDidFinish = delegate
}

@_cdecl("setSwiftCodeKitStepperValueDidChange")
public func setSwiftCodeKitStepperValueDidChange(delegate: @convention(c) @escaping (Double) -> Void) {
    SwiftCodeKit.swiftCodeKitStepperValueDidChange = delegate
}


All methods for exposing are ready, so this means we are ready to build our updated framework, get this artifact, and drop it into the Unity project assets folder (see part 1 for how to do this).

Unity ButtonControllerScript update

Our C# script file has only one imported function for now: startSwiftCodeKitController, so it’s time to add the rest of them. We will walk through the same way as we did with Swift methods:

Here’s the function with parameters:

C#
 
[DllImport("__Internal")]
private static extern void swiftCodeKitConfigureMinMax(double min, double max);

[DllImport("__Internal")]
private static extern void swiftCodeKitConfigureValue(double value);


This function returns a value, as shown below:

Swift
 
[DllImport("__Internal")]
private static extern void swiftCodeKitConfigureMinMax(double min, double max);

[DllImport("__Internal")]
private static extern void swiftCodeKitConfigureValue(double value);


And finally Delegates using AOT:

C#
 
using AOT;

public class ButtonControllerScript : MonoBehaviour
{
    // Delegates

    public delegate void SwiftCodeKitDidStartDelegate();
    [DllImport("__Internal")]
    private static extern void setSwiftCodeKitDidStart(SwiftCodeKitDidStartDelegate callBack);

    public delegate void SwiftCodeKitDidFinishDelegate();
    [DllImport("__Internal")]
    private static extern void setSwiftCodeKitDidFinish(SwiftCodeKitDidFinishDelegate callBack);

    public delegate void SwiftCodeKitStepperValueDidChange(double value);
    [DllImport("__Internal")]
    private static extern void setSwiftCodeKitStepperValueDidChange(SwiftCodeKitStepperValueDidChange callBack);

    // Handle delegates

    [MonoPInvokeCallback(typeof(SwiftCodeKitDidStartDelegate))]
    public static void swiftCodeKitDidStart()
    {
        Debug.Log("SwiftCodeKit did start");
    }

    [MonoPInvokeCallback(typeof(SwiftCodeKitDidFinishDelegate))]
    public static void swiftCodeKitDidFinish()
    {
        Debug.Log("SwiftCodeKit did finish");
    }

    [MonoPInvokeCallback(typeof(SwiftCodeKitStepperValueDidChange))]
    public static void setSwiftCodeKitStepperValueDidChange(double value)
    {
        Debug.Log("SwiftCodeKit value did change. Value: " + value);
    }
}


The last thing we need to do is to update OnPressButton() method where we use all methods from our Swift framework.

C#
 
// On Press Button

public void OnPressButton()
{
    startSwiftCodeKitController();

    swiftCodeKitConfigureMinMax(0, 20);
    swiftCodeKitConfigureValue(10);

    Debug.Log("SwiftCodeKit get value: " + swiftCodeKitGetValue());
    Debug.Log("SwiftCodeKit get version: " + swiftCodeKitGetVersion());

    setSwiftCodeKitDidStart(swiftCodeKitDidStart);
    setSwiftCodeKitDidFinish(swiftCodeKitDidFinish);
    setSwiftCodeKitStepperValueDidChange(setSwiftCodeKitStepperValueDidChange);
}


Directly from Unity, we configured our iOS Controller and used our three delegates. Now, it’s time to build our project. If you followed me correctly and all your methods were recognizable by Unity, your result will look like the following GIF animation.

The console of Xcode should print something like this:

Plain Text
 
SwiftCodeKit did start
SwiftCodeKit get value: 10
SwiftCodeKit get version: Swift Code Kit 0.0.1
SwiftCodeKit value did change. Value: 11
SwiftCodeKit value did change. Value: 12
SwiftCodeKit value did change. Value: 13
SwiftCodeKit did finish


Wrapping up of Part 2

I always ask the same question every time when finalizing a task: How can we make this better? I think this task could be improved endlessly. I’m not an expert with Unity (if you know better, please comment), but I know that we can even build DLL Library with all our functions and eventually just import this build directly to the script.

Also, as an iOS Developer using UnsafePointer keep in mind that this memory should be released.

Thanks for reading.

Source Code

GitHub: https://github.com/maxkalik/swift-in-unity

Framework Swift (programming language) unity Apple iOS

Published at DZone with permission of Max Kalik. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • How to Make a Picture-in-Picture Feature in iOS App Using AVFoundation
  • HLS Streaming With AVKit and Swift UI for iOS and tvOS
  • Vision AI on Apple Silicon: A Practical Guide to MLX-VLM
  • Feature Flag Framework in Salesforce Using LaunchDarkly

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!