Unity3D: Modifying the delegate of an iOS application

    I think many in the course of developing a game for iOS had to deal with the fact that there is a need to use one or another native functionality. With regards to Unity3D, there can be a lot of problems in this matter: in order to implement a feature, you have to look towards native plug-ins written in Objective-C. Someone at this moment immediately despair and throws the idea. Someone is looking for ready-made solutions in AssetStore or on forums, hoping that a ready-made solution already exists. If there are no ready-made solutions, then the most resistant of us see no other way out than to dive into the depths of iOS programming and the interaction of Unity3D with Objective-C code.

    Those who choose the last path (although I think they themselves know) will face many problems on this difficult and thorny path:

    • iOS is a completely unfamiliar and detached ecosystem, developing in its own way. At a minimum, you will have to spend quite a lot of time to figure out how to get close to the application, and where in the depths of the automatically generated XCode of the project there is the Unity3D interaction code of the engine with the native component of the application.
    • Objective-C is rather detached and not very similar to a programming language. And when it comes to interacting with the C ++ code of the Unity3D application, a “dialect” of this language, called Objective-C ++, comes on the scene. Information about him is very little, most of it is ancient and archival.
    • The Unity3D interaction protocol with the iOS application is rather poorly described. You should rely solely on tutorials of enthusiasts in the network who write how to develop the simplest native plugin. At the same time, few people raise deeper questions and problems arising from the need to do something complicated.

    Those who want to learn about the mechanisms of interaction with Unity3D iOS application, please under the cat.

    In order to bring more clarity to the gloom-covered bottleneck of Unity3D interaction with native code, this article describes aspects of how an iOS delegate interacts with Unity3D code, with which C ++ and Objective-C tools it is implemented, and how to modify the application delegate itself. This information can be useful both for a better understanding of the mechanisms of operation of the Unity3D + iOS bundle, and for practical use.

    Interaction between iOS and the application


    As an introduction, let's consider how the interaction of an application with the system is implemented in iOS and vice versa. Schematically, the launch of an iOS application looks like this:

    image

    To study this mechanism from a code point of view, a new application created in XCode using the “Single View App” template will be suitable.



    By selecting this template, at the output we get the simplest iOS application that can run on a device or an emulator and show a white screen. Xcode will helpfully create a project that contains only 5 files with source code (2 of them are header .h files) and several auxiliary files that are not interesting to us (layout, configs, icons).



    Let's see what the source code files are responsible for:

    • ViewController.m / ViewController.h - not very interesting source code. Since your application has a View (which is represented not by the code, but by using the Storyboard), you need the Controller class, which will control this View. In general, this is how Xcode itself pushes us to use the MVC pattern. The project that generates Unity3D will not have these source files.
    • AppDelegate.m / AppDelegate.h - delegate of your application. The point of interest in the application where the custom application code begins.
    • main.m - the starting point of the application. In the manner of any C / C ++ application contains a function main, with which the program starts.

    Now, let's see the code starting from the file main.m :

    int main(int argc, char * argv[]) { //1@autoreleasepool {  //2returnUIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); // 3
    } } 

    With line 1, everything is clear and without explanation, let's move on to line 2. It indicates that the life cycle of the application will occur inside the Autorelease pool. Using the autorelease pool, it tells us that we will entrust the memory management of the application to this particular pool, that is, it will deal with the issues of when to free up memory for a particular variable. The story about the management of memory on iOS goes beyond the scope of this story, so it makes no sense to delve into this topic. For those who are interested in this topic, you can read, for example, with this article .

    Let's go to line 3. UIApplicationMain function is called in it.. The program launch parameters (argc, argv) are passed to it. Then, in this function it is indicated which class to use as the main class of the application, an instance is created. And, finally, it is indicated which class to use as the application delegate, an instance is created, connections between the application class instance and its delegate are configured.

    In our example, as a class that will represent an instance of an application, nil is passed - roughly speaking, the local equivalent is null. In addition to nil, you can pass there a specific class inherited from UIApplication. If nil is specified, UIApplication will be used. This class is a centralized point of control and coordination of the application on iOS and is singleton (singleton). With it, you can find out almost everything about the current state (state) of an application, notifications, windows, events that occurred in the system itself, which affect this application and many other things. This class is almost never inherited. We will dwell on the creation of the Application Delegate class.

    Creating an application delegate


    An indication of which class to use as an application delegate occurs in a function call

    NSStringFromClass([AppDelegate class])

    Let's sort this call in parts.

    [AppDelegate class]

    This construct returns an object of the class AppDelegate (which is declared in AppDelegate.h / .m), and the function NSStringFromClass returns the name of the class as a string. We simply pass to the UIApplicationMain function the string name of the class that needs to be created and used as a delegate. For greater understanding, line 3 in main.m could be replaced by the following:

    returnUIApplicationMain(argc, argv, nil, @"AppDelegate");

    And the result of its implementation would be identical to the original version. Apparently, the developers decided to come to exactly this approach so as not to use a string constant. With the standard approach, if you rename the delegate class, the parser will immediately generate an error. In the case of using the usual string, the code will be compiled successfully, and you will get an error only by running the application.

    A similar class creation mechanism, using only the string class name, can remind you of Reflection from C #. Objective-C and its execution environment (runtime) have much greater capabilities than Reflection in C #. This is quite an important point in the context of this article, but it would take a lot of time to describe all the possibilities. However, we will still meet with “Reflection” in Objective-C below. It remains to understand the concept of the application delegate and its functions.

    Application Delegate


    All application interaction with iOS occurs in the UIApplication class. This class takes on a lot of responsibility - notifies about the origin of events, about the state of the application and much more. For the most part, his role is a notifying one. But when something happens in the system, we should be able to somehow respond to this change, to perform some kind of custom functionality. If an instance of the UIApplication class is also involved in this - this practice will begin to resemble an approach called the Divine Object . Therefore, it is worth thinking about how to free this class from a part of its duties.

    For these purposes, the iOS ecosystem uses such a thing as an application delegate. From the name itself it can be concluded that we are dealing with such a design pattern as Delegation . In short, we simply transfer the responsibility for handling the reaction to certain application events to the application delegate. For this purpose, in our example, the AppDelegate class has been created, in which we can write custom functionality, while leaving the UIApplication class to work in black box mode. Such an approach may seem controversial to someone in terms of the beauty of architecture design, but the iOS authors themselves are pushing us towards this approach and the vast majority of developers (if not all) use it.

    To visually see how often an application delegate receives a message during an application, take a look at the diagram:

    image

    In the yellow boxes, calls to certain delegate methods are indicated in response to certain application life events. This diagram only illustrates events related to a change in application state and does not reflect many other aspects of delegate responsibility, such as accepting notifications or interacting with frameworks.

    Here are some examples of when we may need access to the application delegate from Unity3D:

    1. handling push and local notifications
    2. logging to application analytics
    3. Determining how to launch the application - “to clean” or exit from the background
    4. how exactly the application was launched - by tachu on notification, using Home Screen Quick Actions or simply by tachu on Inconcus
    5. interaction with WatchKit or HealthKit
    6. opening and processing URLs from another application. If this URL refers to your application, you can process it in your application instead of letting the system open this URL in a browser.

    This is not the whole list of scenarios. In addition, it is worth noting that the delegate modifies many analytics and advertising systems in their native plugins.

    How Unity3D Implements Application Delegate


    Let's now take a look at the XCode project generated by Unity3D and see how the application delegate is implemented in Unity3D. When building for the iOS platform, Unity3D automatically generates the Xcode project for you, which uses quite a lot of generic code. This template code also includes the Delegate code for the Application. Inside any generated project, you can find the files UnityAppController.h and UnityAppController.mm . These files contain the code of the UnityAppController class of interest.

    In fact, Unity3D uses a modified version of the “Single View Application” template. Only in this template Unity3D uses the application delegate not only to handle iOS events, but also to initialize the engine itself, prepare graphic components and much more. It is very easy to understand if you look at the method

    - (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
    

    in the UnityAppController class code. This method is called at the time of application initialization, when you can transfer control to your custom code. Inside this method, for example, you can find the following lines:

    UnityInitApplicationNoGraphics([[[NSBundle mainBundle] bundlePath] UTF8String]);
        [self selectRenderingAPI];
        [UnityRenderingView InitializeForAPI: self.renderingAPI];
        _window         = [[UIWindow alloc] initWithFrame: [UIScreen mainScreen].bounds];
        _unityView      = [self createUnityView];
        [DisplayManager Initialize];
        _mainDisplay    = [DisplayManager Instance].mainDisplay;
        [_mainDisplay createWithWindow: _window andView: _unityView];
        [self createUI];
        [self preStartUnity];
    

    Even without going into the details of what these calls do, one can guess that they are related to preparing Unity3D for work. It turns out the following script:

    1. The main function from main.mm is called.
    2. An instance of the application class and its delegate is created.
    3. The application delegate prepares and launches the Unity3D engine.
    4. Your custom code starts working. If you use il2cpp, then your code is translated from C # to IL and then to C ++ code that directly goes into the XCode project.

    This script sounds quite simple and logical, but carries with it a potential problem: how can we modify the application delegate if we do not have access to the source code when working in Unity3D?

    Unity3D target for modifying the application delegate


    We can look at the files AppDelegateListener.mm/.h . They contain macros that allow you to register any class as a listener for the application delegate events. This is a good approach, we do not need to modify the existing code, but just add a new one. But there is a significant drawback: not all the events of the application are supported and there is no possibility to get information about the launch of the application.

    The most obvious, however, unacceptable way out is to change the delegate source code with your hands after Unity3D builds the XCode project. The problem with this approach is obvious - the option will work if you are doing the assembly with your hands and you are not bothered by the need to modify the code manually after each assembly. In the case of using collectors (Unity Cloud Build or any other build machine) this option is absolutely unacceptable. For these purposes, the developers of Unity3D left us a loophole.

    The UnityAppController.h file, in addition to declaring variables and methods, also contains the definition of a macro:

    #define IMPL_APP_CONTROLLER_SUBCLASS(ClassName) ... 

    This macro just gives the opportunity to override the application delegate. To do this, you need to take a few simple steps:

    1. Write your own application delegate in Objective-C
    2. Somewhere inside the source code add the following line
      IMPL_APP_CONTROLLER_SUBCLASS(Имя_класса_вашего_класса)
    3. Put this source inside the folder of your Unity3D project's Plugins / iOS

    Now you will receive a project in which the standard Unity3D delegate of the application will be replaced with your custom one.

    How a delegate replacement macro works


    Let's look at the full source code of the macro:

    #define IMPL_APP_CONTROLLER_SUBCLASS(ClassName) ...@interfaceClassName(OverrideAppDelegate)       \
    {                                               \
    }                                               \
    +(void)load;                                    \
    @end                                            \
    @implementationClassName(OverrideAppDelegate)  \
    +(void)load                                     \
    {                                               \
        externconstchar* AppControllerClassName;  \
        AppControllerClassName = #ClassName;        \
    }                                               \
    @end

    Using this macro in your source code will add the code described in the macro to the body of your source code at the compilation stage. This macro does the following. First, it adds a load method to your class interface. An interface in the context of Objective-C can be viewed as a set of public fields and methods. Speaking in C #, a static load method will appear in your class that returns nothing. Next, the implementation of this load method will be added to the code of your class. In this method, the variable AppControllerClassName, which is an array of type char, will be declared and then this variable will be assigned a value. This value is the string name of your class. Obviously, this information is not enough to understand the mechanism of this macro, so we should deal with

    The official documentation states that load is a special method that is called once for each class (specifically a class, and not its instances) at the earliest stage of the application launch, before the main function is called. The Objective-c (runtime) runtime at the start of the application will register all classes that will be used during the operation of the application and call their load method, if it is implemented. It turns out that even before the start of any code of our application, the variable AppControllerClassName will be added to your class.

    Here you might think: “What is the point of having this variable, if it is declared inside a method and will be destroyed from memory, when exiting this method?”. The answer to this question lies a bit beyond Objective-C.

    And here With ++?


    Let's take another look at the declaration of this variable.

    externconstchar* AppControllerClassName;

    The only thing that can be incomprehensible in this declaration is the extern modifier. If you try to use this modifier in pure Objective-C, then Xcode will generate an error. The fact is that this modifier is not part of Objective-C, it is implemented in C ++. Objective-C can be described quite succinctly, saying that it is a “C language with classes”. It is an extension of the C language and allows unlimited use of C code in conjunction with Objective-C code.

    However, to use extern and other features of C ++, you need to go for some trick - use Objective-C ++. There is practically no information about this language, for the reason that it is just Objective-C code that allows insertion of C ++ code. In order for the compiler to consider that some source file should be compiled as Objective-C ++, and not Objective-C you just need to change the extension of this file from .m to .mm .

    The extern modifier itself is used to declare a global variable. More precisely, to tell the compiler “Believe me, such a variable exists, but the memory for it was allocated not here, but in another source. And she has value too, I guarantee. ” Thus, our line of code simply creates a global variable and stores in it the name of our custom class. It remains only to understand where this variable can be used.

    Back to main


    We recall what was said earlier - the application delegate is created by specifying the name of the class. If a delegate created a standard Xcode template using the constant value [myClass class], then apparently the guys from Unity decided that this value should be wrapped in a variable. Using the scientific method, take the XCode project generated by Unity3D and go to the main.mm file .

    In it, we see a more complex code than before, part of this code is missed as unnecessary:

    // WARNING: this MUST be c decl (NSString ctor will be called after +load, so we cant really change its value)constchar* AppControllerClassName = "UnityAppController";
    int main(int argc, char* argv[])
    {
        ...
            UIApplicationMain(argc, argv, nil, [NSString stringWithUTF8String: AppControllerClassName]);
        }  return0;
    }

    Here we see the declaration of this variable itself, and the creation of the application delegate with its help.
    If we created a custom delegate, then the desired variable exists and already matters - the name of our class. And declaring and initializing a variable before the main function ensures that it has a default value, UnityAppController.

    Now with this decision should be all very clear.

    Macro problem


    Of course, for the vast majority of situations, using this macro is a great solution. But it is worth noting that there is a large pitfall in it: you cannot have more than one custom delegate. This is because if 2 or more classes use the IMPL_APP_CONTROLLER_SUBCLASS (ClassName) macro, then for the first one, the value of the variable we need will be assigned, and further assignments are ignored. And this variable is a string, that is, it cannot be assigned more than one value.

    You might think that this problem is degenerate and in practice is unlikely. But, this article would not have happened if such a problem had not really happened, and even under very strange circumstances. The situation may be as follows. You have a project in which you use a lot of analytics and advertising services. Many of these services have Objective-C components. They have long been in your project and you do not know the troubles with them. Here you need to write a custom delegate. You use a magic macro, designed to save you from problems, collect the project and get a report on the success of the assembly. Run the project on the device, and your functionality does not work and at the same time you do not get a single error.

    But the point may be that one of the plug-ins of advertising or analytics uses the same macro. For example, in the plugin fromAppsFlyer this macro is used.

    What value will the extern variable take in case of multiple declarations?


    It is interesting to understand, if the same extern variable is declared in several files, and they are initialized in the manner of our macro (in the load method), how can you understand what value the variable will take? To understand the pattern, a simple test application was created, the code of which can be viewed here .

    The essence of the application is simple. There are 2 classes A and B, in both classes the extern variable AexternVar is declared, it is assigned a specific value. Variable values ​​in classes are set differently. In the main function, the value of this variable is output to the log. Experimentally, it turned out that the value of a variable depends on the order in which source codes are added to the project. The order in which the Objective-C runtime environment will register classes during the course of an application depends on this. If you want to repeat the experiment, open the project and select the Build Phases tab in the project settings. Since the project is test and small - there are only 8 sources in it. All of them are present on the Build Phases tab in the Compile Sources list.



    If in this list the source of class A is higher than the source of class B, then the variable will take the value from class B. Otherwise, the variable will take the value from class A.

    Just imagine how many problems this small nuance can theoretically cause. Especially if the project is huge, automatically generated and you do not know in which classes such a variable is declared.

    Solution to the problem


    Earlier in the article it was already said that Objective-C will give odds to C # Reflection. Specifically, to solve our problem, you can use a mechanism that is called Method Swizzling . The essence of this mechanism is that we have the opportunity to replace the implementation of the method of any class with another in the course of the application. Thus, we can replace the method we are interested in in UnityAppController with a custom one. We take existing implementation and we supplement with the code necessary to us. We write code that replaces the existing implementation of the method with the one we need. In the course of the application, the delegate using the macro will work as before, calling the base implementation of the UnityAppController, and there our custom method will come into play and we will achieve the desired result. This approach is well written and illustrated inthis article . With this trick, we can make an auxiliary class — an analogue of a custom delegate. In this class, we will write the entire custom code, making the custom class a kind of Wrapper to call the functionality of other classes. Such an approach will work, but it is extremely implicit due to the fact that it is difficult to track where the method is replaced and what consequences it will have.

    Another solution


    The main aspect of the problem that has happened is that there are many who want to have custom delegates, but only one, or partially replaced by the second. At the same time, there is no possibility to make the code of custom delegates not crawl along different source files. It turns out that the reference situation can be considered when there is only one delegate in the application, you need to be able to create custom classes as many as you want, and none of these classes use the macro to avoid problems.

    Things are going well, it remains to determine how this can be done with Unity3D, while leaving the possibility of building the project using a build machine. The solution algorithm is as follows:

    1. We write custom delegates in the required quantity, dividing the logic of plug-ins into different classes, observing the principles of SOLID and not resorting to sophistication.
    2. To avoid using a macro, we take the UnityAppController source from the generated Xcode project and modify it for our own needs. Directly we create copies of the necessary classes and we cause methods of these classes from UnityAppController.
    3. We save our modified UnityAppController and add the project to our Unity.
    4. We are looking for an opportunity when assembling an Xcode project is automated to replace the standard UnityAppController with the one we saved in the project

    The most difficult item from this list is undoubtedly the last. However, this feature can be implemented in Unity3D by means of the post process build script. Such a script was written on one beautiful night; you can look at it on GitHub .

    This post process is quite simple to use, select it in the Unity project. Look in the Inspector window and see there a field called NewDelegateFile. Drag and drop into this field your modified version of UnityAppController'a and save.



    When building an iOS project, the standard delegate will be replaced with a modified one, with no manual intervention required. Now, when adding new custom delegates to the project, you will only need to modify the UnityAppController variant that you have in the Unity project.

    PS


    Thanks to everyone who got to the end, the article really turned out to be extremely long. Hopefully, the written information will be useful.

    Also popular now: