Menu
Day icon Night icon Day icon Night icon

A great way to integrate Unity into the native iOS app

Most of the Unity apps we worked on needed to be integrated into the native environment (iOS, Android or even React Native). Our clients want their apps to have a native look and feel while using Unity rendering capabilities. So, we needed to devise a practical way to integrate Unity into the native app. We ended up with the pretty nice abstraction: wrap the output of the Unity build into the Cocoa framework (we’re talking about iOS for start).

We’re going to show how we do it step by step. 

Make the Space Shooter example work on mobile

We’re going to use the Space Shooter example from the Unity tutorial:

Unfortunately, the game is not made for mobile, so let’s make a few changes to correct this:

First up, let’s implement the touch controls – tap on the left half of the screen, the ship goes left and same for the right. In Done_PlayerController.cs insert the following code just under float moveVertical = Input.GetAxis (“Vertical”); in FixedUpdate:

if (Input.touchCount > 0) {
	Touch touch = Input.GetTouch(0);
	if (touch.position.x < Screen.width*0.5) {
		moveHorizontal = -0.5f;
	} else {
		moveHorizontal = 0.5f;
	}
}

We want to make our ship fire repeatedly. In the same file, Update method, remove the first condition from if, so the code should look like:

if (Time.time > nextFire) 

In Done_GameController.cs, let’s do this in the Update method to make the restart easier:

if (Input.GetKeyDown(KeyCode.R) || Input.touchCount > 2)

It’s super handy to be able to build the Unity project from command line, so let’s also create the editor script in Editor/Build.cs:

using UnityEngine;
using System.Collections;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;

public static class Build {

    public static void BuildPlayerIOS() {
        EditorSceneManager.OpenScene("Assets/_Complete-Game/Scenes/Done_Main.unity");
        string[] scenes = { "Assets/_Complete-Game/Scenes/Done_Main.unity" };
        BuildPipeline.BuildPlayer( scenes, "PlayerBuild", BuildTarget.iOS, BuildOptions.None); 
    }
}

Pack Unity generated code into the framework

Now we have the game working on iOS, so let’s proceed with creating framework. In Unity, switch to the iOS platform and press Build to generate the Xcode project.

Open the new Xcode window and create a new project. For the project template, choose “Cocoa Touch Framework”.

Create folder Unity in the framework project and copy Classes, Libraries and Data folders from the Unity generated Xcode project.

Add everything to the Xcode framework project. Keep in mind that Classes and Libraries should be added as groups and Data as the folder reference. Also, the folder “libil2cpp” in libraries should not be added to Xcode.

Let’s also remove main.mm from the project since this is a framework and we don’t actually need the entry point. We’ll add our own main-like file later in the process.

Now we need to configure the build settings. I usually copy the settings one by one from the Unity generated Xcode project and press Cmd+B until I get “Build successful”.

The first thing we need to do is add the prefix header. In build settings, set the Prefix Header to Unity/Classes/Prefix.pch

Here are the rest of the build settings we need to change:

Build settingValue
Header Search Paths$(PROJECT_DIR)/Unity/Classes
$(PROJECT_DIR)/Unity/Classes/Native
$(PROJECT_DIR)/Unity/Libraries/bdwgc/include
$(PROJECT_DIR)/Unity/Libraries/libil2cpp/include
Other C flags-DINIT_SCRIPTING_BACKEND=1
-fno-strict-overflow
-DRUNTIME_IL2CPP=1
User-Defined SettingsUNITY_RUNTIME_VERSION=2018.2.1f1
UNITY_SCRIPTING_BACKEND=il2cpp
Mismatched Return TypeYes
Other Linker Flags-weak_framework
CoreMotion
-weak-lSystem

Go back to Project Settings, General and add the following to Linked Framework And Libraries:

After completing these steps, you should be able to build the project without errors. So just try Cmd+B and if you get Build Successful, everything is ok. If not, try to check if you missed anything.

