序
JSON は非常に便利。軽量のデータを格納するには記述も便利だし(plist は XML なので記述は非常に面倒くさい…)Cocoa フレームワークも JSON のパーサがあるので自分で書かなくてもサードパーティのライブラリを導入しなくてもすぐ中身を取り出して扱えます。実にいい。
ただしそんな JSON ですが不便なところもあります。中身は必要なデータのみという思想で作られたか、コメントも入れられないし数式も入れられません。これじゃあ例えば何かの設定を作るときにこれは何のために使うのかとか、この数字はどういう意味を持ってるのかとかというのは JSON データを読んでもわからないのです。不便ですです。
というわけで JavaScript Object から JSON データを作りたいのだが、もちろん JavaScript 系のものなのでウェブで探せばそういったツールはいくらでもあります。ただ筆者の場合はちょうどたまたまこの iOS プロジェクトで使うデータを変換するための OS X のツールも作っているので、そのツールでついでに JSON も一緒に変換したいと考えました。だって同じプロジェクトでコンバーターを2つも使わないといけないとかダルいじゃん?
前準備
必要なものをとりあえずまとめておきます。まず当たり前だが JavaScript Object の定義ファイル。そしてそれを解読するための WebView
オブジェクト。OS X なので iOS と違い UIKit
ではなく、直接 WebKit
フレームワークをインポートする必要があります。あとは WebView Instance の stringByEvaluatingJavaScriptFromString
メソッドで JavaScript をパーシングし、得た結果を NSJSONSerialization
クラスで JSON string として吐き出す、と言った感じです。
JavaScript Object
具体的な文法は省きますが、まあ結構 Swift の配列/辞書構造に近いですです。違いというと Swift は辞書でも[]
を使いますが、JavaScript Object は配列は[]
だが辞書は{}
を使う、くらいですかね。あとは最後の要素後に,
を入れてはいけないとか文の最後に;
を入れなければならないとかといったまあちょっと Swift と比べると微妙に面倒なところですが、それは文句を言ってはいけません。そもそも JavaScript と Swift は生まれた時代が違います。新しいものがより便利になるのは当然であって決してそれを理由に古いものを罵ってはいけません。というわけで仮にこのようなオブジェクトを作るとしましょう:
var member = {
// 名前
"name": "Kotori",
// 年齢
"age": 16+1,
// 既婚か
"married": true,
// 関係者
"relations": {
// 旦那
"husband": "Umi",
// 嫁
"wife": "Honoka"
},
// 呼び名
"nicknames": [
"チュンチュン",
"(・8・)"
]
};
JSON.stringify(member);
ここで肝になっているのは最後の JSON.stringify(member);
です。先ほど言いましたが stringByEvaluatingJavaScriptFromString
でパーシングしているのでかならず JavaScript で何かしらの文字列を生成しなければなりません。この文がないと JavaScript は単純に一つのオブジェクトを作ってるだけでなにも返しません。
Playground で実際やってみよう
何度でも言うが Playground は偉大です。というわけで先ほど書いた JavaScript Object を実際ファイルに保存して Playground に入れましょう。
入れたら Playground に戻ってこの JavaScript Object を変換しましょう:
//: Playground - noun: a place where people can play
import Cocoa
// WebKit フレームワークも忘れないでね♥
import WebKit
// とりあえず WebView Instance を作る
let webView = WebView()
// JavaScript ファイルの URL を取得する
let jsURL = NSBundle.mainBundle().URLForResource("member_setting", withExtension: "js")!
// JavaScript ファイルの中身の文字列を取り出す
let jsString = String(contentsOfURL: jsURL, encoding: NSUTF8StringEncoding, error: nil)!
// JavaScript を WebView Instance でパーシングして結果である JSON 文字列を取得
let json = webView.stringByEvaluatingJavaScriptFromString(jsString)
// 得られた文字列を NSData に変換する
let jsonData = json.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!
// Playground なのでコードを省くがあとは jsonData をファイルに書き出す
と言った感じで最後は得られた jsonData を writeToFile: atomically:
辺りで書き出せばOKです。ちなみにこの jsonData から JSON ファイルとして読み込んで Swift 辞書に直すのは簡単です:
let memberSetting = NSJSONSerialization.JSONObjectWithData(data, options: .allZeros, error: nil) as! [String: AnyObject]
注意点
上記のスクリーンショットで気づいた人も居るかもしれませんが、実は Cocoa で作られた JSON データは、なんと、married
のところの、本来 Bool
型であるはずのデータを、Int
型として扱っているのです!これはどういう理由かというと、あくまで筆者の推測ですので保証はありませんが(むしろ正確な答えがわかってる方いるなら教えていただきたいです)、JSON は Object ですので、Cocoa
フレームワークでは NSValue
として保存しているのです。しかしこの NSValue
は Bool
用の型がなく、数値などと一緒に NSNumber
型で保存されています。だから値が true
にも関わらず NSNumber
なので数値として 1
を表示しています。
ただまあ解決法はあります。NSValue
オブジェうとは objCType
という属性があって、こいつはちゃんと値は実際どの型なのかという情報を保持しています。ちなみに直接に参照の仕方は Swift では用意されていませんが Bool 型は "c"
だということを覚えておけばあとは楽です:
let married: AnyObject = memberSetting["married"]!
if married is NSNumber {
let type = String.fromCString(married.objCType)
if type == "c" {
println("\"married\" is Bool")
}
}
これで万事解決です!
ちょっとしたカスタマイズ
上記の JavaScript Object は書き方として問題ないのですが、でも例えば実際 JavaScript がわからない人に中身を編集してもらう時、最初の var member = {
とか最後の JSON.stringify(member);
とかの文はイミワカンナイしキモチワルイから、中身だけを編集してもらいたいって気持ちは(筆者には)あります。
というわけでちょっと直してみましょう。JavaScript ファイルから要らないもの全部排除しておきます。ついでにもう JavaScript ファイルじゃなくなるから拡張子もおそらく .js から別のものにしたほうが良さそうです。まあテキストファイルだと思って編集しましょう。.txt に。
// 名前
"name": "Kotori",
// 年齢
"age": 16+1,
// 既婚か
"married": true,
// 関係者
"relations": {
// 旦那
"husband": "Umi",
// 嫁
"wife": "Honoka"
},
// 呼び名
"nicknames": [
"チュンチュン",
"(・8・)"
]
もちろん中身は JavaScript の一部として読み込むのでちゃんと JavaScript の文法通りにしないといけませんがまあそれは適当にこのファイルを編集するスクリプターに必要最低限のマニュアル用意してあげればいいでしょう。そして次はこれをちゃんと JavaScript として完成させるために、Swift のコードも改造しましょう:
// JavaScript ファイルの URL を取得する
let jsURL = NSBundle.mainBundle().URLForResource("member_setting", withExtension: "txt")!
// 不足している部分を作成
let prefix = "var member = {"
let suffix = "}; JSON.stringify(member);"
// JavaScript ファイルの中身の文字列を取り出す
let jsString = prefix + String(contentsOfURL: jsURL, encoding: NSUTF8StringEncoding, error: nil)! + suffix
これでスクリプターは最初と最後のワケガワカラナイ var
とかの命令を気にせず編集することができます。ただもちろんデメリットも有ります。ちゃんと JavaScript ファイルとして編集したほうが、出来上がったものをエディターの Validator で文法的に間違いがあるかどうかのチェックが出来るのですが中身だけ分離しちゃうとできなくなります。
余談
まあぶっちゃけなはなし、どうせもう JavaScript で書いちゃったんだから別に JSON で保存する必要なくね?NSArray
か NSDictionary
に格納したらそのまま書き出せば plist ファイルになるし問題ないっしょ?とか思ってるあなた、ツッコんだら負けです(爆
いや別にまあ確かにもう JavaScript で書いちゃったんだから JSON で保存する必要はないっちゃないんだけど、一手間増えるんじゃないですかヤダー
=========================追記 on May 21st 2015=========================
JSContext を使う方法
Facebook の友人のコメントで、WebView
ではなく直接 JavaScriptCore を使う方法もあると知りました。やり方に関しましては、まず WebView Instance の代わりに JSContext Instance を作ります
/*
// とりあえず WebView Instance を作る
let webView = WebView()
*/
// とりあえず JSContext Instance を作る
let jsContext = JSContext()
次に WebView
のパーシングの代わりに JSContext
でパーシング
/*
// JavaScript を WebView Instance でパーシングして結果である JSON 文字列を取得
let json = webView.stringByEvaluatingJavaScriptFromString(jsString)
*/
// JavaScript を JSContext Instance でパーシングして結果である JSON 文字列を取得
let json = jsContext.evaluateScript(jsString).toString()
ここで注意すべきことは evaluateScript:
で得る結果は JSValue
なので、それを String
に直すために toString()
メソッドを最後に追加することです。