Galapagos Engineering Blog

株式会社ガラパゴス エンジニアチームによるブログです。

Swift 4でJSONの扱い

ガラパゴスのコードヒーヨアン(twitter: @luinily)です。

先月WWDCApple社のOS、SDKなどの新バージョンか公開されました。その中にSwift 4も発表されて、その中にJSONの扱いに関するツールが追加されたことがわかりました。

弊社では、サーバーとのやりとりを行うアプリの開発が珍しくありませんが、その際JSONを扱うことが多く、扱いやすくなると仕事がしやすくなります。 今回の記事ではSwift 4で導入される新しいJSON用のツールを紹介したいと思います。

JSONとは

JSONは軽量なデータ記述言語の1つです。JavaScriptから生まれ、サーバーとアプリの情報交換などによく使われるようになり、多くの言語で扱うツールが存在します。 下記のようなJSONの形式は人間に可読なテキストファイルです。

{
    "firstName": "オルシュファン",
    "lastName": "グレイストーン",
    "age": 28,
    "address": {
        "place": "ドラゴンヘッドt",
        "region": "クルザス中央地",
        "state": "イシュガルド",
    }
}

フォーマットの詳細は「JSON - Wikipedia」を参考してください。

今までの扱い

今までJSONを扱うときは下記のような扱いになっていた:

  1. JSONデータをData形式で取得します。
  2. そのデータを[String: Any]に変換します。
  3. ディクショナリーの各キーにあるデータを正しく変換してデータを取り出す。

難しいことはしていませんが、任意なキーがないときや、オブジェクトがあるときなど、いろんな場合に対しての処理を、変換処理に加えて対応する必要がありますので、実装が時間をかかってしまって、馬具しやすいコードになります。そのために現在いろんなライブラリーが存在していますが、SwiftやiOS SDKで正式なライブラリーがないため、プロジェクトによって違うライブラリーを使ったり、使用していたライブラリーの開発が止まったりますし、ライブラリーを使うことでうまく変換できないときデバッグが難しくなったりします。

Swift 4で追加されるCodable

Swift 4ではCodableというプロトコールとJSONDecoderというクラスが追加されます。それを使えば簡単いJSONの扱いができるようになります。 紹介するためにCodableとJSONDecoderを使う例を実装しようと思います。

JSON Feed

JSON Feedは5月に公開されたRSSのようなブログの更新情報を持つフォーマット。RSSXMLに比べて、JSONで定義されていて扱いやすい、データ量の軽いフォーマットだと思われます。 5月に発表されていて、すでに複数のブログまとめサービスなどで対応されています。実装は結構簡単なので今回の例にちょうどいいと思います。詳しいことは公式サイトで確認できます。

JSON FeedのJSONファイルの簡単な例を見ましょう

{
    "version": "https://jsonfeed.org/version/1",
    "title": "My Example Feed",
    "home_page_url": "https://example.org/",
    "feed_url": "https://example.org/feed.json",
    "items": [
        {
            "id": "2",
            "content_text": "This is a second item.",
            "url": "https://example.org/second-item"
        },
        {
            "id": "1",
            "content_html": "<p>Hello, world!</p>",
            "url": "https://example.org/initial-post"
        }
    ]
}

フェードの中にはversion、title、home_page_url、feed_urlのフィードの情報があります。 最後にitemsという配列の中には各記事の情報があります:id、url、テキストフォーマットでのコンテンツ、またはHTMLフォーマットでのコンテンツ このファイルを取得してブログのフィードの情報と、各記事の方法を取得できます。

アプリの中に、このJSONをどうすれば、Swiftの情報に変換できるか見て見ましょう。

データを入れるための構造体を作成します

まずはこのデータを入れるための構造体を書きます。

struct JsonFeed {
    var version: URL
    var title: String
    var homePage: URL?
    var feed: URL?
    var items: [JsonFeedItem]
}

version、homePage、feedはURLで定義します。JSON Feedの定義にはhomePageとfeedが任意だと書いてありますので、入ってない場合に備えてオプショナルの「URL?」として定義します。titleはそのままStringとして定義して、itemsは記事の情報の配列なので、配列として定義します。 記事の情報を持つために同じく構造体を作成します。

