読者です 読者をやめる 読者になる 読者になる

WonderPlanet DEVELOPER BLOG

ワンダープラネットの開発者ブログです。モバイルゲーム開発情報を発信。

Unity iOSビルド時にinfo.plistに設定を自動で登録する

今回のエンジニアブログを担当する加賀です。

UnityでiOSビルドを行うとXcodeプロジェクトが生成されるのですが、
その際にiOSのコードで使用する設定を、自動で登録するようにしてみました。
今回のクラスはUnityのPostProcessBuildで使用するクラスです。
また、今回のコードはUnity 4.5.0f6のProライセンスで確認しています。

この2種類の設定をinfo.plistに登録してみようと思います。

  1. 設定値
  2. URLスキーマ

.plistはXML形式で記述されているので、System.Xml内のクラスを使用して処理します。

using UnityEngine;  
using System.IO;  
using System.Xml;  
  
// info.plistの構成  
//  
//<?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>...</key>  
//    <string>...</string>  
//    ...  
//  </dict>  
//</plist>  
  
public class PlistMod {  
  // すべての設定の直接の親であるdictエレメントを取得  
  private static XmlNode FindPlistDictNode(XmlDocument doc) {  
    var cur = doc.FirstChild;  
    while (cur != null) {  
      if (cur.Name.Equals ("plist") && cur.ChildNodes.Count == 1) {  
        var dict = cur.FirstChild;  
        if (dict.Name.Equals ("dict")) {  
          return dict;  
        }  
      }  
      cur = cur.NextSibling;  
    }  
    return null;  
  }  
  
  // すでにそのkeyが存在しているか?  
  // dict:親ノード  
  private static bool HasKey(XmlNode dict, string keyName) {  
    var cur = dict.FirstChild;  
    while (cur != null) {  
      if (cur.Name.Equals ("key") && cur.InnerText.Equals (keyName)) {  
        return true;  
      }  
      cur = cur.NextSibling;  
    }  
    return false;  
  }  
  
  // 子エレメントを追加  
  // elementName:<...>の<>の中の文字列  
  // innerText:<key>...</key>のタグで囲まれた文字列  
  private static XmlElement AddChildElement(XmlDocument doc, XmlNode parent,  
                 string elementName, string innerText = null) {  
    var newElement = doc.CreateElement (elementName);  
    if (!string.IsNullOrEmpty (innerText)) {  
      newElement.InnerText = innerText;  
    }  
    parent.AppendChild (newElement);  
    return newElement;  
  }  
  
  // 指定したkeyに対応する値を更新する  
  // <key>KEY_TEXT</key>  
  // <ELEMENT_NAME>VALUE</ELEMENT_NAME>  
  // 以上の構造の場合のみ正常に動作  
  // key:KEY_TEXT  
  // elementName:ELEMENT_NAME  
  // value:VALUE  
  private static XmlNode UpdateKeyValue(XmlNode node, string key, string elementName, string value){  
    // まず<key>...</key>のノードを取得  
    var keyNode = GetChildElement (node, "key", key);  
    if (keyNode.NextSibling != null && keyNode.NextSibling.Name.Equals (elementName)) {  
      // 取得したkeyノードの次のノードのelementNameが指定された文字列だった場合、値を更新する  
      keyNode.NextSibling.InnerText = value;  
      return keyNode;  
    }  
    return null;  
  }  
  
  // 子エレメントを取得  
  // elementName:<...>の<>の中の文字列  
  // innerText:<key>...</key>のタグで囲まれた文字列  
  private static XmlNode GetChildElement(XmlNode node, string elementName, string innerText=null) {  
    var cur = node.FirstChild;  
    while (cur != null) {  
      if (cur.Name.Equals (elementName)) {  
        if ((innerText == null && cur.InnerText == null) ||  
            (innerText != null && cur.InnerText.Equals (innerText))) {  
          return cur;  
        }  
      }  
      cur = cur.NextSibling;  
    }  
    return null;  
  }  
  
