Override app delegate in Unity for iOS and macOS (1/4)

작성자

카테고리:

http://blog.eppz.eu/override-app-delegate-unity-ios-macos-1/

  • workflow
  • advanced
  • code design
  • github
  • ios
  • macos
  • understanding
  • unity
  • unity3d
  • workflow
  • march 2, 2017

When it comes to create native plugins for Unity, often the plugins need customize behaviour how the application starts / launches by override app delegate (see UIApplicationDelegate for iOS and NSApplicationDelegate for macOS). Opening app via Push Notifications, URL Schemes, Documents, User activities, or even via WatchKit events are all great examples of this.

TL;DR

You may get the example project immediately by heading to GitHub (in that case you should really be aware of the folder structure at the end of the article, as there are numerous sub-projects can be found all around). However, I strongly recommend to take the next 15 minutes to understand what is going on, and read along. See Unity.Blog.Override_App_Delegate at GitHub

Override app delegate in iOS Unity player (Poor man’s method)

Like many things in the Unity ecosystem, this feature is also heavily underdocumented. However, if you take a closer look to a Unity iOS player Xcode project, you can find that Unity provided some tools that can help.

There is an external constant called AppControllerClassName can be found in every main.mm of every iOS Unity player Xcode project. This class name will be passed as the app delegate class to UIApplicationMain. That means that UIApplication (or any principal class given in Info.plist) will create an instance of that delegate (that should conform to UIApplicationDelegate), and call the given template methods throughout the application lifecycle.

There you can find the only piece of documentation about this thing in that single comment line.

Unity also provided a macro called IMPL_APP_CONTROLLER_SUBCLASS(className) that creates a tiny little category, which overrides the AppControllerClassName constant as soon as it gets loaded (along all the other classes).

In short, if you put this file OverrideAppDelegate.m into your Plugins folder, you can essentially extend / override app delegate this way.#import “UnityAppController.h”@interface OverrideAppDelegate : UnityAppController@endIMPL_APP_CONTROLLER_SUBCLASS(OverrideAppDelegate)@implementation OverrideAppDelegate-(BOOL)application:(UIApplication*) application didFinishLaunchingWithOptions:(NSDictionary*) options{ NSLog(@”[OverrideAppDelegate application:%@ didFinishLaunchingWithOptions:%@]”, application, options); return [super application:application didFinishLaunchingWithOptions:options];}@end See OverrideAppDelegate.m at GitHub

This can be a quick solution when your project has a single iOS plugin intended to override app delegate. But if you are about to use more iOS plugins that uses the same technique, one or the other will be broken. (Unfortunately) you can find countless Unity plugins on GitHub using the very same technique.

You may create an override chain of plugins, but if you are conflicting with a closed source iOS plugin binary that uses this technique, then you basically can’t (unless you crack it).

Unity Plugin workflow considerations when override app delegate

To have a maintainable plugin, the first consideration is being totally encapsulated. It should not make a single assumption about the application, also it should not change a single thing in the application architecture itself. In the example above, you make two assumptions, both can be wrong. You assume that the base class of the app is UnityAppController, then you are assume that the value of AppControllerClassName is not gonna be changed after you have changed so.

So in general, if you make such assumptions, the application may break the plugin (often the case when using multiple plugins), or the plugin may break the app (often the case when the app development progresses, but the plugin does not follow).

I found that the most common consideration people pass over during plugin development is being compatible with other plugins (!). You may define a new base class for the main Unity controller, but if you are using another plugin that essentially does the same, you will break its intended behaviour.

Almost the same considerations lead to the Unity Android Plugin Architecture in the article Unity Android plugin tutorial (2/3) Project setup and workflow, that extends behaviour without overriding application’s main Activity. Two or more plugins wanting to do the same could also create such conflicting situations.

Ultimately this mindset leads to a healthier Unity plugin ecosystem in the long run.

Unity Plugin architectures for iOS and macOS

On iOS, Unity plugins can be packed / compiled along with the rest of the application code. They can be added in form of native classes, static or dynamic libraries (Libraries or Frameworks using iOS terms). That means native plugin classes gets loaded alongside the application classes. As application launch happens after the classes gets loaded, iOS plugins can extend application launching behaviours more easily.

On macOS, however, Unity asks for plugins in form of bundles (following the general macOS plugin pattern). That means that plugin code will be loaded by the Objective-C application runtime (probably using an :[NSBunde load]: call). As a consequence, the plugin code gets loaded after the application has finished launching, seemingly even after Unity Player has launched, around the loading of the first scene. Obviously, using solely this architecture, you cannot customize app launching behaviour with macOS plugins.

A simple yet maintainable Unity iOS plugin workflow

Now comes the step-by-step tutorial part. First we create a simple Unity iOS plugin. While you can add Objective-C sources directly to Unity, I find it more maintainable to pack plugins as libraries (or even frameworks).

In general, I found it really convenient to have the native plugin projects (iOS, macOS, Android) in the Unity project root folder (outside Assets folder), then setup a copy command in Xcode that puts the resulting plugins into Assets folder (I actually did something very similar at Unity Android plugin tutorial (2/3) Project setup and workflow). Having this, version control also comes really easy (you can even maintain sub-projects as Git Submodules).

So for this plugin, you should make a folder called iOS beside the Assets folder. As your native platform count grows, this folder structure can really grow with them.

