Snippets

Danny Chung Enable Remote Push for UnityCloudBuild With xCode 8

Updated by Danny Chung

File Builder.cs Modified

  • Ignore whitespace
  • Hide word diff
 	}	
 	
 	/// <summary>
+	/// Retrieve PList item with given name from given XmlNode
+	/// 
+	/// From Everyplay's EveryplayPostprocessor.cs
+	/// </summary>
+	/// <returns>The plist item.</returns>
+	/// <param name="dict">Dict.</param>
+	/// <param name="name">Name.</param>
+	public static PListItem GetPlistItem(XmlNode dict, string name)
+	{
+		for (int i = 0; i < dict.ChildNodes.Count - 1; i++)
+		{
+			XmlNode node = dict.ChildNodes.Item(i);
+
+			if (node.Name.ToLower().Equals("key") && node.InnerText.ToLower().Equals(name.Trim().ToLower()))
+			{
+				XmlNode valueNode = dict.ChildNodes.Item(i + 1);
+
+				if (!valueNode.Name.ToLower().Equals("key"))
+				{
+					return new PListItem(node, valueNode);
+				}
+				else
+				{
+					Debug.Log("Value for key missing in Info.plist");
+				}
+			}
+		}
+
+		return null;
+	}	
+	
+	/// <summary>
 	/// Convert given path string to Mac path
 	/// </summary>
 	/// <returns>The to mac path.</returns>
Updated by Danny Chung

File Builder.cs Modified

  • Ignore whitespace
  • Hide word diff
 	#region Utility
 	
 	/// <summary>
+	/// Find dictionary for given UrlName
+	/// </summary>
+	/// <returns>The dictionary with URL name.</returns>
+	/// <param name="dictContainer">Dict container.</param>
+	/// <param name="urlName">URL name.</param>
+	private static XmlNode FindDictionaryForUrlName(XmlNode dictContainer, string urlName)
+	{
+		foreach (XmlNode dict in dictContainer.ChildNodes) {
+			if (dict.Name.ToLower ().Equals ("dict")) {
+				PListItem bundleUrlName = GetPlistItem (dict, "CFBundleURLName");
+				if (bundleUrlName != null &&
+					bundleUrlName.ItemValueNode.Name.Equals ("string") &&
+					bundleUrlName.ItemValueNode.InnerText.Equals (urlName))
+					return dict;
+			}
+		}
+
+		return null;
+	}	
+	
+	/// <summary>
 	/// Convert given path string to Mac path
 	/// </summary>
 	/// <returns>The to mac path.</returns>
Updated by Danny Chung