struct JsonFeedItem {
    var id: String
    var url: URL?
    var contentHTML: String?
    var contentText: String?
}

ここはフィードの方と同じく、任意の項目をオプショナルとして定義します。

JSONのデータを構造体に入れる

ここが関心のところです、CodableとJSONDecoderを使用し、作った構造体にJSONが持っているデータを入れます。 手順は簡単です。 まずは構造体にCodableを追加します。

struct JsonFeed: Codable {
    var version: URL
    var title: String
    var homePage: URL?
    var feed: URL?
    var items: [JsonFeedItem]
}

struct JsonFeedItem: Codable {
    var id: String
    var url: URL?
    var contentHTML: String?
    var contentText: String?
}

そうしたら、JSONDecoderを使用し、変換をします。

let decoder = JSONDecoder()
let feed = try? decoder.decode(JsonFeed.self, from: data)

変換が成功した場合、JsonFeed型のfeedにデータが入っています。それだけです!

キーのマッチング

ただし、このまま変換すると変換が失敗してしまいます。 理由は構造体のメンバーにJSONの定義と違う名称をつけたからです。 JSONで「home_page_url」となっているところを「homePage」とSwiftっぽい定義しました。JSONDecoderはメンバーとJSONのキーの名称が同じだと自動的に入れてくれますが、名称が違う場合、どのキーがどのメンバーになるのか教える必要があります。

実はCodableプロトコルを適用すると構造体の中に暗黙的にCodingKeysという列挙型が作成されます。この列挙型を上書きすることで、構造体のメンバーにどのキーを適用させるかのを定義できます。 JsonFeedとJsonFeedItemでCondingKeysを定義しましょう。

struct JsonFeed: Codable {
    var version: URL
    var title: String
    var homePage: URL?
    var feed: URL?
    var items: [JsonFeedItem]

    enum CodingKeys: String, CodingKey {
        case version
        case title
        case homePage = "home_page_url"
        case feed = "feed_url"
        case items
    }
}

struct JsonFeedItem: Codable {
    var id: String
    var url: URL?
    var contentHTML: String?
    var contentText: String?

    enum CodingKeys: String, CodingKey {
        case id
        case url
        case contentHTML = "content_html"
        case contentText = "content_text"
    }
}

列挙型は「enum CodingKeys: String, CodingKey」として定義をします。構造体の全てのメンバーのcaseを追加する必要があります。 メンバーとキーの名称が同じであれば、メンバーと同じ名称のcaseだけを定義します メンバーとキーの名称が異なる場合は、メンバーと同じ名称のcaseを定義して、そのcaseにキーの名称をStringとして定義します。 この修正を適用するとJSONデータの変換が成功します。

まとめ

CodableとJSONDecoderを使う場合、JSONの変換に必要な手順は

  1. JSONの情報を持つための構造体を作成する。
  2. その構造体にCodableプロトコルを定義する。
  3. 構造体のメンバーの名称がJSONのキーの名称と異なる場合。CodingKeysの列挙型を定義する
  4. JSONDecodeのインスターンスを作成する
  5. Dataとしてもつ JSONデータをJSONDecode.decodeで変換させる

構造体の定義は以前でも必要がありましたが、CodableとJSONDecoderのおかげで変換コードが2行だけになりました。自作のアプリにCodableを適用して見たら、80行ぐらいあった変換コードがたったの2行になりました。

Codable関連はWWDC「What’s new in Foundation」で発表されました。 9月からSwift 4がリリースされたら、業務で使うのが楽しみです。

UIScrollViewのスクロール方向を扱いやすくしてみる

こんにちは、iOSチームの本柳です。

UIScrollViewのスクロールしている方向を取扱いたいことって割りとありますよね?

通常はscrollViewWillBeginDraggingでスクロール開始位置のcontentOffsetを保存しておいて、scrollViewDidScrollで現在のcontentOffsetと保存しておいたcontentOffsetを比較して方向を取るみたいなことをするかと思います*1

しかし、この方法、Viewにスクロール開始時の状態を増やしてしまったり、スクロール方向を決める処理の実装が煩雑だったりと個人的にあまり好きではありません。