Create Cocoa Touch Static Library
Create Override iOS Static Library

Now in Xcode create a new Cocoa Touch Static Library called Override_iOS. It automatically creates a single class called Override_iOS. To see the plugin working, we simply log a message to the console when it is loaded. Fortunately Objective-C classes have a class method template called load you can use for this.#import “Override_iOS.h”@implementation Override_iOS+(void)load{ NSLog(@”[Override_iOS load]”); }@end See Override_iOS.m at GitHub

New Copy Files Build Phase

You can build the library ⌘+B, the resulting file libOverride_iOS.a can be found in the Products group. Next we make Xcode to deploy the library file automatically. First make sure you have a folder called Plugins in Unity. After that, select the Override_iOS target in Xcode, and add a New Copy Files Phase in the targets Build Phases.

Copy Files Build Phase Absolute Path

Drag the libOverride_iOS.a to the file list, then simply define the Assets/Plugins folder as an Absolute Path (you can see where my project resides above, you should locate yours). After hit build ⌘+B in Xcode, the library should be copied right into your Unity project.

iOS Static Library Unity Import Setting
iOS Unity PlayerSettings

If you click on the libOverride_iOS.a file in Unity project window, you can see the Unity plugin inspector. Select iOS as the plugin platform, and click apply. Similar to the structure I used with plugin projects, I prefer somewhat similar folder structure for Unity builds as well. So make a folder called Build beside Assets, then create an iOS folder in it. To be able to run the app in the simulator, set Target SDK to Simulator SDK in iOS PlayerSettings.

Unity iOS Player Plugin Static Library
Unity iOS Player Build Phases Link Binary With Libraries

There you go, build an iOS player, then open it in Xcode. You can see that the library has been deployed to the Libraries/Plugins group, also if you take a look on the Build Phases tab, it is linked in the Link Binary With Libraries along with the rest of the libraries.

Load iOS Unity plugin before application launches

Now if you run ⌘+R the resulting iOS app in the simulator, you should see the message coming from the plugin in the console indicating that the framework has been loaded. But you don’t.

Unity iOS Build Target
Unity iOS Build Other Linker Flags ObjC

You don’t see it, as by default iOS won’t link the library until you need it. You can change this behaviour by tell the linker to force load static library symbols. Select the Unity-iPhone build target in Xcode, then in the Build Settings tab, add this entry to the Other Linker Flags -ObjC.

Unity iOS Build Other Linker Flags ObjC

After this, run ⌘+R the resulting iOS app in the simulator, and take a look at the console. You can see [Override_iOS load] right at the top, that means the plugin code has been loaded before (!) any other thing happened in the application. This is very promising considering that we want to override app launching behaviour later on.

To manage the workflow accordingly, the build settings alteration should be included right into the Unity project. There is an API called PBXProject to manipulate the built Xcode project. So create a file in Unity called BuildPostProcessor.cs in Assets/Editor, and put this content from the Gist below (you can remove the frameworks part leaving the linker flag only). This method also proven quiet useful when hijacking macOS executables later.https://platform.twitter.com/embed/Tweet.html?dnt=false&embedId=twitter-widget-2&features=eyJ0ZndfZXhwZXJpbWVudHNfY29va2llX2V4cGlyYXRpb24iOnsiYnVja2V0IjoxMjA5NjAwLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X2hvcml6b25fdHdlZXRfZW1iZWRfOTU1NSI6eyJidWNrZXQiOiJodGUiLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X3NwYWNlX2NhcmQiOnsiYnVja2V0Ijoib2ZmIiwidmVyc2lvbiI6bnVsbH19&frame=false&hideCard=false&hideThread=false&id=821489274692444160&lang=en&origin=http%3A%2F%2Fblog.eppz.eu%2Foverride-app-delegate-unity-ios-macos-1%2F&sessionId=4998a53c135c3b6deb85bfe528b8e68ec5bf17bd&theme=light&widgetsVersion=86e9194f%3A1641882287124&width=550px

Implement basic features in Unity iOS plugin

Ready to add some basic features to the plugin, actually a simple hello world that comes to Unity from the plugin side. So create some files in the plugin Xcode project. A helper tool called UnityString_C++.mm for string conversion between iOS and Unity, and a simple Objective-C++ file called Override_C++.mm with a single static external C function called getMessage.#import “UnityString_C++.mm”#import “Override_iOS.h”extern “C”{ const char* getMessage(){ return UnityStringFromNSString(@”Greetings from iOS!”); }} See Override_C++.mm at GitHub

After this you can now create the C# counterpart in Unity. Create a C# script file called Override.cs in Assets/Plugins folder, then hook up the message from the plugin with a text label. These parts are mostly taken after the official Building Plugins for iOS documentation.using UnityEngine;using UnityEngine.UI;using System.Runtime.InteropServices;public class Override : MonoBehaviour{ public Text label;#if !UNITY_EDITOR && UNITY_IOS [DllImport(“__Internal”)] static extern string getMessage(); void Awake(){ label.text = getMessage(); }#endif} See Override.cs at GitHub

Unity iOS Plugin Workflow Folder Structure
Unity iOS Plugin Hello World 400px

There you go, you have setup a really convenient plugin workflow that fits well to version control, changes, also come without any need for manual Xcode project post-processing (you can recap the resulting folder structure to the left). Ready to be filled up with all the heavy features in the upcoming parts of the series.