Making the Unity generated code work inside the framework

Once we got the Unity generated code to build successfully, it’s time to do necessary changes so it actually works. The first thing we need to do is fake the entry point (main.mm). Unity uses main.mm to fire up the engine, so we need to do the same with the UnityMain.mm. We’ll just copy all the code from main.mm, and:

  • Remove the UIApplicationMain call
  • Rename the main function to UnityInitialize
  • Export UnityInitialize so it can be called from outside

You can check the full code on GitHub

Next, we need to override UnityAppController to prevent Unity to take over the app UI and use the framework path instead of the main bundle when loading the resources. Override didFinishLaunchingWithOptions and do the following changes:

[...]

// we will replace this:
//     UnityInitApplicationNoGraphics([[[NSBundle mainBundle] bundlePath] UTF8String]);
// with this:
UnityInitApplicationNoGraphics([[[NSBundle bundleForClass:[self class]] bundlePath] UTF8String]);
    
[self selectRenderingAPI];
[UnityRenderingView InitializeForAPI: self.renderingAPI];

// we don't want Unity to create the window, so comment out or remove this line:
//_window         = [[UIWindow alloc] initWithFrame: [UIScreen mainScreen].bounds];

[...]    

In createUI:

[...]
// we don't need _window, so remove this assert
//NSAssert(_window != nil, @"_window should be inited at this point");

_rootController = [self createRootViewController];
[self willStartWithViewController: _rootController];
    
NSAssert(_rootView != nil, @"_rootView  should be inited at this point");
NSAssert(_rootController != nil, @"_rootController should be inited at this point");

// we don't want the window key and visible
//    [_window makeKeyAndVisible];

[UIView setAnimationsEnabled: NO];
    
// TODO: extract it?

// also remove this:
//   ShowSplashScreen(_window);

[...]

In showGameUI:

[...]

[_unityView recreateRenderingSurface];

// remove the following 3 lines since we don't want Unity to install its own UIViewController
//    [_window addSubview: _rootView];
//    _window.rootViewController = _rootController;
//    [_window bringSubviewToFront: _rootView];

#if UNITY_SUPPORT_ROTATION

[...]

Let’s expose a nice API to the outside world that will allow the person using the framework to control the engine. Also, it will hide all the Unity generated code. So, here’s how I do it:

//
//  SpaceAppController.m
//  SpaceShooter
//
//  Created by Luka Mijatovic on 06/03/2019.
//  Copyright © 2019 KodBiro. All rights reserved.
//

#import "SpaceAppController.h"
#import "SpaceUnityAppController.h"
#import "UnityMain.h"
#import "fishhook.h"
#import <mach-o/dyld.h>

int (*_NSGetExecutablePath_original)(char*, uint32_t*);

int _NSGetExecutablePath_patched(char* path, uint32_t* size) {
    
    NSString* imageName;
    unsigned long imageCount = _dyld_image_count();
    for(uint32_t i = 0; i < imageCount; i++) {
        const char *dyld = _dyld_get_image_name(i);
        imageName = [[NSString alloc] initWithUTF8String:dyld];
        if ([imageName hasSuffix:@"SpaceShooter.framework/SpaceShooter"]) {
            break;
        }
    }
    
    if (path == NULL) {
        *size = (uint32_t)[imageName length];
    } else {
        strcpy(path, [imageName UTF8String]);
    }
    
    return 0;
}


// App states
const NSInteger APP_STATE_INITIAL = 0;
const NSInteger APP_STATE_FINISHED_LAUNCHING = 1;
const NSInteger APP_STATE_BECOME_ACTIVE = 2;
const NSInteger APP_STATE_RESIGNED_ACTIVE = 3;

@interface SpaceAppController()
@property (nonatomic, strong) SpaceUnityAppController* unityAppController;
@property (nonatomic, assign) NSInteger appState;
@end

@implementation SpaceAppController

