[Unity] When implementing iOS native plugin with Swift, the setting method will change around 2019.3.

Due to the addition of ** "Unity as a Library" ** as a new feature from Unity 2019.3, the configuration of .xcodeproj after building for iOS has changed significantly.

Perhaps because of that, ** For example, even if I tried to implement the Swift code by the method described in the following article, the build did not pass **, so this time I will note the various solutions while comparing it with the conventional method. I would like to summarize it in.

-Native plugin for writing Unity iOS native plugin in Swift

There may be an oversight such as "I couldn't actually make this pattern", so I will add it as soon as I understand it. (If you have any information that says "I can't do that with the content here", I would appreciate it if you could let me know in the comments and edit requests: pray :: sweat_drops :)

version

** Public repository **

The set of projects used for this verification is available on GitHub. We have prepared two projects, "a project that applies the conventional method" and "a project that applies the method from 2019.3."

What kind of changes have been made since 2019.3.

In the past, all code and data were aggregated in the target called Unity-iPhone, but from ** 2019.3, a new framework target called" Unity Framework "has been added. So, ** Classes, Libraries folders, and other dependent frameworks are now separated here.

The official document also has a description of these details, so please see the following for details.

-** Structure of a Unity Xcode Project --2019.4 (after change) ** -Structure of a Unity XCode Project --2018.4 (before change)

The part that will be greatly affected by this change is the update process of PBX Project, which will probably be processed by[PostProcessBuild], and it is necessary to redirect the GUID to Unity Framework as follows. It came out.

        [PostProcessBuild]
        static void OnPostProcessBuild(BuildTarget target, string path)
        {
            if (target != BuildTarget.iOS) return;

            var projectPath = PBXProject.GetPBXProjectPath(path);
            var project = new PBXProject();
            project.ReadFromString(File.ReadAllText(projectPath));

#if UNITY_2019_3_OR_NEWER
            // NOTE: 2019.From 3`UnityFramework`Apply settings to
            var targetGUID = project.GetUnityFrameworkTargetGuid();
#else
            var targetName = PBXProject.GetUnityTargetName();
            var targetGUID = project.TargetGuidByName(targetName);
#endif

            project.AddFrameworkToProject(targetGUID, "MonafuwaLowMemoryWarningFramework.framework", false);
            File.WriteAllText(projectPath, project.WriteToString());
        }

** However, when dealing with Swift, simply changing the direction of the GUID to Uinty Framework as shown in ↑ will not solve the problem, so we will explain the specific solution later. ** **

How to implement traditional Swift code

First, as a preliminary knowledge, I will describe the conventional implementation method up to 2019.2.

That said, what you're doing is pretty much the same as in the article below. If you are familiar with this, you can skip it.

-Native plugin for writing Unity iOS native plugin in Swift

Here, I will explain based on Exsamples in the following project.

PBX Project changes

Almost as described in the article. I will summarize only a few points in a bulleted list.

--Set UnitySwift-Bridging-Header.h to Objective-C Bridging Header -* UnitySwift-Bridging-Header.h is applied so that the one imported as a plug-in in advance is set during the project. - Assets/UnitySwift/UnitySwift-Bridging-Header.h --Set unityswift-Swift.h to Objective-C Generated Interface Header Name --Set @ executable_path/Frameworks to Runpath Search Paths --Set Swift version to 5.0 -* As mentioned in the comment, if you do not specify this explicitly, the area around 3.0 will be set by default? Due to the influence, it may be treated as Unspecified depending on Xcode. -* By the way, since 5.0 is the latest, it is only specified for the time being, and there is no particular reason to say this. For this version, set the required value according to your requirements.

XcodePostProcess.cs


    /// <summary>
    ///Automatically apply the settings required to implement Swift
    /// </summary>
    sealed class XcodePostProcess
    {
        [PostProcessBuild]
        static void OnPostProcessBuild(BuildTarget target, string path)
        {
            if (target != BuildTarget.iOS) return;

            var projectPath = PBXProject.GetPBXProjectPath(path);
            var project = new PBXProject();
            project.ReadFromString(File.ReadAllText(projectPath));

            var targetGuid = project.TargetGuidByName(PBXProject.GetUnityTargetName());
            project.SetBuildProperty(targetGuid, "SWIFT_OBJC_BRIDGING_HEADER", "Libraries/UnitySwift/UnitySwift-Bridging-Header.h");
            project.SetBuildProperty(targetGuid, "SWIFT_OBJC_INTERFACE_HEADER_NAME", "unityswift-Swift.h");
            project.AddBuildProperty(targetGuid, "LD_RUNPATH_SEARCH_PATHS", "@executable_path/Frameworks");

            // Swift version: 5.0
            // NOTE:3 if not explicitly specified.It seems that the old one is set to about 0, so depending on Xcode`Unspecified`Be treated
            project.AddBuildProperty(targetGuid, "SWIFT_VERSION", "5.0");

            File.WriteAllText(projectPath, project.WriteToString());
        }
    }

