Unity generates XCode project through PBXProject

1. Introduction

Recently, I read some relevant information about Unity exporting iOS projects, but I didn’t see a very complete script to automate project settings. After several days of trying, I finally produced this article. Unity provides a very powerful PBXObject that uses C# code to set various settings of the iOS project. The official document link: https://docs.unity3d.com/2018.4/Documentation/ScriptReference/iOS.Xcode.PBXProject.html, the script is written Then just put it in the Unity project. The exported iOS project can be run directly without the need for various settings. It is very convenient. Article Article 4. Complete Example has posted a complete file for For your reference, the Unity version used in the project is 2018.4.20f.

2. Detailed introduction

a. Get an iOS project object

You can easily get a PBXObject object through the path in the OnPostprocessBuild method.

public static void OnPostprocessBuild(BuildTarget buildTarget, string path)
{
  string projPath = path + "/Unity-iPhone.xcodeproj/project.pbxproj";
  PBXProject proj = new PBXProject();
  proj.ReadFromFile(projPath);
}
b. Obtain the target through PBXObject

Basically all settings will be completed in this target. Those who understand iOS development will know that we set signatures and paths (Framework search path, Library search path, Other link flags) etc.

string target = proj.TargetGuidByName("Unity-iPhone");
c. Use target to set specific parameters
c1. Set automatic signature
proj.SetBuildProperty(target, "CODE_SIGN_IDENTITY", "Apple Development");
proj.SetBuildProperty(target, "CODE_SIGN_STYLE", "Automatic");
proj.SetTeamId(target, teamId); //teamId is the exact team id corresponding to the developer (can be seen in the Apple backend)
c2. Add system Framework
proj.AddFrameworkToProject(target, "AdSupport.framework", true);
proj.AddFrameworkToProject(target, "CoreTelephony.framework", true);
proj.AddFrameworkToProject(target, "StoreKit.framework", true); //In-app purchase is required otherwise PBXCapabilityType.InAppPurchase will not be added
c3. Set bitcode and Other link flags
//Set BitCode
proj.SetBuildProperty(target, "ENABLE_BITCODE", "false");
// Set other link flags -ObjC
proj.AddBuildProperty (target, "OTHER_LDFLAGS", "-ObjC");
c3. Add the system’s tbd library (different from adding Framework)
//Customize an adding method
private static void AddLibToProject(PBXProject proj, string target, string lib) {
   string file = proj.AddFile("usr/lib/" + lib, "Frameworks/" + lib, PBXSourceTree.Sdk);
    proj.AddFileToBuild(target, file);
}
// transfer
AddLibToProject(proj,target,"libc + + .1.tbd");
AddLibToProject(proj,target,"libresolv.tbd");
c4. Add a custom dynamic library (Embed & amp;Sign)
string defaultLocationInProj = Application.dataPath + "/Editor/Plugins/iOS"; //The path where the framework is stored
const string coreFrameworkName = "boxjing.framework"; // The file name of the framework
string framework = Path.Combine(defaultLocationInProj, coreFrameworkName);
string fileGuid = proj.AddFile(framework, "Frameworks/" + coreFrameworkName, PBXSourceTree.Sdk);
PBXProjectExtensions.AddFileToEmbedFrameworks(proj, target, fileGuid);
proj.SetBuildProperty(target, "LD_RUNPATH_SEARCH_PATHS", "$(inherited) @executable_path/Frameworks");
c5. Add in-app purchases and push. Here you need to copy the entitlements file of the correct running project to the project, and then let the code be automatically copied into the iOS project for use
proj.AddCapability(target, PBXCapabilityType.InAppPurchase);
// Add Capabilities - Push
string entitlement = Application.dataPath + "/Editor/AppleNative/Unity-iPhone.entitlements";
File.Copy(entitlement, path + "/Unity-iPhone.entitlements");
proj.AddCapability(target, PBXCapabilityType.PushNotifications,"Unity-iPhone.entitlements",true);
d. Modify the Info.plist file

