このエントリは Unity Advent Calendar 2015 の9日目の記事になります。
前日の記事は @fakestarbaby さんの「Visual Studio Code を利用して Unity アプリをデバッグする」でした。
ログ仕込んでデバッグするのもアリですが、やっぱりちゃんとブレークポイント仕込んでデバッグできると楽ですよね!
私も最近 Visual Studio Code 遣いになったので、ガンガン活用していこうかと思います!
前置き
Unity で iOS 向けにビルドした後に、 Info.plist
を編集する という作業が発生する人も多いのではないでしょうか?
例えば、
- ATS (App Transport Security) 関連の設定
- URL Schemes の設定
- オリエンテーションに関する設定を書き換え
…などなど。
この辺、毎回手動で書き換えるのは面倒なので、自動化しちゃいましょうってなお話です。
plist
って何よ?
本題に入る前に、まず plist
という形式について理解しておく必要があります。
成り立ちとかそういう小難しいコトは Wikipedia に説明を譲るとして、端的に言えば XML で記述されるデータ定義ファイル のコトです。
こいつには、
- そもそも XML である
- 型に応じたノードが定義されている
-
Dictionary
(連想配列) は<key>
ノードと対になる型ノードを連続して記述する
といった、クセがあって、サックリ書き換えようと思っても、なかなか難しかったりします。
実際に、iOS 出力した際に生成される Info.plist
をテキストエディタで開いてみれば、「嗚呼…面倒くさい…。」となるコト請け合いです。ええ。
汎用的に編集するための作戦
この面倒くさいフォーマットを攻略するための戦略を立ててみましょう。
-
System.Xml
のクラスを使うコトを前提とする - いわゆるスカラーなノードは単一のテキストノードとして取り扱う
-
array
ノードとdict
ノードはちょっと頑張る必要あり -
dict
ノードに於けるキーの重複は上書きで対応する- ただし、再帰的に潜れる場合は、末端のノードでのみ上書きを行う
-
array
ノードは原則的に追加のみ - ノードの削除も実装できるとオシャレ
この辺りを定義したパッチ的なファイルを読み込むことで、 plist
ファイルを編集できたら楽ですよね?
PlistMods
と、言うわけで作ってみました。
変更定義ファイル
.plistmods
という拡張子を持つ JSON
ファイルとして変更方法を定義してみました。
要点としては以下の通り。
-
{ "type": ..., "value": ... }
という1つ分の plist ノードを表現するためのObject
ノードを持つ -
type
の値に応じてvalue
ノードの型も変わる-
array
ならArray
ノードを取る -
dict
なら plist 的なkey
ノードの値を、そのままキーとしたObject
ノードの連想配列とする
-
-
dict
なノードのキーをハイフン始まりにすると、「該当のキーと、それに隣接する値ノードを削除する」とみなす
まぁ、サンプル見ればある程度理解はできるんじゃないかと。
{
"type": "dict",
"value": {
"-CFBundleIconFiles": null,
"-UISupportedInterfaceOrientations": null,
"-UISupportedInterfaceOrientations~ipad": null,
"UISupportedInterfaceOrientations": {
"type": "array",
"value": [
{
"type": "string",
"value": "UIInterfaceOrientationLandscapeLeft"
},
{
"type": "string",
"value": "UIInterfaceOrientationLandscapeRight"
}
]
},
"UISupportedInterfaceOrientations~ipad": {
"type": "array",
"value": [
{
"type": "string",
"value": "UIInterfaceOrientationLandscapeLeft"
},
{
"type": "string",
"value": "UIInterfaceOrientationLandscapeRight"
}
]
},
"NSAppTransportSecurity": {
"type": "dict",
"value": {
"NSExceptionDomains": {
"type": "dict",
"value": {
"kidsstar.tv": {
"type": "dict",
"value": {
"NSExceptionAllowsInsecureHTTPLoads": {
"type": "bool",
"value": true
},
"NSExceptionRequiresForwardSecrecy": {
"type": "bool",
"value": false
},
"NSIncludesSubdomains": {
"type": "bool",
"value": true
}
}
},
"cloudfront.net": {
"type": "dict",
"value": {
"NSExceptionAllowsInsecureHTTPLoads": {
"type": "bool",
"value": true
},
"NSExceptionRequiresForwardSecrecy": {
"type": "bool",
"value": false
},
"NSIncludesSubdomains": {
"type": "bool",
"value": true
}
}
}
}
}
}
},
"CFBundleURLTypes": {
"type": "array",
"value": [
{
"type": "dict",
"value": {
"CFBundleTypeRole": {
"type": "string",
"value": "Editor"
},
"CFBundleURLName": {
"type": "string",
"value": "tv.kidsstar.app.${PRODUCT_NAME:rfc1034identifier}"
},
"CFBundleURLSchemes": {
"type": "array",
"value": [
{
"type": "string",
"value": "tv.kidsstar.app.${PRODUCT_NAME:rfc1034identifier}"
}
]
}
}
}
]
}
}
}
.plistmods
の適用
んで、 .plistmods
な定義ファイルを読み込んで、 Info.plist
を編集する感じの動きになります。
まぁ、実際のコードを見てもらうのが早いです。
using UnityEngine;
using System.IO;
using System.Xml;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using MiniJSON;
namespace KidsStar.Editor.iOS {
public class PlistMods {
private XmlDocument plist;
public PlistMods(string path) {
this.plist = new XmlDocument();
this.plist.Load(path);
}
public void Apply(string pathPlistMods) {
IDictionary mods = (IDictionary)Json.Deserialize(File.ReadAllText(pathPlistMods));
// 第1階層が dict ノードであることを前提としている
IDictionary rootItem = (IDictionary)mods["value"];
XmlNode rootNode = this.plist.SelectSingleNode("/plist/dict");
foreach (object key in rootItem.Keys) {
this.Apply(rootNode, (string)key, (IDictionary)rootItem[key]);
}
}
public void Apply(string[] pathPlistModsList) {
foreach (string pathPlistMods in pathPlistModsList) {
this.Apply(pathPlistMods);
}
}
public void Apply(List<string> pathPlistModsList) {
this.Apply(pathPlistModsList.ToArray());
}
public void Save(string path) {
XmlDocumentType _documentType = this.plist.CreateDocumentType("plist", "-//Apple//DTD PLIST 1.0//EN", "http://www.apple.com/DTDs/PropertyList-1.0.dtd", null);
if (null != this.plist.DocumentType) {
this.plist.RemoveChild(this.plist.DocumentType);
}
this.plist.InsertAfter(_documentType, this.plist.FirstChild);
this.plist.Save(path);
}
private void Apply(XmlNode parent, string key, IDictionary item) {
// キーがハイフンで始まる場合、削除とみなす
if (!string.IsNullOrEmpty(key) && Regex.IsMatch(key, "^-")) {
key = Regex.Replace(key, "^-", string.Empty);
if (parent.HasKeyNode(key)) {
parent.RemoveChild(parent.GetKeyNode(key).NextSibling);
parent.RemoveChild(parent.GetKeyNode(key));
}
return;
}
if (null == item["type"] || null == item["value"]) {
Debug.LogError("処理対象のノードに type か value が含まれていません");
return;
}
switch ((string)item["type"]) {
case "bool":
this.ApplyScalar(parent, key, (bool)item["value"] ? "true" : "false");
break;
case "integer":
this.ApplyScalar(parent, key, "integer", (int)item["value"]);
break;
case "real":
this.ApplyScalar(parent, key, "real", (float)item["value"]);
break;
case "string":
this.ApplyScalar(parent, key, "string", (string)item["value"]);
break;
case "date":
this.ApplyScalar(parent, key, "date", (string)item["value"]);
break;
case "data":
this.ApplyScalar(parent, key, "data", (string)item["value"]);
break;
case "array":
this.ApplyArray(parent, key, (IList)item["value"]);
break;
case "dict":
this.ApplyDict(parent, key, (IDictionary)item["value"]);
break;
}
}
private void ApplyScalar(XmlNode parent, string key, string type, object value = null) {
// キーが空の場合、Array ノードへの挿入と見なす
if (string.IsNullOrEmpty(key)) {
if (null == value) {
parent.AppendChild(this.plist.CreateElement(type));
} else {
parent.AppendChild(this.plist.CreateSimpleTextNode(type, value.ToString()));
}
return;
}
if (parent.HasKeyNode(key)) {
parent.RemoveChild(parent.GetKeyNode(key).NextSibling);
} else {
parent.AppendChild(this.plist.CreateKeyNode(key));
}
if (null == value) {
parent.InsertAfter(this.plist.CreateElement(type), parent.GetKeyNode(key));
} else {
parent.InsertAfter(this.plist.CreateSimpleTextNode(type, value.ToString()), parent.GetKeyNode(key));
}
}
private void ApplyArray(XmlNode parent, string key, IList itemList) {
// キーが空の場合、Array ノードへの挿入と見なす
if (string.IsNullOrEmpty(key)) {
foreach (object item in itemList) {
this.Apply(parent, null, (IDictionary)item);
}
return;
}
if (!parent.HasKeyNode(key)) {
// キーがないなら、キーノードと array ノードを作る
parent.AppendChild(this.plist.CreateKeyNode(key));
parent.AppendChild(this.plist.CreateElement("array"));
}
foreach (object item in itemList) {
this.Apply(parent.GetKeyNode(key).NextSibling, null, (IDictionary)item);
}
}
private void ApplyDict(XmlNode parent, string key, IDictionary item) {
// キーが空の場合、Array ノードへの挿入と見なす
if (string.IsNullOrEmpty(key)) {
XmlNode dictNode = parent.AppendChild(this.plist.CreateElement("dict"));
foreach (object k in item.Keys) {
this.Apply(dictNode, (string)k, (IDictionary)item[k]);
}
return;
}
if (!parent.HasKeyNode(key)) {
// キーがないなら、キーノードと dict ノードを作る
parent.AppendChild(this.plist.CreateKeyNode(key));
parent.AppendChild(this.plist.CreateElement("dict"));
}
foreach (object k in item.Keys) {
this.Apply(parent.GetKeyNode(key).NextSibling, (string)k, (IDictionary)item[k]);
}
}
}
internal static class XmlExtension {
public static bool HasChildNode(this XmlNode self, string name, bool ignoreCase = true) {
return null != self.GetChildNode(name, ignoreCase);
}
public static XmlNode GetChildNode(this XmlNode self, string name, bool ignoreCase = true) {
foreach (XmlNode childNode in self.ChildNodes) {
if (childNode.Name == name || (ignoreCase && childNode.Name.ToLower() == name.ToLower())) {
return childNode;
}
}
return null;
}
public static bool HasKeyNode(this XmlNode self, string key) {
return null != self.GetKeyNode(key);
}
public static XmlNode GetKeyNode(this XmlNode self, string key) {
return self.SelectSingleNode(string.Format("./key[.=\"{0}\"]", key));
}
public static XmlNode CreateSimpleTextNode(this XmlDocument xml, string name, string text) {
XmlElement _node = xml.CreateElement(name);
_node.InnerText = text;
return _node;
}
public static XmlNode CreateKeyNode(this XmlDocument xml, string key) {
return xml.CreateSimpleTextNode("key", key);
}
}
}
MiniJSON を使っていますが、昨日 (2015/12/08) リリースされた Unity 5.3 であれば標準で JSON Serializer を搭載しているので、それ使ってあげるように修正しても良いかもです。
Save()
メソッドで DOCTYPE
ノードを再構築していますが、普通に保存すると何故か []
という記号が最後に挿入されてしまって、ビルドエラーをぶちかましてくれるので、泣く泣く強引な処理を噛ましています。
なお、現時点で、 data
ノードが正しく取り扱われない というバグがあります!
「対応したぜ!」って人が居れば、後述のサンプルプロジェクトにプルリク飛ばしてくださいw
PostProcessBuild での適用
この辺は、本題じゃないんで、サラッと流します。
using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
using System.IO;
using System.Collections.Generic;
namespace Sample.Editor {
public class PostProcessBuild {
[PostProcessBuild(200)]
public static void OnPostProcessBuild(BuildTarget target, string path) {
if (target != BuildTarget.iOS) {
return;
}
// 適用する .plistmods ファイルを掻き集める
List<string> files = new List<string>();
if (Directory.Exists(System.IO.Path.Combine(Application.dataPath, "Sample/PlistMods"))) {
files.AddRange(System.IO.Directory.GetFiles(System.IO.Path.Combine(Application.dataPath, "Sample/PlistMods"), "*.plistmods", System.IO.SearchOption.AllDirectories));
}
// .plistmods ファイルを適用する
string plistPath = Path.Combine(path, "Info.plist");
KidsStar.Editor.iOS.PlistMods plistMods = new KidsStar.Editor.iOS.PlistMods(plistPath);
plistMods.Apply(files);
plistMods.Save(plistPath);
}
}
}
PlistMods.Apply()
の引数として、
-
string
として、単一の.plistmods
へのパス -
string[]
として、複数の.plistmods
へのパス -
List<string>
として、複数の.plistmods
へのパス
というオーバーロードを用意してありますので、お好きな方法でどーぞ。
あ、ちなみに、 Info.plist
に限らず、 plist
形式ならどんなファイルでも行けるようにしてあるので、独自プロパティリストを使うような SDK とかをアレコレしたい場合なんかにも使えるんじゃないかな?
サンプルプロジェクト
github にて公開してあります。
こいつをそのまま取り込んで iOS 向けビルドしていただければ Assets/Sample/PlistMods/sample.plistmods
の変更定義に従って Info.plist が書き換わるハズです。
Unity のバージョンは 5.2.3f1 で確認済となります。
まとめ
実は、Unity 5 からは、 UnityEditor.iOS.Xcode
なるネームスペースの下に PlistDocument
というクラスが生えていて、こいつを駆使してあげれば同じようなことは実現出来るんですが、追加・変更・削除の処理そのものは、個別に実装する必要があるため、こんなモノを作ってみた次第です。
同じような感じで Android の AndroidManifest.xml
の編集とか、 iOS の .pbxproj
(Xcode の Build Settings とかの設定が定義されているファイル) の編集とかも実装してみたら面白いんじゃないでしょーか?
むしろ、誰か作って!w