+ (instancetype)sharedController {
    static SpaceAppController* sharedController;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        rebind_symbols((struct rebinding[1]) {
            {"_NSGetExecutablePath", (void*)_NSGetExecutablePath_patched, (void **)&_NSGetExecutablePath_original}
        }, 1);

        sharedController = [[[self class] alloc] init];
    });
    return sharedController;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        _appState = APP_STATE_INITIAL;
        
        // Grab the arguments used to start the process (apparently Unity needs them)
        NSArray* arguments = [[NSProcessInfo processInfo] arguments];
        NSInteger count = [arguments count];
        char **array = (char **)malloc((count + 1) * sizeof(char*));
        
        for (NSInteger i = 0; i < count; i++) {
            array[i] = strdup([[arguments objectAtIndex:i] UTF8String]);
        }
        array[count] = NULL;
        char** argv = array;
        
        // call UnityMain which is actually the code from the Unity generated main.mm
        UnityInitialize(1, argv);
        
        // Intialize the UnityAppController instance
        _unityAppController = [[SpaceUnityAppController alloc] init];
    }
    return self;
}


- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
    if (self.appState != APP_STATE_INITIAL) {
        return YES;
    }
    self.appState = APP_STATE_FINISHED_LAUNCHING;
    return [self.unityAppController application:application didFinishLaunchingWithOptions:launchOptions];
}
- (void)applicationWillResignActive:(UIApplication*)application {
    if (self.appState != APP_STATE_BECOME_ACTIVE) {
        return;
    }
    self.appState = APP_STATE_RESIGNED_ACTIVE;
    [self.unityAppController applicationWillResignActive:application];
}
- (void)applicationDidBecomeActive:(UIApplication*)application {
    if (self.appState != APP_STATE_FINISHED_LAUNCHING && self.appState != APP_STATE_RESIGNED_ACTIVE) {
        return;
    }
    self.appState = APP_STATE_BECOME_ACTIVE;
    [self.unityAppController applicationDidBecomeActive:application];
}
- (void)applicationWillEnterForeground:(UIApplication*)application {
    [self.unityAppController applicationWillEnterForeground:application];
}
- (void)applicationWillTerminate:(UIApplication*)application {
    [self.unityAppController applicationWillTerminate:application];
}
- (UIView*)unityView {
    return (UIView*)self.unityAppController.unityView;
}


@end

It should be pretty straight-forward: we want to expose lifecycle management (app did launch, become active, etc.), unity view (so we can embed it in the UI), call UnityInitialize. I also added the appState variable so we can track the state of the API and prevent the lifecycle methods to be called twice.

Now the ugly stuff: the framework will crash in runtime if built with Unity 2018.2.1f1. This worked flawlessly in Unity 5.x and in Unity 2018.1.x.

This is the crash – Unity tries to find some section in the app executable, but that section is not there since the Unity code is now in the framework.

I did some snooping and it appears Unity does something with the dynamic linker (DYLD) in il2cpp::os::Image::Initialize. It first tries to find the path to executable containing the Unity code (in method il2cpp::os::Image::GetImageIndex). However, the result of this method is always the app executable. All of Unity now lives in the framework, so it will never be able to find what it’s looking for in the main app executable. And it will crash.

Unity uses _NSGetExecutablePath to find the path to the executable. So if we could only get this function to return the path to the framework.

It appears there is a way – I used the fishhook library from Facebook to patch _NSGetExecutablePath function to always return the path to framework binary. This is super dangerous, but here at KB, we yearn for danger 🙂

Symbolic breakpoint in il2cpp::os::Image::GetImageIndex. Unity uses _NSGetExecutablePath to find the executable. We trick it by replacing _NSGetExecutablePath with our function which will always return the path to the framework.

How to use the framework in the app