今回は、別のアプローチでスクロール方向を取得する方法を考えてみました。

KVOを利用してcontentOffsetを監視する

ScrollViewがスクロールするとcontentOffsetが更新されます。

なので、contentOffsetを監視すればscrollDidScrollと同じようにスクロールした時というのを取得することが可能となります。

監視するための処理をextensionを利用してUIScrollViewに拡張しましょう。

extension ScrollDetectable where Self: UIScrollView {

    var scrollDetectableKeyPath: String { return "contentOffset" }

    /// contentOffsetの監視を始める
    func enableScrollDirectionDetect() {
        var _self = self
        _self.scrollObserved = true
        addObserver(self,
                    forKeyPath: scrollDetectableKeyPath,
                    options: [.new, .old],
                    context: nil)
    }

    /// contentOffsetの監視をやめる
    func disableScrollDirectionDetect() {
        if scrollObserved {
            var _self = self
            _self.scrollObserved = false
            removeObserver(self, forKeyPath: scrollDetectableKeyPath)
        }
    }

}

KVOを利用して監視を行うと、プロパティの変更前、変更後の値を取得出来るのでスクロールの開始位置を保存しておく必要がなくなります。

スクロール方向は型で欲しい

スクロール方向は型として取り扱いたいですよね?

ということでスクロール方向を型にしてみます。

// 横方向のスクロール状態
enum ScrollDirectionX { case none, lead, tail }

// 縦方向のスクロール状態
enum ScrollDirectionY { case none, top, bottom }

そういうケースがあるか分からないのですが、斜め方向へのスクロールのことも考慮して縦と横のスクロールで型を分けるようにしました。

スクロール方向をstructとして定義する

スクロール方向に関する情報は一つのオブジェクトとして取り扱えるとプログラムを書きやすいですよね。

struct ScrollDirection {

    /// 直前のcontentOffset
    let old: CGPoint

    /// 現在のcontentOffset
    let new: CGPoint

    /// 横スクロール方向
    var directionX: ScrollDirectionX {
        if old.x > new.x {
            return .lead
        } else if old.x < new.x {
            return .tail
        } else {
            return .none
        }
    }

    /// 縦スクロール方向
    var directionY: ScrollDirectionY {
        if old.y > new.y {
            return .top
        } else if old.y < new.y {
            return .bottom
        } else {
            return .none
        }
    }

    init(old: CGPoint, new: CGPoint) {
        self.old = old
        self.new = new
    }

}

イニシャライザで新旧のcontentOffsetを渡してあげるとdirectionYdirectionXからスクロール方向を取得できるようになります。

ScrollDirectionを生成するための処理を定義

最後にUIScrollViewを編集してスクロール方向を取得する処理を定義すれば完成です!

var scrollObserved: Bool = false

override func observeValue(forKeyPath keyPath: String?,
                           of object: Any?,
                           change: [NSKeyValueChangeKey : Any]?,
                           context: UnsafeMutableRawPointer?) {
    if keyPath == scrollDetectableKeyPath, let _change = change {
        guard
            let old = (_change[.oldKey] as? NSValue)?.cgPointValue,
            let new = (_change[.newKey] as? NSValue)?.cgPointValue else {
            fatalError("Undefined KVO key.")
        }

        let direction = ScrollDirection(old: old, new: new)
        print(direction.directionY)
        print(direction.directionX)
    }
}

適当なタイミングで、enableScrollDirectionDetectdisableScrollDirectionDetectを呼び出してあげればいい感じにスクロール方向を取得出来るようになります。


ということで、スクロール方向をいい感じに取得する方法を考えてみました。

なんだかもっといい方法がありそうなのですけどね(^_^;)

ということで、ガラパゴスではもっといい方法でiOSプログラムを書いていける!という腕自慢のエンジニアを絶賛募集中です!

www.glpgs.com

たくさんのご応募お待ちしております。

*1:良い 方法をご存知の方は是非教えて下さい!

PhoenixでElmしてみる

ご機嫌よう、奢侈文弱なガラパゴスのおとめです。