When packaging normal projects, you need to set version and build, and add certain permission descriptions used by the project, such as photo albums, microphones, etc. The basics are to modify the Info.plist file.
The Info.plist file is quite special. It is basically a map. You can directly read it out and operate it as a Map for easy modification:

string plistPath = path + "/Info.plist";
PlistDocument plist = new PlistDocument();
plist.ReadFromString(File.ReadAllText(plistPath));
PlistElementDict infoDict = plist.root;

After all modifications are completed, you need to call save:

File.WriteAllText(plistPath, plist.WriteToString());
d1. Modify version and build
infoDict.SetString("CFBundleShortVersionString",version); //version
infoDict.SetString("CFBundleVersion",build); //build
d2. Add permission description
//Permissions Modify the following prompt copy according to your own project
infoDict.SetString("NSLocationWhenInUseUsageDescription", "In order to discover friends around you, please allow the App to access your location"); //Geographical location
infoDict.SetString("NSPhotoLibraryUsageDescription", "In order to select photos for upload, please allow the App to access your photo album"); //Album
infoDict.SetString("NSMicrophoneUsageDescription", "In order to be able to record sound, please allow the App to access your microphone permission"); //Microphone

3.Special needs

During the development process, you may encounter functions that require modifying the iOS native code. For example, if the project integrates Aurora Push, you need to add some code to UnityAppController.mm and add some code to Modify a macro definition value in Preprocessor.h to achieve this. There are two methods available here. One is to use a script to directly replace the files in the project packaged by Unity with another written UnityAppController.mm file, or One way is to perform string replacement directly in the file.

a. Replacement of the entire file
string UnityAppControllerMM = Application.dataPath + "/Editor/AppleNative/UnityAppController.mm"; //Get our own code file path
 string tagUnityAppControllerMM = path + "/Classes/UnityAppController.mm"; //Replace the file path in the project
if (File.Exists(tagUnityAppControllerMM)) //If there is one, delete it first and then copy it.
{
     File.Delete(tagUnityAppControllerMM);
}
File.Copy(UnityAppControllerMM, tagUnityAppControllerMM);
b. Replace part of the code or insert code after a certain line

Here you need to encapsulate a file operation class to facilitate code modification:

