エンジニア

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

投稿日:2014年6月28日 更新日:

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

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を使用するように変更。

採用情報

ワンダープラネットでは、一緒に働く仲間を幅広い職種で募集しております。

-エンジニア
-

© WonderPlanet Inc.