🚧

Cocoa の標準 API で XML を組み立てる

2022/01/02に公開

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 上の要素を表すルートクラスで設定するようです。(XMLDocumentXMLNode のサブクラス)

上記の例では以下のように 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