So the framework should be done now. Let’s create the example app which will use the framework. Add the framework project to the app project (close the Xcode framework project before doing this). Add the SpaceShooter.framework to the Embedded Binaries. And finally, add the path to the SpaceAppController.h to the Header Search Paths.

Here’s how we use the framework:

//
//  ViewController.m
//  SpaceApp
//
//  Created by Luka Mijatovic on 06/03/2019.
//  Copyright © 2019 KodBiro. All rights reserved.
//

#import "ViewController.h"
#import "SpaceAppController.h"

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView* unityContainerView;
@property (nonatomic, strong) UIView* unityView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [[SpaceAppController sharedController] application:[UIApplication sharedApplication] didFinishLaunchingWithOptions:[NSDictionary dictionary]];
    [[SpaceAppController sharedController] applicationDidBecomeActive:[UIApplication sharedApplication]];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive) name:UIApplicationWillResignActiveNotification object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil];
    

    self.unityView = [SpaceAppController sharedController].unityView;
    [self.unityContainerView addSubview:self.unityView];
}

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    self.unityView.frame = self.unityContainerView.bounds;
}


- (void)applicationWillResignActive {
    [[SpaceAppController sharedController] applicationWillResignActive:[UIApplication sharedApplication]];
}

- (void)applicationDidBecomeActive {
    [[SpaceAppController sharedController] applicationDidBecomeActive:[UIApplication sharedApplication]];
}

- (void)applicationWillEnterForeground {
    [[SpaceAppController sharedController] applicationWillEnterForeground:[UIApplication sharedApplication]];
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [[SpaceAppController sharedController] applicationWillResignActive:[UIApplication sharedApplication]];
}

@end

Here’s how it looks like in the app:

And the same framework can be used in iMessage app:

The process

What to do when we change something in Unity code? I’ve added a simple build script which does the Unity build and copies all the files to the framework project. Unity will sometimes generate more (or fewer) cpp files, usually in the Classes/Native. In that case, you need to manually add/remove those files from Xcode framework Unity project. I haven’t found a good way to automate this yet.

I’ve put everything on Github so you can check how it works: https://github.com/Luka-M/unity-in-framework


Comments (8)

  1. nitinbiltoria says:

    getting error with xcode 10.2.1

  2. ken says:

    very nice!!
    I embed the unity to native iOS app,but when I use Application.Quit() to close the unity ,my app is also closed,uinty use exit(0)to close itself.
    How can I to to close unity and keep my app still running?

  3. Dheeraj says:

    error: Build input file cannot be found: ‘/Users/devappboxeripad/Desktop/IntegrateUnityAndIOS/Unity/Classes/Prefix.pch’

    path is right ..but still i am getting error

  4. minsoo says:

    Maybe you added classes, libraries folder with an option reference copying, you need to add two folders with an option create if needed

  5. La Win Ko says:

    Is it possible to release to the UnityAppController instance? I want to completely relaunch the game not to resume

  6. Orlando Nandito Nehemia says:

    hi, this tutorial is awesome.. but did you have any experience to import file from document files app? so i read tutorial on internet like this : https://www.appcoda.com/files-app-integration/

    but in my case, my game i enable filesharing. so in document theres my game document. so in the document there is .txt file that i would like to import in my game. now i can see my game folder. but how can i access that document files? so, for example i have one button to oppen document, and then select the .txt file to import it to my game. how can i make like that? i really confused to do that. because its just a small documentatin from unity about that. thankyou.

  7. Thomas says:

    Hello,

    I’ve got an error after setting up the frameworks:

    clang: error: no such file or directory: ‘CoreMotion’
    clang: error: no such file or directory: ‘MediaToolbox’

    Did someone get this error ?

  8. Vikas Roy says:

    “Also, the folder “libil2cpp” in libraries should not be added to Xcode.”

    But in header search path you have mentioned the libil2cpp folder.

    How will this work?

    I am getting “‘il2cpp-config.h’ file not found” error after deleting libil2cpp folder.

Leave a Comment

*