ガラパゴスでは社内勉強会というものが毎週開催されているのですが、その席で、Elm推しのナイスミドルで格好いい本柳さんが「PhoenixとElm連携できます(`・ω・´)キリッ」と発表されていましたので、今回はそれを試してみます。

この記事はPhoenix Framework 1.3-rc.1を対象にしています。1.2以前はディレクトリ構造などが異なります。また、PhoenixやElmのインストールなどは済んでいるものとします。

$ iex -v
Erlang/OTP 19 [erts-8.3] [source-d5c06c6] [64-bit] [async-threads:10] [hipe] [kernel-poll:false]

IEx 1.4.1
$ mix phx.new -v
Phoenix v1.3.0-rc.1
$ node -v
v6.10.3
$ elm -v
0.18.0

おさらい:Phoenix FrameworkとElm

もちろんこのblogをご覧の皆様には説明の必要などないと思われますが、Phoenix Frameworkは、RailsErlangのいいとこ取りをしようといったWebフレームワークで(1.3でRails Wayとは一線を画す素敵な方向に進んだことはこのblogでも触れました)、一方のElmは乱立するAltJSの戦いで瀕死のフロントエンドエンジニアにもたらされた希望です。

因みにですが、PhoenixのデフォルトではフロントエンドにはES6が使われます。

まとめ

いきなりですがまとめますと、Phoenix Frameworkで使用しているbrunchにElm対応の拡張を入れると、それだけでフロントエンドはElmで書くことができます。ほら、簡単でしょう?

アセット管理のおはなし

簡単でしょう? ……??

何を言っているのかちょっと意味がわかりませんね。Elm対応の拡張を入れる? なに言うてはりますのん?

その前にbrunchについてお話ししましょう。brunchはフロントエンドのビルドツールで、Phoenixではデフォルトのアセット管理ツールとして採用されています。アセットというのはJavascriptCSSなど、Viewのナカミのように動的に生成されるわけではないリソースのことですが、例えばLESSやSCSSで書いたものはCSSコンパイルしたり、Javascriptはサイズを圧縮したりしますね? そういったことを行うためのツールです。行うと言っても実際には意識することはほとんどなくて、アプリケーションの実行時にフレームワークがよしなにそういうことをしてくれるのですが。

さて、ここまでのおはなしで、諸兄諸姉にはもう、アセットというものは静的で、変更後一回だけ生成すればよく、しかしその一回は確実に実行されなければならない、ということがお分かりかと思います。

ところで、例えばES6ならイマドキのモダンなブラウザなら普通に使うことができます−−できるはずですね?−−が、Elmの場合はJavascriptに変換する必要も出てきます。トランスパイルですね。もちろん、Elmで書いて手動でトランスパイルして、出てきたJavascriptをbrunch(などのビルドツール)で使う、ということもできますが、面倒じゃなくてですか? もしも万が一手順書のようなものに従って何かするとしたら、トランスパイルしたり、そうして出力されたファイルを決まった場所にコピーしたり、といった「手順」はいかにも省略されそうじゃなくてですか? 人が行うなら、確実に実行される、という要求を満たせそうにありません。

そんなわけですので、ソースツリー上では普通にElmを書いて、トランスパイル以下略などは自動的によろしくして頂きたいですね。

elm-brunchを使ってみる

もちろんシェルスクリプトなどを書いてCIに組み込んでも良いのですが(それはそれでローカルデバッグの時面倒そうですが)、もっと朗報があります。Elmに対応したツールを使えば良いのです。はじめの方でお話ししたように、brunchにElm対応の拡張を入れましょう。

ここにelm-brunchがありんす。早速インストールしましょう。

$ npm install --save-dev elm-brunch

Phoenixプロジェクト上にElmのためのフォルダを作って関連するパッケージをインストールします。

$ mkdir assets/elm
$ cd assets/elm
$ elm package install elm-lang/html

せっかくelmディレクトリにいることですし、早速Elmを書いていきましょう(テキストに「ご機嫌よう世界」と出力するだけの簡単なものです)。この時、App.elmというファイル名にすると、トランスパイルして出力されたファイルが既存のapp.jsを上書きしてしまうので注意しましょう。今回はElmApp.elmという名前にしてみました。

module ElmApp exposing (..)

import Html exposing (Html, text)

main : Html msg
main =
  text "ご機嫌よう世界"

さて、もちろん現時点ではまだbrunchの設定などは行なっていませんので、このままではトランスパイルなどは自動では行われません。そこで、brunchの設定をします。

まず、assets/brunch-config.jsに、elm-brunchに関する設定を書きます。brunchが監視しているフォルダに"elm"を追加するのもお忘れなく。

elmBrunchの設定でelmFolderを指定する設定方法をよく見るのですが、それでは動かなかったので、ここではmainModulesでパスも指定しています。

...
  paths: {
    // Dependencies and current project directories to watch
    watched: ["static", "css", "js", "vendor", "elm"],
    // Where to compile files to
    public: "../priv/static"
  },

  // Configure your plugins
  plugins: {
    elmBrunch: {
      mainModules: ["elm/ElmApp.elm"],
      outputFolder: "../assets/js",
      makeParameters: ["--debug"]
    },
    babel: {
      // Do not use ES6 compiler in vendor code
      ignore: [/vendor/]
    }
  },
...

依存関係にもElm関連を指定する必要がありますので、assets/package.jsonに追加します。

...
  "devDependencies": {
    "babel-brunch": "6.0.6",
    "brunch": "2.10.7",
    "elm": "^0.18.0",
    "elm-brunch": "~0.8.0",
    "clean-css-brunch": "2.10.0",
    "css-brunch": "2.10.0",
    "uglify-js-brunch": "2.1.1"
  }
...

この段階で、一旦アセットパイプラインを動かして、Elmがトランスパイルされることを確認してみましょう。Phoenixでbrunchなアセットパイプラインを手動で実行するには以下のようにします。問題がなければ、このように完了メッセージが表示されるはずです。

$ node node_modules/brunch/bin/brunch build
14:56:35 - info: compiled 66 files into 2 files, copied 3 in 2.8 sec
Elm compile: elm/ElmApp.elm, to ../assets/js/elmapp.js

ところで先ほど書いたElmApp.elmはどこでどう動くのでしょう?

assets/brunch-config.jsに、outputFolder: "../assets/js"と書きました。これは、トランスパイルしたファイルの出力先の指定になります。アセットパイプラインを実行した際のコンソールログからもこのことはお分かりいただけますね? でも、このままではアプリケーションにロードされませんので、どこかに書く必要があります。

どこかに……?

ElmApp.elmは、単に「ご機嫌よう世界」と出力するだけなのを思い出してください。とりあえず適当な要素にでも出してみましょう。

というわけで、elmというIDの要素に出力してみます。assets/js/app.jsにElmAppを使います的な感じで、要素を書き換えるJavascriptを記述します。

...
import "phoenix_html"
import Elm from "./elmapp.js"
...
const elmElement = document.querySelector("#elm")
const elmApp = Elm.ElmApp.embed(elmElement)

次に、出力先の要素を適当に用意します。プロジェクトを作ったばかりの場合はインデックスページ(lib/YOUR_APP_NAME/web/templates/page/index.html.eex)しかありませんが、ここに追加してしまいましょう。

...
  <div id="elm"></div>
...

ではサーバを起動してみます。

$ mix phx.server

するとこのように、無事にご機嫌よう世界できました。

f:id:glpgsinc:20170517153955p:plain

elmapp.jsがありません、というようなエラーになる場合は、app.jsの該当の行を一旦コメントアウトしてアセットパイプラインを実行してください。これは、elmのトランスパイルがjsのコンパイルよりも後に行われることで発生します。一度トランスパイルしてしまえば大丈夫です。トランスパイルを必ず先に行うこともできると思うのですが、この記事ではそこまで突っ込みません

さいごに

突然ですがガラパゴスではサーバーサイドエンジニアを募集していますガラパゴスで実際にPhoenix Frameworkを採用したプロジェクトはまだ一つしかありませんが、推しを共有できる仲間が欲しいですのん、というわけで興味がおありの方は是非に。皆さまの応募お待ちしています。

では、ご機嫌よう。

この記事は業務の一環として業務時間中に書きました