Cocoa の標準 API で XML を組み立てる
Apple Platform 向けのアプリケーション開発で扱う xcworkspace の中身の XML ファイルを Swift + macOS な環境でプログラム上から生成しました。そのときに Xode 自身が生成するファイルと同等のファイルを生成するために必要だった作業をここにメモとして記載します。
使ったもの
macOS では Fundation Framework に含まれている XML 関連のクラス群。
- XMLDocument
- XMLElement
- XMLNode
などを使用しました。外部ライブラリなどには依存しない標準機能のみで実装しました。
生成したい XML は以下のようなものです。
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Sample/Package">
</FileRef>
<FileRef
location = "group:Sample/iOS.xcodeproj">
</FileRef>
<FileRef
location = "group:Sample/macOS.xcodeproj">
</FileRef>
</Workspace>
これと同等のものを生成しました。
Step1. XML 宣言の出力
1 行目も <?xml version="1.0" encoding="UTF-8"?>
の部分です。
地味に方法を探るのに 1 番時間がかかりました。僕が行き着いた答えはこちらです。
let doc = XMLDocument()
doc.version = "1.0"
doc.characterEncoding = "UTF-8"
print(doc.xmlString)
<?xml version="1.0" encoding="UTF-8"?>
XMLDocument
を生成して version
, characterEncoding
の各プロパティに値をセットしてあげるだけです。これらのプロパティは XMLDocument
だけに存在しています。
Step2. Workspace タグ
ルートの要素として Workspace
というタグがあります。version
というアトリビュートも存在しています。まずは、Workspace
タグだけ出力できるようにしていきます。
let element = XMLElement(name: "Workspace")
let attr = XMLNode(kind: .attribute)
attr.name = "version"
attr.stringValue = "1.0"
element.addAttribute(attr)
print(element.xmlString)
<Workspace version="1.0"></Workspace>
僕の勝手なイメージで、要素を作ってそれに対して Key/Value な感じの API が付いているのかと思っていました。実際はアトリビュートも XMLNode
という XML 上の要素を表すルートクラスで設定するようです。(XMLDocument
の XMLNode
のサブクラス)
上記の例では以下のように XMLNode
の種別を .attribute
にし、名前と値を別々で設定していました。
let attr = XMLNode(kind: .attribute)
attr.name = "version"
attr.stringValue = "1.0"
これらを一行で実行できる class func も存在していて
XMLNode.attribute(withName: "version", stringValue: "1.0")
という感じで実行できます。
こちらの方が行数的にも短くてわかりやすいのですが、Objective-C 由来の古い API の宿命なのか、このメソッドの戻り値の型が Any
になっています。ここで生成したインスタンスを要素にアトリビュートとして設定するために element.addAttribute(attr)
としないといけません。この addAttribute()
メソッドの引数が XMLNode
であることを期待しているので as!
などでキャストする必要が出てしまいます。僕の好みとしては、長くなるけどキャストしなくてもよい前者のやり方の方が好みです。
話が逸れましたが、Step1. で生成した Document にこの Workspace 要素を追加してみます。Workspace は XML のルート要素なので setRootElement()
メソッドを使います。ここまでの全体として以下のようなコードになります。
// Document の生成
let doc = XMLDocument()
doc.version = "1.0"
doc.characterEncoding = "UTF-8"
// Workspace 要素の生成
let root = XMLElement(name: "Workspace")
let attr = XMLNode(kind: .attribute)
attr.name = "version"
attr.stringValue = "1.0"
root.addAttribute(attr)
// Workspace 要素を Document のルートにセット
doc.setRootElement(root)
print(doc.xmlString)
<?xml version="1.0" encoding="UTF-8"?><Workspace version="1.0"></Workspace>
Step3. FileRef 要素を追加
ここまでできれば今までやってきた操作の組み合わせで実装できますね。FileRef 要素自体は以下のようなコードで生成できます。
let element = XMLElement(name: "FileRef")
let attr = XMLNode(kind: .attribute)
attr.name = "location"
attr.stringValue = "group:Sample/Package"
element.addAttribute(attr)
<FileRef location="group:Sample/Package"></FileRef>
全体像として以下のようになります。FileRef 要素を追加するのはルートとなる Workspace 要素です。addChild()
メソッドで子要素として追加します。
// Document の生成
let doc = XMLDocument()
doc.version = "1.0"
doc.characterEncoding = "UTF-8"
// Workspace 要素の生成
let root = XMLElement(name: "Workspace")
let attr = XMLNode(kind: .attribute)
attr.name = "version"
attr.stringValue = "1.0"
root.addAttribute(attr)
// Workspace 要素を Document のルートにセット
doc.setRootElement(root)
let fileRefElement = XMLElement(name: "FileRef")
let locationAttr = XMLNode(kind: .attribute)
locationAttr.name = "location"
locationAttr.stringValue = "group:Sample/Package"
fileRefElement.addAttribute(attr)
// FileRef 要素を Workspace 要素の子要素として追加
root.addChild(fileRefElement)
print(doc.xmlString(options: [.nodePrettyPrint]))
<?xml version="1.0" encoding="UTF-8"?>
<Workspace version="1.0">
<FileRef location="group:Sample/Package"></FileRef>
</Workspace>
ちょっと出力が長くなってきたので、print するときの文字列化部分に PrettyPrint のオプションを付けるようにしています。
あとは、必要な要素分繰り返すだけです。
まとめ
普段の開発では JSON をデコードするくらいしかやらないので XML のような構造化されたデータを構築しないので少し苦戦しました。ググってもこのような XML の組み立てをやっている記事はほとんどなかったので苦労しましたが分かってしまうと簡単でした。
注意点として、今回使っている XMLDocument などの DOM に関する API は iOS 向けには利用できません。僕の場合は macOS 向けのコマンドラインツールを開発する過程で必要になったのでこれで問題ありませんでしたが、iOS で似たようなことをしたい場合は libxml などをラップしたサードパーティーライブラリを使ったほうが良さそうです。
Discussion