  // info.plistのあるディレクトリパスと設定値を受け取り、info.plistに設定を登録する  
  public static void UpdatePlist(string path, string val) {  
    // info.plistを読み込む  
    string fullPath = Path.Combine (path, "info.plist");  
    var doc = new XmlDocument();  
    doc.Load (fullPath);  
  
    // すべての設定の直接の親であるdictエレメントを取得する  
    var dict = FindPlistDictNode (doc);  
    if (dict == null) {  
      Debug.LogError ("Error plistの解析に失敗 パス:" + fullPath);  
      return;  
    }  
  
    // 1. 設定値  
    // key:sample_key として登録します  
    //  
    // 登録後の例  
    // <key>sample_key</key>  
    // <string>val</string>  
    if(!HasKey (dict, "sample_key")) {  
      AddChildElement (doc, dict, "key", "sample_key");  
      AddChildElement (doc, dict, "string", val);  
    } else {  
      UpdateKeyValue (dict, "sample_key", "string", val);  
    }  
  
    // 2. URLスキーマ  
    // <key>CFBundleURLTypes</key>  
    // <array>  
    //   <dict>  
    //     <key>CFBundleURLName</key>  
    //     <string>BUNDLE_IDENTIFIER</string>  
    //     <key>CFBundleURLSchemes</key>  
    //     <array>  
    //       <string>BUNDLE_IDENTIFIER</string>  
    //     </array>  
    //   </dict>  
    //   ...  
    // </array>  
    {  
      XmlNode urlSchemeTop = null;  
      if (!HasKey (dict, "CFBundleURLTypes")) {  
        AddChildElement (doc, dict, "key", "CFBundleURLTypes");  
        urlSchemeTop = AddChildElement (doc, dict, "array");  
      } else {  
        //すでにkey:CFBundleURLTypesが存在している  
        //key:CFBundleURLTypesを取得  
        var urlScheme = GetChildElement (dict, "key", "CFBundleURLTypes");  
        urlSchemeTop = urlScheme.NextSibling;  
      }  
      //存在確認・更新  
      bool isExist = false;  
      foreach (XmlNode urlDict in urlSchemeTop.ChildNodes) {  
        if (urlDict.Name.Equals ("dict") && urlDict.HasChildNodes) {  
          //子がdict構造であり、更に子を持っている  
          var urlUrlName = GetChildElement (urlDict, "key", "CFBundleURLName");  
          if (urlUrlName != null && urlUrlName.NextSibling != null) {  
            //key:CFBundleURLNameの要素があり、その次の要素も存在する  
            var urlUrlString = urlUrlName.NextSibling;  
            if (urlUrlString.Name.Equals ("string") &&  
                urlUrlString.InnerText.Equals (PlayerSettings.bundleIdentifier)) {  
              //同じBundleIDの設定が見つかった  
              isExist = true;  
  
              //設定の上書き  
              urlUrlString.InnerText = PlayerSettings.bundleIdentifier;  
              break;  
            }  
          }  
        }  
      }  
      if (!isExist) {  
        //存在していない場合のみ追加  
        var urlSchemeDict = AddChildElement (doc, urlSchemeTop, "dict");  
        AddChildElement (doc, urlSchemeDict, "key", "CFBundleURLName");  
        AddChildElement (doc, urlSchemeDict, "string", PlayerSettings.bundleIdentifier);  
        AddChildElement (doc, urlSchemeDict, "key", "CFBundleURLSchemes");  
        var innerArray = AddChildElement (doc, urlSchemeDict, "array");  
        {  
          AddChildElement (doc, innerArray, "string", PlayerSettings.bundleIdentifier);  
        }  
      }  
    }  
  
    // 保存  
    doc.Save(fullPath);  
  
    // <!DOCTYPE の行を書き換えて保存してしまうため、修正する  
    string textPlist = string.Empty;  
    using (var reader = new StreamReader (fullPath)) {  
      textPlist = reader.ReadToEnd ();  
    }  
  
    // 本来の行が存在していれば処理終了  
    int fixupStart = textPlist.IndexOf ("<!DOCTYPE plist PUBLIC", System.StringComparison.Ordinal);  
    if (fixupStart <= 0) {  
      return;  
    }  
    int fixupEnd = textPlist.IndexOf ('>', fixupStart);  
    if (fixupEnd <= 0) {  
      return;  
    }  
  
    // 修正処理  
    string fixedPlist = textPlist.Substring (0, fixupStart);  
    fixedPlist += "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">";  
    fixedPlist += textPlist.Substring (fixupEnd+1);  
  
    using (var writer = new StreamWriter (fullPath, false)) {  
      writer.Write (fixedPlist);  
    }  
  }  
}  

95行目からの[UpdatePlist]関数が実装の本体です。

値を追加するときに[HasKey]関数を呼んで、keyが存在していない場合のみ
値を追加していますが、これは値の2重登録を防ぐためです。
info.plistはiOSビルド時に毎回作り直されるため、値の変更が反映されないことはありません。
keyが存在している場合は、再設定するようにすれば、値の変更が反映されます。

まとめ

ビルドするたびに毎回手動で設定し直すことは、非常に手間がかかり、時間が無駄になります。
設定する数が増えれば設定漏れも起きやすくなります。

自動で設定するようにコードを記述しておけば、そのような心配もないでしょう。

修正・変更点

2014/06/30
・すでにXcodeプロジェクトが存在している状態で再度ビルドすると、URLスキーマの設定項目が増えてしまうのを修正。
・すでにXcodeプロジェクトが存在している状態で再度ビルドすると、値が更新されないことがあるのを修正。
・StreamReader、StreamWriterの部分にusingを使用するように変更。