In addition to Foundation.h and UIKit.h, UnityInterface.h is imported to UnitySwift-Bridging-Header.h in advance. (UnitySendMessage etc. are declared in UnityInterface.h, so if you want to call UnitySendMessage from Swift, you need to import it)

UnitySwift-Bridging-Header.h


//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "UnityInterface.h"

Swift code

The Swift code is implemented as follows.

callSwiftMethod only prints the string passed from the C # side with print, and callUnityMethod calls UnitySendMessage and sends the string "Hello, Unity!" To the C # side. I will.

Exsample.swift


import Foundation

class Example : NSObject {

    // ObjC++Call a Swift method from
    @objc static func callSwiftMethod(_ message: String) {
        print("\(#function) is called with message: \(message)")
    }
    
    //Call SendMessage from a Swift method
    @objc static func callUnityMethod() {
        // Call a method on a specified GameObject.
        UnitySendMessage("CallbackTarget", "OnCallFromSwift", "Hello, Unity!")
    }
}

Objective-C ++ code

Since it is not possible to call from C # with only Swift code, prepare Obj C ++ code so that it can be called with P/Invoke. The content is just calling the method on the Swift side as it is.

ExsampleBridge.mm


#import <Foundation/Foundation.h>

// ObjC++Required to access Swift classes from
// NOTE: `unityswift-Swift.h`The actual situation at build time`DerivedData`Automatically generated below
#import "unityswift-Swift.h"    // Required

// P/Invoke
extern "C" {

    void callSwiftMethod(const char *message) {
        [Example callSwiftMethod:[NSString stringWithUTF8String:message]];
    }

    void callUnityMethod() {
        [Example callUnityMethod];
    }
}

C # code

Implement the Swift code implemented above and the ObjC ++ code call processing for P/Invoke. In Exsample, you can call callSwiftMethod and callUnityMethod on the Swift side from two Buttons on the screen.

For callUnityMethod, for the convenience of receiving the result with SendMessage, the received character string is output to the log by matching the GameObject name and method name.

** `Exsample.cs` (click to expand) **

Exsample.cs


using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.UI;

namespace UnitySwift.Exsamples
{
    /// <summary>
    ///iOS Native Plugin call sample
    /// </summary>
    public sealed class Exsample : MonoBehaviour
    {
        [SerializeField] Button _callSwiftMethod = default;
        [SerializeField] Button _callUnityMethod = default;

        void Start()
        {
            //Set the name of the GameObject specified when calling SendMessage from Swift
            this.name = "CallbackTarget";

            _callUnityMethod.onClick.AddListener(() =>
            {
#if !UNITY_EDITOR && UNITY_IOS
                CallUnityMethod();
#endif
            });

            _callSwiftMethod.onClick.AddListener(() =>
            {
#if !UNITY_EDITOR && UNITY_IOS
                CallSwiftMethod("Gorilla");
#endif
            });
        }

        /// <summary>
        ///A method of action called by SendMessage from Swift
        /// </summary>
        /// <param name="message"></param>
        void OnCallFromSwift(string message)
        {
            Debug.Log(message);
        }

        /// <summary>
        /// ObjC++Call a Swift method from
        /// </summary>
        /// <remarks>[C# -> ObjC++ -> Swift]Called in the flow of</remarks>>
        [DllImport("__Internal", EntryPoint = "callSwiftMethod")]
        static extern void CallSwiftMethod(string message);

        /// <summary>
        ///Call SendMessage from a Swift method
        /// </summary>
        /// <remarks>* There is an intention to call the method defined on the Unity side from Swift</remarks>>
        [DllImport("__Internal", EntryPoint = "callUnityMethod")]
        static extern void CallUnityMethod();
    }
}