//Define file update class
public partial class XClass : System.IDisposable
{
        private string filePath;
        public XClass(string fPath) //Initialize the object through the file path
        {
            filePath = fPath;
            if( !System.IO.File.Exists( filePath ) ) {
                Debug.LogError( filePath + "The file does not exist, please check the path!" );
                return;
            }
        }
      //Replace some strings
        public void ReplaceString(string oldStr,string newStr,string method="")
        {
            if (!File.Exists (filePath))
            {
                return;
            }
            bool getMethod = false;
            string[] codes = File.ReadAllLines (filePath);
            for (int i=0; i<codes.Length; i + + )
            {
                string str=codes[i].ToString();
                if(string.IsNullOrEmpty(method))
                {
                    if(str.Contains(oldStr))codes.SetValue(newStr,i);
                }
                else
                {
                    if(!getMethod)
                    {
                        getMethod=str.Contains(method);
                    }
                    if(!getMethod)continue;
                    if(str.Contains(oldStr))
                    {
                        codes.SetValue(newStr,i);
                        break;
                    }
                }
            }
            File.WriteAllLines (filePath, codes);
        }
      //Insert code after a certain line
        public void WriteBelowCode(string below, string text)
        {
            StreamReader streamReader = new StreamReader(filePath);
            string text_all = streamReader.ReadToEnd();
            streamReader.Close();

            int beginIndex = text_all.IndexOf(below);
            if(beginIndex == -1){
                return;
            }
            int endIndex = text_all.LastIndexOf("\\
", beginIndex + below.Length);

            text_all = text_all.Substring(0, endIndex) + "\\
" + text + "\\
" + text_all.Substring(endIndex);

            StreamWriter streamWriter = new StreamWriter(filePath);
            streamWriter.Write(text_all);
            streamWriter.Close();
        }
        public void Dispose()
        {

        }
}

It is very convenient to use:

//Preprocessor.h file
 XClass Preprocessor = new XClass(filePath + "/Classes/Preprocessor.h");
 //Add a line of code after the specified code
Preprocessor.WriteBelowCode("The line of code before the position to be added","The code to be added");
 //Replace a line in the specified code to allow remote push
Preprocessor.ReplaceString("#define UNITY_USES_REMOTE_NOTIFICATIONS 0","#define UNITY_USES_REMOTE_NOTIFICATIONS 1");

4. Complete example

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;
using UnityEditor.iOS.Xcode.Extensions;
using System.Text.RegularExpressions;

#if UNITY_IOS || UNITY_EDITOR
public class XcodeBuildPostprocessor
{
    // [PostProcessBuildAttribute(88)]
    [PostProcessBuild]
    public static void OnPostprocessBuild(BuildTarget buildTarget, string path)
    {
        if (buildTarget == BuildTarget.iOS)
        {
            UnityEngine.Debug.Log("XCodePostProcess: Starting to perform post build tasks for iOS platform.");
            // Modify project settings such as Bitcode Framework *tbd, etc.
            ProjectSetting(path,"xxxxxxx"); // Replace the following parameter TeamId with your own TeamID for automatic signature

            // Modify the Info.Plist file such as permissions version build
            InfoPlist(path,"1.0.0","20"); //The next two parameters are version and build

            //Replace native code files
            // ReplaceNativeCodeFile(path); //Replacement files such as xxx's UnityAppController.mm (with push-related code added) can also be implemented using the method in EditNativeCode

            // Modify part of the code in the native code file to append, insert and replace
            EditNativeCode(path); //Modify the code in the file. For example, in xxx's Preprocessor.h, change UNITY_USES_REMOTE_NOTIFICATIONS to 1 for push use.

        }
    }
    private static void ProjectSetting(string path,string teamId) {
        // Main official documents https://docs.unity3d.com/cn/2018.4/ScriptReference/iOS.Xcode.PBXProject.html
        string projPath = path + "/Unity-iPhone.xcodeproj/project.pbxproj";
        PBXProject proj = new PBXProject();
        proj.ReadFromFile(projPath);

        string target = proj.TargetGuidByName("Unity-iPhone");

        // Certificate settings automatic signature
        proj.SetBuildProperty(target, "CODE_SIGN_IDENTITY", "Apple Development");
        proj.SetBuildProperty(target, "CODE_SIGN_STYLE", "Automatic");
        proj.SetTeamId(target, teamId);

        // add extra framework(s)
        proj.AddFrameworkToProject(target, "AdSupport.framework", true);
        proj.AddFrameworkToProject(target, "CoreTelephony.framework", true);
        proj.AddFrameworkToProject(target, "StoreKit.framework", true); //In-app purchase is required otherwise PBXCapabilityType.InAppPurchase will not be added

        //XXXX's tersafe2.framework dynamic library needs to set Embed &Sign
        string defaultLocationInProj = Application.dataPath + "/Editor/Plugins/iOS"; //The path where the framework is stored
        const string coreFrameworkName = "tersafe2.framework"; // The file name of the framework
        string framework = Path.Combine(defaultLocationInProj, coreFrameworkName);
        string fileGuid = proj.AddFile(framework, "Frameworks/" + coreFrameworkName, PBXSourceTree.Sdk);
        PBXProjectExtensions.AddFileToEmbedFrameworks(proj, target, fileGuid);
        proj.SetBuildProperty(target, "LD_RUNPATH_SEARCH_PATHS", "$(inherited) @executable_path/Frameworks");

        // Add Capabilities - In-app Purchase
        proj.AddCapability(target, PBXCapabilityType.InAppPurchase);
        // Add Capabilities - Push
        string entitlement = Application.dataPath + "/Editor/AppleNative/Unity-iPhone.entitlements";
        File.Copy(entitlement, path + "/Unity-iPhone.entitlements");
        proj.AddCapability(target, PBXCapabilityType.PushNotifications,"Unity-iPhone.entitlements",true);

        // Custom resources such as image resources used in custom code, etc.
        // AddCustomResource(path,proj,target,"xxx.png");

        //Set BitCode
        proj.SetBuildProperty(target, "ENABLE_BITCODE", "false");
        
        // Set other link flags -ObjC
        proj.AddBuildProperty (target, "OTHER_LDFLAGS", "-ObjC");

        // add other files like *.tbd
        AddLibToProject(proj,target,"libc + + .1.tbd");
        AddLibToProject(proj,target,"libresolv.tbd");

        // save
        File.WriteAllText(projPath, proj.WriteToString());
    }
    //Add lib method
    private static void AddLibToProject(PBXProject proj, string target, string lib) {
        string file = proj.AddFile("usr/lib/" + lib, "Frameworks/" + lib, PBXSourceTree.Sdk);
        proj.AddFileToBuild(target, file);
    }
    // Add custom resources such as images used in custom code. The resources are placed in Assets/Editor/AppleNative
    private static void AddCustomResource(string path, PBXProject proj, string target, string fileName)
    {
        string customImagePath = Application.dataPath + "/Editor/AppleNative/" + fileName;
        File.Copy(customImagePath, path + "/" + fileName);
        string file = proj.AddFile(path + "/" + fileName, fileName , PBXSourceTree.Source);
        proj.AddFileToBuild(target, file);
    }
    //Modify Info.Plist
    private static void InfoPlist(string path,string version,string build) {
        string plistPath = path + "/Info.plist";
        PlistDocument plist = new PlistDocument();
        plist.ReadFromString(File.ReadAllText(plistPath));
        // Get root
        PlistElementDict infoDict = plist.root;

        // Version
        infoDict.SetString("CFBundleShortVersionString",version); //version
        infoDict.SetString("CFBundleVersion",build); //build

        //Permissions
        infoDict.SetString("NSLocationWhenInUseUsageDescription", "In order to discover friends around you, please allow the App to access your location"); //Geographical location
        infoDict.SetString("NSPhotoLibraryUsageDescription", "In order to select photos for upload, please allow the App to access your photo album"); //Album
        // infoDict.SetString("NSMicrophoneUsageDescription", "In order to be able to record sound, please allow the App to access your microphone permission"); //Microphone

        //Set BackgroundMode remote push
        PlistElementArray bmArray = null;
        if (!infoDict.values.ContainsKey("UIBackgroundModes"))
            bmArray = infoDict.CreateArray("UIBackgroundModes");
        else
            bmArray = infoDict.values["UIBackgroundModes"].AsArray();
        bmArray.values.Clear();
        bmArray.AddString("remote-notification");

        // Allow HTTP requests. If not set, only all HTTPS requests will be allowed.
        var atsKey = "NSAppTransportSecurity";
        PlistElementDict dictTmp = infoDict.CreateDict(atsKey);
        dictTmp.SetBoolean("NSAllowsArbitraryLoads", true);

        /*------If you want to set the scheme whitelist, please release this paragraph and add the whitelist you need to add. The following example is the whitelist required for WeChat sharing-----*/
        /*
        // Add whitelist scheme to open other apps. For example, to share to WeChat, wechat, weixin, and weixinULAPI need to be added.
        PlistElement array = null;
        if (infoDict.values.ContainsKey("LSApplicationQueriesSchemes"))
        {
            array = infoDict["LSApplicationQueriesSchemes"].AsArray();
        }
        else
        {
            array = infoDict.CreateArray("LSApplicationQueriesSchemes");
        }
        infoDict.values.TryGetValue("LSApplicationQueriesSchemes", out array);
        PlistElementArray Qchemes = array.AsArray();
        Qchemes.AddString("wechat");
        Qchemes.AddString("weixin");
        Qchemes.AddString("weixinULAPI");
        */

        // save
        File.WriteAllText(plistPath, plist.WriteToString());
    }
    //replace file
    private static void ReplaceNativeCodeFile(string path)
    {
        string UnityAppControllerMM = Application.dataPath + "/Editor/AppleNative/UnityAppController.mm";
        string tagUnityAppControllerMM = path + "/Classes/UnityAppController.mm";
        if (File.Exists(tagUnityAppControllerMM))
        {
            File.Delete(tagUnityAppControllerMM);
        }
        File.Copy(UnityAppControllerMM, tagUnityAppControllerMM);
    }
    //Modify some code
    private static void EditNativeCode(string filePath)
    {
        //Preprocessor.h file
        XClass Preprocessor = new XClass(filePath + "/Classes/Preprocessor.h");
 
        //Add a line of code after the specified code
        // Preprocessor.WriteBelowCode("The line of code before the position to be added","The code to be added");
 
        //Replace a line xxxxxx in the specified code to allow remote push
        Preprocessor.ReplaceString("#define UNITY_USES_REMOTE_NOTIFICATIONS 0","#define UNITY_USES_REMOTE_NOTIFICATIONS 1");
 
        //Add a line after the specified code
        // Preprocessor.WriteBelowCode("UnityCleanup();\\
}","- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url\r{\r return [ShareSDK handleOpenURL :url wxDelegate:nil];\r}");