File Builder.cs Modified

  • Ignore whitespace
  • Hide word diff
 internal class Builder
 {
 
-    [PostProcessBuild(2000)]
-    public static void OnPostProcessBuild(BuildTarget target, string path)
-    {
-        #if UNITY_IOS
-            OnPostProcessBuild_iOS(target, path);
-        #endif
-    }
+	[PostProcessBuild(2000)]
+	public static void OnPostProcessBuild(BuildTarget target, string path)
+	{
+		#if UNITY_IOS
+		OnPostProcessBuild_iOS(target, path);
+		#endif
+	}
     
 	/// <summary>
 	/// Class to represent *.projmods file which is used in XCodeEditor
Created by Danny Chung

File Builder.cs Added

  • Ignore whitespace
  • Hide word diff
+internal class Builder
+{
+
+    [PostProcessBuild(2000)]
+    public static void OnPostProcessBuild(BuildTarget target, string path)
+    {
+        #if UNITY_IOS
+            OnPostProcessBuild_iOS(target, path);
+        #endif
+    }
+    
+	/// <summary>
+	/// Class to represent *.projmods file which is used in XCodeEditor
+	/// 
+	/// Reference: https://github.com/dcariola/XCodeEditor-for-Unity
+	/// </summary>
+	public class ProjectModsContent
+	{
+		/// <summary>
+		/// The pluginpath in Unity project. Parent directory is [UnityProject
+		/// </summary>
+		public string pluginpath;
+
+		/// <summary>
+		/// The copy dependencies into xCode project
+		/// </summary>
+		public bool copyDependencies;
+
+		/// <summary>
+		/// The dependency file list to be added into xCode project
+		/// </summary>
+		public List<string> dependencyList = new List<string> ();
+
+		/// <summary>
+		/// all files and folders will be parented to this group
+		/// </summary>
+		public string group;
+
+		/// <summary>
+		/// Gets the name of the projmod file.
+		/// </summary>
+		/// <value>The name of the proj mod file.</value>
+		public string projModFileName
+		{
+			get { return group + "XCode.projmods"; }
+		}
+
+		/// <summary>
+		/// Gets the framework search path for plugin.
+		/// </summary>
+		/// <value>The framework search path for plugin.</value>
+		public string frameworkSearchPathForPlugin
+		{
+			get 
+			{
+				return (copyDependencies ? 
+				"$(SRCROOT)/" + group : 
+					ConvertToMacPath (Path.Combine (Application.dataPath, PathWithPlatformDirSeparators (pluginpath))));
+			}
+		}
+
+		public List<string> patches = new List<string>();
+
+		/// <summary>
+		/// add libraries to build phase
+		/// </summary>
+		public List<string> libs = new List<string>();
+		public List<string> librarysearchpaths = new List<string>();
+
+		/// <summary>
+		/// add frameworks to the project
+		/// </summary>
+		public List<string> frameworks = new List<string>();
+
+		/// <summary>
+		/// Add weak frameworks to the project
+		/// </summary>
+		[JsonIgnore]
+		public List<string> weakframeworks = new List<string> ();
+
+		/// <summary>
+		/// Subject to be udpated by calling UpdateThenCreateProjModFile.
+		/// frameworkSearchPathForPlugin will be added
+		/// </summary>
+		public List<string> frameworksearchpaths = new List<string>();
+
+		/// <summary>
+		/// add header paths to build phase
+		/// </summary>
+		public List<string> headerpaths = new List<string>();
+
+		/// <summary>
+		/// add single files to the project
+		/// 
+		/// Subject to be udpated by calling UpdateThenCreateProjModFile
+		/// </summary>
+		public List<string> files = new List<string>();
+
+		/// <summary>
+		/// create a subgroup and add all files to the project (recursive)
+		/// </summary>
+		public List<string> folders = new List<string>();
+
+		/// <summary>
+		/// file mask to exclude
+		/// </summary>
+		public List<string> excludes = new List<string>();
+
+		/// <summary>
+		/// The build setting single value.
+		/// </summary>
+		public Dictionary<string, string> buildSettingSingleValue = new Dictionary<string, string> ();
+
+		/// <summary>
+		/// The build setting multiple value.
+		/// </summary>
+		// TODO: Not sure why we put JsonIgnore here
+		public Dictionary<string, List<string>> buildSettingMultipleValue = new Dictionary<string, List<string>>();
+
+		/// <summary>
+		/// The development team ID. Used for setting System capabilities, since it's required to be set for CI 
+		/// since XCode 8
+		/// </summary>
+		public string developmentTeamID = "";
+
+		/// <summary>
+		/// The system capabilities value. To enable or disable project system capabilities like following, since
+		/// it's required to be set for CI since XCode 8
+		/// 
+		/// SystemCapabilities = {
+		/// 	com.apple.Push = {
+		/// 		enabled = 1;
+		/// 	};
+		/// 	com.apple.Wallet = {
+		/// 		enabled = 0;
+		/// 	};
+		/// };
+		/// </summary>
+		public Dictionary<string, bool> systemCapabilitiesValue = new Dictionary<string, bool>();
+	}	
+	
+	/// <summary>
+	/// From Everyplay's EveryplayPostprocessor.cs
+	/// </summary>
+	public class PListItem
+	{
+		public XmlNode ItemKeyNode;
+		public XmlNode ItemValueNode;
+
+		public PListItem(XmlNode keyNode, XmlNode valueNode)
+		{
+			ItemKeyNode = keyNode;
+			ItemValueNode = valueNode;
+		}
+	}	
+	
+    public static void OnPostProcessBuild_iOS(BuildTarget target, string path)
+	{
+		// 1. Set projmods configuration
+		ProjectModsContent projmods = ProjectModsContent () {
+			group = "Ads",
+
+			// Values to update projmods
+			pluginpath = "Plugins/Ads",
+			copyDependencies = true,
+			dependencyList = new List<string> () {
+				"Chartboost.framework",
+				"GoogleMobileAds.framework",
+			},
+
+			patches = new List<string> (),
+
+			libs = new List<string> (),
+			librarysearchpaths = new List<string> (),
+
+			frameworks = new List<string> () {
+				"SafariServices.framework",
+				"GLKit.framework",
+			},
+
+            // NOTE BY DY: Can add more to modify projmod file of xCode
+			/*
+			weakframeworks = new List<string> (),
+
+			frameworksearchpaths = new List<string> (),
+
+			headerpaths = new List<string> (),
+
+			files = new List<string> (),
+
+			folders = new List<string> (),
+
+			excludes = new List<string> (),
+			*/
+
+			buildSettingSingleValue = new Dictionary<string, string>()
+			{
+				{ "ENABLE_BITCODE", "NO" },
+
+                /*
+                 * NOTE BY DY:
+                 * 
+                 * Required for OneSignal & UnityCloudBuild
+                 * 
+                 * Since xCode8, Build like UnityCloudBuild doesn't enable Push notification base on Provisioning's capability.
+                 * Thus, we got to modify projmod, plist and entitlement.
+                 * More detail of the issue is following, https://forum.unity3d.com/threads/ios-capabilities.380740/#post-2916100
+                 * */
+
+                #region Required for OneSignal & UnityCloudBuild
+
+                // Necessary to enable Push notification in iOS with building in CI like UnityClouldBuild. 
+                // It will also trigger to create .entitlements file which is XML format
+                { "CODE_SIGN_ENTITLEMENTS", "Unity-iPhone/ios.entitlements" }
+
+                #endregion
+            },
+
+            // NOTE BY DY: Can add more to modify projmod file of xCode
+            //buildSettingMultipleValue = new Dictionary<string, List<string>>();
+
+            #region Required for OneSignal & UnityCloudBuild
+
+            // Can find from the certification of distribution team between ( and )
+            developmentTeamID = "YOUR_TEAM_UDID_IN_CERTIFICATION", 
+
+			systemCapabilitiesValue = new Dictionary<string, bool>()
+			{
+				{ "com.apple.Push", true },
+                // Required by OneSignal. Need to modifiy plist, too
+				{ "com.apple.BackgroundModes", true }
+			}
+
+            #endregion
+        };
+
+		// 2. Update and Apply ProjectModsContent
+		UpdateProjModFileThenApply (path, projmods);
+
+		// 3. Update InfoPList
+		UpdateXCodeInfoPList (
+			path, 
+			new Dictionary<string, List<string>> () {
+				{ "NSCalendarsUsageDescription", new List<string>() { "Some Ad contents may access calendars" } },
+				{ "NSPhotoLibraryUsageDescription", new List<string>() { "Customer service may access photos" } },
+
+                #region Required for OneSignal & UnityCloudBuild
+
+                // Required by OneSignal
+                // NOTE BY DY: Empty string element is required to add string array with only one element
+                { "UIBackgroundModes", new List<string>() { "remote-notification", "" } },
+
+                #endregion
+
+                //{ "CFBundleURLTypes", new List<string>() { "Only for test" } },
+            }
+			// NOTE BY DY: Example of List<KeyValuePair<string, List<string>>> urlNameAndSchemeArrayPairList = null
+			/*
+			,new List<KeyValuePair<string, List<string>>>()
+			{
+				new KeyValuePair<string, List<string>>(
+					"testKey001",
+					null
+				),
+				new KeyValuePair<string, List<string>>(
+					"testKey002",
+					new List<string> {
+						"testUrlScheme001",
+						"testUrlScheme002",
+					}
+				),
+				new KeyValuePair<string, List<string>>(
+					"",
+					new List<string> {
+						"testUrlScheme003",
+						"testUrlScheme004",
+					}
+				),
+				new KeyValuePair<string, List<string>>(
+					"",
+					new List<string> {
+						"testUrlScheme005",
+					}
+				),
+			}
+			*/
+		);
+	}    
+	
+	/// <summary>
+	/// Update given ProjectModsContent.folders and ProjectModsContent.frameworksearchpaths, then apply to xCode project
+	/// </summary>
+	/// <param name="path">Path.</param>
+	/// <param name="projMod">Proj mod.</param>
+	public static void UpdateProjModFileThenApply(string path, ProjectModsContent projMod)
+	{
+		string modPath = Path.Combine (path, projMod.group);
+
+		if (Directory.Exists (modPath))
+			FileUtility.ClearDirectory (modPath, false);
+		else
+			Directory.CreateDirectory (modPath);
+
+		string pluginsPath = Path.Combine (Application.dataPath, PathWithPlatformDirSeparators (projMod.pluginpath));
+
+		projMod.folders.Clear (); // Will be updated depending on projMod.copydependencies
+
+
+		#region Modify pbxproj file
+
+		string projectPath = path + "/Unity-iPhone.xcodeproj/project.pbxproj";
+
+		// NOTE BY DY: Got from MoPubInternal.Editor.ThirdParty.xcodeapi.PBXProject
+		PBXProject pbxProject = new PBXProject ();
+		pbxProject.ReadFromFile (projectPath);
+
+		// NOTE BY DY: Since this is for Unity project, main target named is pretty much fixed as "Unity-iPhone"
+		string targetGUID = pbxProject.TargetGuidByName ("Unity-iPhone");
+
+		string dependencyTargetPath = projMod.copyDependencies ? modPath : pluginsPath;
+
+		string targetFile, source;
+
+		foreach (string dependencyFile in projMod.dependencyList) 
+		{
+			targetFile = Path.Combine (dependencyTargetPath, dependencyFile);
+			source = Path.Combine (pluginsPath, dependencyFile);
+
+			if (projMod.copyDependencies) 
+			{
+				try 
+				{
+					if (Directory.Exists (source))
+						FileUtility.DirectoryCopy (source, targetFile);
+					else if (File.Exists (source))
+						File.Copy (source, targetFile);
+
+					targetFile = Path.Combine(projMod.group, dependencyFile);
+					Debug.Log ("Copy & Add file - " + targetFile);
+					pbxProject.AddFileToBuild(targetGUID, 
+																	// NOTE BY DY: Got from MoPubInternal.Editor.ThirdParty.xcodeapi.PBXSourceTree
+						pbxProject.AddFile ( targetFile, targetFile, PBXSourceTree.Source));
+				} catch (System.Exception e) 
+				{
+					Debug.Log ("Unable to copy file or directory, " + e.Message);
+				}
+			} 
+			else 
+			{
+				targetFile = Path.Combine(projMod.group, dependencyFile);
+				Debug.Log ("Copy & Add file - s: " + source + " t: " + targetFile);
+				pbxProject.AddFileToBuild(targetGUID,
+					pbxProject.AddFile (source, targetFile, PBXSourceTree.Absolute));
+			}
+
+			projMod.files.Add (FileUtility.ConvertToMacPath (targetFile));
+		}
+
+		if (!projMod.frameworksearchpaths.Contains (projMod.frameworkSearchPathForPlugin))
+			projMod.frameworksearchpaths.Add (projMod.frameworkSearchPathForPlugin);
+
+		foreach (string framework in projMod.frameworks) {
+			Debug.Log("Add framework - " + framework);
+			pbxProject.AddFrameworkToProject (targetGUID, framework, false);
+		}
+		foreach (string weakframework in projMod.weakframeworks) {
+			Debug.Log("Add weakframework - " + weakframework);
+			pbxProject.AddFrameworkToProject (targetGUID, weakframework, true);
+		}
+		foreach (string frameworkSearchPath in projMod.frameworksearchpaths) {
+			Debug.Log("Add framework search path - " + frameworkSearchPath);
+			pbxProject.AddBuildProperty (targetGUID, "FRAMEWORK_SEARCH_PATHS", string.Format ("\"{0}\"", frameworkSearchPath));
+		}
+		foreach (string librarySearchPath in projMod.librarysearchpaths) {
+			Debug.Log("Add framework search path - " + librarySearchPath);
+			pbxProject.AddBuildProperty (targetGUID, "LIBRARY_SEARCH_PATHS", string.Format ("\"{0}\"", librarySearchPath));
+		}
+		foreach (string headerPath in projMod.headerpaths) {
+			Debug.Log("Add header path - " + headerPath);
+			pbxProject.AddBuildProperty (targetGUID, "HEADER_SEARCH_PATHS", string.Format ("\"{0}\"", headerPath));
+		}
+
+		foreach (KeyValuePair<string, string> pair in projMod.buildSettingSingleValue) {	
+			Debug.Log ("Set property - n: " + pair.Key + " v: " + pair.Value);
+			pbxProject.SetBuildProperty (targetGUID, pair.Key, pair.Value);
+		}
+
+		foreach (KeyValuePair<string, List<string>> pair in projMod.buildSettingMultipleValue) {
+			if (pair.Value == null || pair.Value.Count == 0) continue;
+
+			Debug.Log ("Set property - n: " + pair.Key + " v: " + pair.Value [0]);
+			pbxProject.SetBuildProperty (targetGUID, pair.Key, pair.Value [0]);
+			for (int i = 1; i < pair.Value.Count; i++) 
+			{
+				Debug.Log ("Add property - n: " + pair.Key + " v: " + pair.Value [i]);
+				pbxProject.AddBuildProperty (targetGUID, pair.Key, pair.Value [i]);
+			}
+		}
+
+		#region Setting system capabilities in pbxproj file
+
+		/*
+		 * NOTE BY DY: Couldn't figure out how to modify PBXProject section with PBXProject class.
+		 * So, premitively, export currenlty modified PBXProject, and then insert System capabliity relatives,
+		 * then import the string to PBXProject, again.
+		 * 
+		 * Origined from: https://teratail.com/questions/52234
+		 * */
+		if (projMod.systemCapabilitiesValue.Count > 0 && !string.IsNullOrEmpty (projMod.developmentTeamID)) {
+			string[] lines = pbxProject.WriteToString ().Split ('\n');
+			List<string> newLines = new List<string> ();
+			bool editFinish = false;
+			string line = "";
+			for (int i = 0; i < lines.Length; i++) {
+				line = lines [i];
+				if (editFinish)
+					newLines.Add (line);
+				else if (line.IndexOf ("isa = PBXProject;") > -1) {
+					while (line.IndexOf ("TargetAttributes = {") == -1) {
+						newLines.Add (line);
+						line = lines [++i];
+					}
+
+					newLines.Add (line); // Add "TargetAttributes = {"
+					newLines.Add (pbxProject.TargetGuidByName ("Unity-iPhone") + " = {");
+					newLines.Add (string.Format ("DevelopmentTeam = {0};", projMod.developmentTeamID));
+					newLines.Add ("SystemCapabilities = {");
+					foreach (KeyValuePair<string, bool> capability in projMod.systemCapabilitiesValue) {
+						Debug.Log ("System capability, " + capability.Key + " , enable: " + capability.Value);
+
+						newLines.Add (capability.Key + " = {");
+						newLines.Add (string.Format ("enabled = {0};", (int)(capability.Value ? 1 : 0)));
+						newLines.Add ("};");		
+					}
+
+					newLines.Add ("};");
+					newLines.Add ("};");
+					editFinish = true;
+				} else
+					newLines.Add (line);
+			}
+			
+			pbxProject.ReadFromString (string.Join ("\n", newLines.ToArray ()));
+		} else {
+			if (projMod.systemCapabilitiesValue.Count > 0 && string.IsNullOrEmpty (projMod.developmentTeamID))
+				Debug.LogWarning ("Must enter DevelopmentTeamID to enable or disable SystemCapabilities");
+		}
+
+		#endregion Setting system capabilities in pbxproj file
+
+
+		Debug.Log ("Finish to update pbxproj at " + projectPath);
+		pbxProject.WriteToFile (projectPath);
+
+		#endregion Modify pbxproj file
+
+
+		// Required for OneSignal & UnityCloudBuild
+		#region Create or modify .entitlement
+
+		/*
+		 * Sample .entitlement>>
+		 * 
+		 * <?xml version="1.0" encoding="UTF-8"?>
+		 * <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+		 * <plist version="1.0">
+		 * <dict>
+		 *  <key>aps-environment</key>
+		 *  <string>development</string>
+		 * </dict>
+		 * </plist>
+		 * */
+		if (projMod.buildSettingSingleValue.ContainsKey ("CODE_SIGN_ENTITLEMENTS")) {
+
+			string filePath = Path.Combine (path, projMod.buildSettingSingleValue["CODE_SIGN_ENTITLEMENTS"]);
+			XmlDocument xmlDoc = new XmlDocument ();
+			XmlNode dict = null;
+			if (File.Exists (filePath)) {
+				xmlDoc.Load (filePath);
+				dict = xmlDoc.SelectSingleNode ("plist/dict");
+			} else {
+				XmlDeclaration declaration = xmlDoc.CreateXmlDeclaration ("1.0", "UTF-8", null);
+				xmlDoc.InsertBefore (declaration, xmlDoc.DocumentElement);
+				XmlElement plist = xmlDoc.CreateElement ("plist");
+				XmlAttribute attr = xmlDoc.CreateAttribute ("version");
+				attr.Value = "1.0";
+				plist.Attributes.Append (attr);
+				xmlDoc.AppendChild (plist);
+				dict = plist.AppendChild (xmlDoc.CreateNode (XmlNodeType.Element, "dict", ""));
+			}
+
+			PListItem item = GetPlistItem (dict, "aps-environment");
+			if (item == null) {
+				XmlElement key = xmlDoc.CreateElement ("key");
+				key.InnerText = "aps-environment";
+				dict.AppendChild (key);
+				XmlElement str = xmlDoc.CreateElement ("string");
+				str.InnerText = "development";
+				dict.AppendChild(str);
+
+				Debug.Log ("Finished to generate entitlement file at the path: " + filePath);
+
+				xmlDoc.Save (filePath);
+			}
+				
+			// Remove extra garbage added by the XmlDocument save
+			FileUtility.UpdateStringInFile (filePath, "dtd\"[]>", "dtd\">");
+		}
+
+		#endregion Create or modify .entitlement
+	}	
+	
+	/// <summary>
+	/// Update xCode Info.plist with given keyValuePairs
+	/// 
+	/// Example of KeyValuePairs
+	/// <code>
+	/// new Dictionary<string, string> () {
+	/// 	{ "NSCalendarsUsageDescription", "Some Ad contents may access calendars" },
+	/// 	{ "NSPhotoLibraryUsageDescription", "Customer service may access photos" },
+	/// }
+	/// </code>
+	/// 
+	/// Example of urlNameAndSchemeArrayPairList
+	/// <code>
+	/// new List<KeyValuePair<string, List<string>>>() {
+	/// 	new KeyValuePair<string, List<string>>(
+	/// 		"testKey001",
+	/// 		null
+	/// ),
+	/// new KeyValuePair<string, List<string>>(
+	/// 	"testKey002",
+	/// 	new List<string> {
+	/// 		"testUrlScheme001",
+	/// 		"testUrlScheme002",
+	/// 	}
+	/// ),
+	/// new KeyValuePair<string, List<string>>(
+	/// 	"",
+	/// 	new List<string> {
+	/// 		"testUrlScheme003",
+	/// 		"testUrlScheme004",
+	/// 	}
+	/// ),
+	/// }
+	/// </code>
+	/// </summary>
+	/// <param name="path">Path.</param>
+	/// <param name="keysAndValues">Keys and values.</param>
+	public static void UpdateXCodeInfoPList(
+		string path, 
+		Dictionary<string, List<string>> keysAndValues, 
+		List<KeyValuePair<string, List<string>>> urlNameAndSchemeArrayPairList = null)
+	{
+		if (keysAndValues == null || keysAndValues.Count == 0)
+			return;
+
+		// Fix if there are problems on the given path
+		path = FixProblemInPathToBuildProject (path);
+
+		try {
+			string filePath = Path.Combine (path, "Info.plist");
+			if (!File.Exists (filePath)) {
+				return;
+			}
+
+			// NOTE BY DY: Got from System.Xml
+			XmlDocument xmlDoc = new XmlDocument ();
+			xmlDoc.Load (filePath);
+
+			XmlNode dict = xmlDoc.SelectSingleNode ("plist/dict");
+			if (dict != null) {
+				PListItem item = null;
+				foreach (KeyValuePair<string, List<string>> pair in keysAndValues) {
+					item = GetPlistItem (dict, pair.Key);
+
+					// Url types
+					if(pair.Key.Equals("CFBundleURLTypes"))
+					{
+						if(urlNameAndSchemeArrayPairList != null && urlNameAndSchemeArrayPairList.Count >0)
+						{
+							if(item == null)
+							{
+								XmlElement key = xmlDoc.CreateElement("key");
+								key.InnerText = "CFBundleURLTypes";
+
+								XmlElement array = xmlDoc.CreateElement("array");
+								item = new PListItem(dict.AppendChild(key), dict.AppendChild(array));
+							}
+								
+							for(int i = 0; i < urlNameAndSchemeArrayPairList.Count; i++)
+							{
+								AddUrlScheme(xmlDoc, item.ItemValueNode, urlNameAndSchemeArrayPairList[i].Key, urlNameAndSchemeArrayPairList[i].Value); 
+							}
+						}
+					}
+					// Normal element types
+					else
+					{
+						if (item == null)
+						{
+							if(pair.Value.Count > 0)
+							{
+								XmlElement key = xmlDoc.CreateElement ("key");
+								key.InnerText = pair.Key;
+								dict.AppendChild (key);
+
+								// Append string typed Xml element
+								if(pair.Value.Count == 1)
+								{
+									XmlElement str = xmlDoc.CreateElement ("string");
+									str.InnerText = pair.Value[0];
+									dict.AppendChild(str);
+								}
+								// Append string array typed Xml element
+								else
+								{
+									XmlElement array = xmlDoc.CreateElement("array");
+									XmlElement str = null;
+									for(int i = 0; i < pair.Value.Count; i++)
+									{
+										if(string.IsNullOrEmpty(pair.Value[i])) continue;
+
+										str = xmlDoc.CreateElement("string");
+										str.InnerText = pair.Value[i];
+										array.AppendChild(str);
+									}
+
+									dict.AppendChild (array);
+								}
+							}
+							else
+								Debug.LogWarning("Don't have values for Key: " + pair.Key);
+						}
+						else // overwrite value
+						{
+							if(pair.Value.Count > 0)
+							{
+								if(item.ItemValueNode.Name.ToLower().Equals("string"))
+									item.ItemValueNode.InnerText = pair.Value[0];
+								else if(item.ItemValueNode.Name.ToLower().Equals("array"))
+								{
+									item.ItemValueNode.RemoveAll();
+
+									XmlElement str = null;
+									for(int i = 0; i < pair.Value.Count; i++)
+									{
+										if(string.IsNullOrEmpty(pair.Value[i])) continue;
+
+										str = xmlDoc.CreateElement("string");
+										str.InnerText = pair.Value[i];
+										item.ItemValueNode.AppendChild(str);
+									}
+								}
+								
+							}
+							else
+								Debug.LogWarning("Don't have values for Key: " + pair.Key);
+						}
+					}
+				}
+
+				xmlDoc.Save (filePath);
+			
+				// Remove extra garbage added by the XmlDocument save
+				UpdateStringInFile (filePath, "dtd\"[]>", "dtd\">");
+			} else {
+				Debug.Log ("Info.plist is not valid");
+			}
+		} catch (System.Exception e) {
+			Debug.Log ("Unable to update Info.plist: " + e.Message);
+		}
+	}	
+	
+	/// <summary>
+	/// From Everyplay's EveryplayPostprocessor.cs
+	/// </summary>
+	/// <param name="xmlDoc">Xml document.</param>
+	/// <param name="dictContainer">Dict container.</param>
+	/// <param name="urlScheme">URL scheme.</param>
+	private static void AddUrlScheme(XmlDocument xmlDoc, XmlNode dictContainer, string urlName, List<string> urlSchemes)
+	{
+		XmlNode dict = null;
+
+		// If UrlName is required to be added
+		if (!string.IsNullOrEmpty (urlName)) {
+			if (!CheckIfUrlNameExists (dictContainer, urlName)) {
+				// If it doesn't exist, add URLName
+				dict = xmlDoc.CreateElement ("dict");
+
+				XmlElement key = xmlDoc.CreateElement ("key");
+				key.InnerText = "CFBundleURLName";
+
+				XmlElement str = xmlDoc.CreateElement ("string");
+				str.InnerText = urlName;
+
+				dict.AppendChild (key);
+				dict.AppendChild (str);
+
+				dictContainer.AppendChild (dict);
+			} 
+			else
+				// If it existed, get dictionary contains the UrlName
+				dict = FindDictionaryForUrlName (dictContainer, urlName);
+		}
+
+		// If UrlSchemes are required to be added
+		if (urlSchemes != null && urlSchemes.Count > 0) 
+		{
+			if (dict == null) 
+			{
+				dict = xmlDoc.CreateElement ("dict");
+				dictContainer.AppendChild (dict);
+			}
+
+			PListItem bundleUrlSchemes = GetPlistItem(dict, "CFBundleURLSchemes");
+			XmlNode array = null;
+			if (bundleUrlSchemes == null) {
+				XmlElement key = xmlDoc.CreateElement ("key");
+				key.InnerText = "CFBundleURLSchemes";
+
+				array = xmlDoc.CreateElement ("array");
+				dict.AppendChild (key);
+				dict.AppendChild (array);
+			} 
+			else 
+			{
+				// If the array has existed,remove all children
+				array = bundleUrlSchemes.ItemValueNode;
+				array.RemoveAll ();
+			}
+
+			// Updatae 
+			for (int i = 0; i < urlSchemes.Count; i++) 
+			{
+				XmlElement str = xmlDoc.CreateElement ("string");
+				str.InnerText = urlSchemes [i];
+				array.AppendChild (str);
+			}
+		}
+	}	
+	
+	
+	#region Utility
+	
+	/// <summary>
+	/// Convert given path string to Mac path
+	/// </summary>
+	/// <returns>The to mac path.</returns>
+	/// <param name="path">Path.</param>
+	public static string ConvertToMacPath(string path)
+	{
+		return path.Replace (@"\", "/");
+	}
+	
+	/// <summary>
+	/// Paths the with platform dir separators.
+	/// </summary>
+	/// <returns>The with platform dir separators.</returns>
+	/// <param name="path">Path.</param>
+	public static string PathWithPlatformDirSeparators(string path)
+	{
+		if (Path.DirectorySeparatorChar == '/')
+		{
+			return path.Replace("\\", Path.DirectorySeparatorChar.ToString());
+		}
+		else if (Path.DirectorySeparatorChar == '\\')
+		{
+			return path.Replace("/", Path.DirectorySeparatorChar.ToString());
+		}
+
+		return path;
+	}	
+	
+	/// <summary>
+	/// Clears the directory.
+	/// </summary>
+	/// <param name="path">Path.</param>
+	/// <param name="deleteParent">If set to <c>true</c> delete parent.</param>
+	public static void ClearDirectory(string path, bool deleteParent)
+	{
+		if (path != null)
+		{
+			string[] folders = Directory.GetDirectories(path);
+
+			foreach (string folder in folders)
+			{
+				ClearDirectory(folder, true);
+			}
+
+			string[] files = Directory.GetFiles(path);
+
+			foreach (string file in files)
+			{
+				File.Delete(file);
+			}
+
+			if (deleteParent)
+			{
+				Directory.Delete(path);
+			}
+		}
+	}	
+	
+	public static string FixProblemInPathToBuildProject(string pathToExam)
+	{
+		if (pathToExam.StartsWith ("./")) // Fix two erroneous path cases on Unity 5.4.f03
+			pathToExam = Path.Combine (Application.dataPath.Replace ("Assets", ""), pathToExam.Replace ("./", ""));
+		else if (pathToExam.Contains ("./"))
+			pathToExam = pathToExam.Replace ("./", "");
+
+		return pathToExam;
+	}	
+	
+	/// <summary>
+	/// Replaced every occurance of string subject to string replacement
+	/// This method will delete the file with the filePath once, then create after replacing strings
+	/// 
+	/// From Everyplay's EveryplayPostprocessor.cs
+	/// </summary>
+	/// <param name="filePath">File path.</param>
+	/// <param name="subject">Subject to be replaced</param>
+	/// <param name="replacement">Replacement</param>
+	public static void UpdateStringInFile(string filePath, string subject, string replacement)
+	{
+		try
+		{
+			if (!File.Exists(filePath))
+			{
+				return;
+			}
+
+			string processedContents = "";
+
+			using (StreamReader sr = new StreamReader(filePath))
+			{
+				while (sr.Peek() >= 0)
+				{
+					string line = sr.ReadLine();
+					processedContents += line.Replace(subject, replacement) + "\n";
+				}
+			}
+
+			File.Delete(filePath);
+
+			using (StreamWriter streamWriter = File.CreateText(filePath))
+			{
+				streamWriter.Write(processedContents);
+			}
+		}
+		catch (System.Exception e)
+		{
+			Debug.Log("Unable to update string in file: " + e);
+		}			
+	}	
+	
+	#endregion
+}
HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.