How to implement Swift code from 2019.3.

If you build .xcodeproj using the conventional method described above, you will definitely get an error and get angry.

One of the reasons is that the target called Framework, which is the actual state of Unity Framework, cannot set Bridging-Header.h, so maybe an error related to this is output at an early timing. It seems.

In addition to this, there are some minor changes that have been made, so I will explain them step by step.

Here, I will explain based on Exsamples in the following project.

PBX Project changes

I explained the changes of PBX Project earlier, but from the conclusion, it seemed that it was unnecessary except for **" Swift version specification ". ** **

Since UnitySwift-Bridging-Header.h cannot be set in the first place, it is also deleted from the project.

XcodePostProcess.cs


    sealed class XcodePostProcess
    {
        /// <summary>
        ///Automatically apply the settings required to implement Swift
        /// </summary>
        [PostProcessBuild]
        static void OnPostProcessBuild(BuildTarget target, string path)
        {
            if (target != BuildTarget.iOS) return;

            var projectPath = PBXProject.GetPBXProjectPath(path);
            var project = new PBXProject();
            project.ReadFromString(File.ReadAllText(projectPath));

            // 2019.From 3`UnityFramework`Since it is separated into, targetGuid needs to be printed here.
            // NOTE:If you want to coexist with the previous version,#if UNITY_2019_3_OR_It is also possible to divide by "NEWER"
            var targetGuid = project.GetUnityFrameworkTargetGuid();

            // NOTE:
            //I had set it before`Bridging-Header.h`The kind of setting is 2019.It seems unnecessary from 3.
            //Rather, Cocoa touch Framework is Bridging-Since it does not support Header, it gets angry when it is set.

            // Swift version: 5.0
            // NOTE:3 if not explicitly specified.It seems that the old one is set to about 0, so depending on Xcode`Unspecified`Be treated
            project.AddBuildProperty(targetGuid, "SWIFT_VERSION", "5.0");

            File.WriteAllText(projectPath, project.WriteToString());
        }
    }

Swift code

There are two changes as follows.

-** Set the access level of the class/method exposed to ObjC ++ to public ** -** Use sendMessageToGO implemented in Unity Framework instead of UnitySendMessage **

Regarding the latter change, I referred to the following issue/PR. (In my memory, sendMessageToGO was used around the exchange between" Native → Unity "in UaaL [^ 1])

[^ 1]: UaaL ・ ・ ・ Abbreviation for Unity as a Library

It seems that the conventional UnityInterface.h is not included in the target in the first place and can not even be referenced ...?

Exsample.swift


import Foundation

// NOTE:Access level for items published in ObjC`public` or `open`Must be set to

public class Example : NSObject {

    // ObjC++Call a Swift method from
    @objc public static func callSwiftMethod(_ message: String) {
        print("\(#function) is called with message: \(message)")
    }

    //Call SendMessage from a Swift method
    @objc public static func callUnityMethod() {
    
        // NOTE:Traditional`UnityInterface.h`It is in`SendMessage`Can't be referred to? So, use the following sendMessageToGO.
    
        // Call a method on a specified GameObject.
        if let uf = UnityFramework.getInstance() {
            uf.sendMessageToGO(
                withName: "CallbackTarget",
                functionName: "OnCallFromSwift",
                message: "Hello, Unity!")
        }
    }
}

Objective-C ++ code

The main change here is the ** import header, which changes the target from unityswift-Swift.h to UnityFramework/UnityFramework-Swift.h. ** **

ExsampleBridge.mm


#import <Foundation/Foundation.h>

// 2019.You need to import this from 3
#import <UnityFramework/UnityFramework-Swift.h>

// P/Invoke
extern "C" {

    void callSwiftMethod(const char *message) {
        [Example callSwiftMethod:[NSString stringWithUTF8String:message]];
    }

    void callUnityMethod() {
        [Example callUnityMethod];
    }
}

C # code

There is no change in the calling code on the C # side, so it is omitted.

Reference link

-It's not good to mix Swift and Objective-C in Framework -Native plugin for writing Unity iOS native plugin in Swift -[Unity] Shortest implementation to run Swift from C # in recent Unity

Recommended Posts

[Unity] When implementing iOS native plugin with Swift, the setting method will change around 2019.3.
Change the layout when rotating with onConfigurationChanged
[Swift] Setting the behavior when the Share button is pressed