        XClass AppController = new XClass(filePath + "/Classes/UnityAppController.mm");
        AppController.WriteBelow("#include <sys/sysctl.h>","\\
#import "JPUSHService.h"");
        AppController.WriteBelow("UnitySendDeviceToken(deviceToken);"," [JPUSHService registerDeviceToken:deviceToken];");
 
    }


    //Define file update class
    public partial class XClass : System.IDisposable
    {

        private string filePath;

        public XClass(string fPath) //Initialize the object through the file path
        {
            filePath = fPath;
            if( !System.IO.File.Exists( filePath ) ) {
                Debug.LogError( filePath + "The file does not exist, please check the path!" );
                return;
            }
        }
        //Replace some strings
        public void ReplaceString(string oldStr,string newStr,string method="")
        {
            if (!File.Exists (filePath))
            {

                return;
            }
            bool getMethod = false;
            string[] codes = File.ReadAllLines (filePath);
            for (int i=0; i<codes.Length; i + + )
            {
                string str=codes[i].ToString();
                if(string.IsNullOrEmpty(method))
                {
                    if(str.Contains(oldStr))codes.SetValue(newStr,i);
                }
                else
                {
                    if(!getMethod)
                    {
                        getMethod=str.Contains(method);
                    }
                    if(!getMethod)continue;
                    if(str.Contains(oldStr))
                    {
                        codes.SetValue(newStr,i);
                        break;
                    }
                }
            }
            File.WriteAllLines (filePath, codes);
        }

        //Insert code after a certain line
        public void WriteBelowCode(string below, string text)
        {
            StreamReader streamReader = new StreamReader(filePath);
            string text_all = streamReader.ReadToEnd();
            streamReader.Close();

            int beginIndex = text_all.IndexOf(below);
            if(beginIndex == -1){

                return;
            }

            int endIndex = text_all.LastIndexOf("\\
", beginIndex + below.Length);

            text_all = text_all.Substring(0, endIndex) + "\\
" + text + "\\
" + text_all.Substring(endIndex);

            StreamWriter streamWriter = new StreamWriter(filePath);
            streamWriter.Write(text_all);
            streamWriter.Close();
        }
        public void Dispose()
        {

        }
    }
}
#endif

Write at the end

We can see later whether we can directly add the script for automated packaging of ipa. In this case, Unity developers do not even need to open the Xcode development tool, and can directly upload it to internal testing or to the App Store for review and release.

Author: BoxJing
Link: https://www.jianshu.com/p/a25a60b9991e
Source: Jianshu
Copyright belongs to the author. For commercial reprinting, please contact the author for authorization. For non-commercial reprinting, please indicate the source.