5.1さらうどん

@giginetの技術ブログ。ゲーム開発、iOS開発、その他いろいろ

UITextFieldに簡単にカスタムキーボード貼るライブラリ作った+iOS UIライブラリのCI改善の話

最近、Swiftでライブラリを作るのが趣味なgiginetです。

最近は仕事でもSwiftを使う機会が多く、金曜の夜に仕事でアプリを作っていたら、突如良い感じの実装になってきたので、週末のやっていきでライブラリが誕生した。

github.com

CustomKeyboardTextField

CustomKeyboardTextField は、カスタムキーボードが貼り付いたテキストフィールドを簡単に生成できるライブラリ。

デモアプリはAppetize.ioに上げてあり、↓で動作確認ができる。

こんな感じで作った動作デモをシュッとブログに埋め込んで使ってもらえるの最高の時代だし、実機実行はもはや時代遅れだから今すぐiPhoneを投げ捨てるべき。

アピールポイントとして、Genericsなどを使ってゴリゴリしているので、簡単に型安全なTextFieldのクラスを生成できる。 よく使うであろうPickerを貼るだけのTextFieldであれば、以下のように一瞬で実装できる。

import CustomKeyboardTextField

struct PokemonPickerKeyboardDataSource: UIPickerViewKeyboardDataSource {
    let elements = ["Bulbasaur", "Charmander", "Squirtle"]
}
typealias PokemonPickerTextField = PickerKeyboardTextField<PokemonPickerKeyboardDataSource>

typealiasで簡単に新たなTextField型を生み出せるのがミソで、上記のように最初の3匹を選ばせたいときなんかは役に立つと思う。

サンプルアプリにあるように、画像を埋め込んだちょっとリッチなピッカーにしたり、一番下の雑ゲームパッドのように完全に独自のUIViewを埋め込むこともできる。

実用的な例で言うと、例えばiOS上でユーザー登録フォームを作っていて、誕生日や性別の選択などのフォームを実装したいという時なんかに使えそう。詳しくはREADMEをどうぞ。

気に入ったら是非 ⭐️ スターをお願いします 🙏

UIライブラリを作ってみて新たな知見

最近はSwiftライブラリ作成マンとして活動しつつあるため、ライブラリの作り方などは何度かブログ記事に書いた。

giginet.hateblo.jp

giginet.hateblo.jp

しかし、UI作るのが面倒であまり好きではなくて今まで作ってこなかったので、今回はUIライブラリ用の知見を何個か紹介したい。

Travis CI+Fastlaneで自動的にAppetize.ioにデプロイする

冒頭に埋め込んだ動作デモはAppetize.ioという、ブラウザ上でiOS/Androidアプリを実行できる素晴らしいサービスを使っている。

今回は、Travis CIでサンプルアプリをビルドして、Fastlaneを使って、自動的にAppetize.ioにアップロードするようにしてみた。

実は以前、この仕組みはこのブログでも紹介していて、今回も以前送ったPRが元になっている。

fastlaneを使ってiOSアプリをブラウザから爆速確認できるようにした - 5.1さらうどん

今回改めて調べてみたら、この記事を書いた時代からいろいろと進化していて

  • Appetize.ioのAPIが進化していて、直接アプリをアップロードできるようになった
    • 以前はどこかにアップロードしておいてURLを参照する形だった
  • Fastlaneにzipアクションやbuild_and_upload_to_appetizeというそのまんまなアクションが追加された

これらを使うと、わずかこれだけでシュッと実装できてしまう。

platform :ios do
  desc "Deploy demo application to Appetize.io"
  lane :deploy_to_appetize do
    build_and_upload_to_appetize(scheme: 'DemoApp', xcodebuild: {project: 'DemoApp.xcodeproj'})
  end
end

これを使ってFastlaneを実行するだけで↑みたいなのが簡単にデプロイできる。

$ fastlane deploy_to_appetize

他に必要なこととしてTravis CIには、セキュアな値を暗号化して環境変数にできる仕組みが実装されていて、それを使ってAPIキー各種をセットしておく必要がある。

$ travis encrypt APPETIZE_API_TOKEN=yourapitoken --add
$ travis encrypt APPETIZE_PUBLICKEY=yourpublickey --add

今回はデフォルトで用意されたアクションを使わずにgitのコミットログを一緒に送るようにしていたりとか若干改良している。 興味のある方はFastfileをどうぞ。

Xcode7.xと8系で同時にテストする

この時期はWWDC後で、現行のバージョンとβ版両方を考えないといけないiOS開発者にとって憂鬱な時期。

とはいえ、以前と比べて大体のことはいろいろな手段で解決できるようになっていて、Travis CIのmatrixの仕組みを使えば複数のコンテナを立ち上げてXcode7と8で同時にテストを実行できる。

f:id:gigi-net:20160808010714p:plain

これを利用してSwift 2.2と2.3両方に対応している。

たまに標準ライブラリの型が変わっていたりして、トリッキーなコードを書く必要があったりするけど、2.2と2.3はほとんど文法的に互換性を保っているので、Xcode8.0で設定を済ませるだけで大体の場合は簡単に2.3対応が行える。

今後3が出てきてからどのように複数環境でテストしていくかという知見はまだない。 ただ、2.x系は今後早々と切り捨てられそうなので、近いうちに3以外は考えなくても良い世界が到来しそう。

UIライブラリのユニットテスト

UI系のライブラリはテストを書きづらく、テストがない場合も多いんだけど、申し訳程度にユニットテストを書いた。

本来はXCUITestなどを使って、しっかりUIテストを書くのが望ましいんだろうけど、面倒でそこまでちゃんと書いていない。 気が向いたらUI Testも増やしてみたい。

感想

ライブラリをいろいろ作ってみて、Protocol Orientedな設計が大分わかってきた気になってきたけど、実は良くわかってなくてまだまだ勉強が足りていないのを痛感する。

他に作ったライブラリも含め、興味があったらぜひご利用ください。

github.com

github.com

github.com

XIBを読み込んで良い感じにViewを生成できるライブラリ作った

Macアプリを作ってて、XIBからNSViewを生成したいだけだったのになかなか面倒だったので簡単にできる奴を作った。 似たようなものはありそうなのに、探してみたら意外とちゃんと使える奴が少なかった。

github.com

自分で定義したCustomViewクラスとCustomView.xibを作っておけば、以下のようにするだけで簡単にViewが生成できる。 Genericsを使っているのでカスタムビューのクラスで取得できる。

let customView: CustomView = try! CustomView.view(withOwner: self)
customView.label.text = "Hello World"
self.view.addSubView(customView)

UIKitにもCocoaにも対応しているので、UIViewでもNSViewでも同じように使える。ついでにtvOSでも使える。

クラスを作るまでもなく、ただUIViewのXIBを読みたいだけという用途の場合、自分でファイル名も指定できる。

let customView: UIView = try! UIView.view(
    fromNibNamed: "MyCustomView", 
    owner: self, 
    bundle: NSBundle(forClass: self.dynamicType)
)

気合入れて大きなライブラリを作らなくても、こういうスニペットレベルのものの方がむしろ汎用性が高くてよさそう。

知見

本当に小さいライブラリだし、Swiftライブラリ作成の知見は前回書いてしまったので、他に特に書くことはない。

giginet.hateblo.jp

一つ言えることは、最初はNibLoaderというライブラリ名だったんだけど、CocoaPodsに既に同名のライブラリがホストされてて、完成段階になって泣きながら名前を変えるハメになって大変面倒だったので、作り始める前は下調べをするべき。

どうぞご利用ください。

SwiftでJSONのテストを良い感じにするJSONMatcher作った

この度、JSONMatcherというSwift向けのテストライブラリを開発しました。

github.com

これは何?

SwiftでJSONのオブジェクトや文字列を検査するマッチャーです。

JSONオブジェクトのテストがこんな感じで簡単に書けます。

import XCTest
import Nimble
import JSONMatcher

class ExampleTestCase: XCTestCase {
    func testComplexExample() {
        expect([
            "name" : "Snorlax",
            "no" : 143,
            "species" : "Sleeping",
            "type" : ["normal"],
            "stats" : [
                "hp" : 160,
                "attack" : 110,
                "defense" : 65,
                "special_attack" : 65,
                "special_defense" : 65,
                "speed" : 30
            ],
            "moves" : [
                ["name" : "Tackle", "type" : "normal", "level" : 1],
                ["name" : "Hyper Beam", "type" : "normal", "level" : NSNull()],
            ]
        ]).to(beJSONAs([
            "name" : "Snorlax",
            "no" : Type.Number, // value type matching
            "species" : try! NSRegularExpression(pattern: "[A-Z][a-z]+", options: []), // regular expression matching
            "type" : ["[a-z]+".regex], // shorthands for NSRegularExpression
            "stats" : [
                "hp" : 160,
                "attack" : 110,
                "defense" : 65,
                "special_attack" : 65,
                "special_defense" : 65,
                "speed" : 30
            ],
            "moves" : [
                ["name" : "Tackle", "type" : "[a-z]+".regex, "level" : Type.Number], // nested collection
                ["name" : "Hyper Beam", "type" : "normal", "level" : NSNull()],
            ]
        ]))
    }
}

JSONMatcherは、Swift製のマッチャーライブラリ、Nimbleの拡張として動作し、与えられたJSON文字列、オブジェクトを簡単に比較できます。

上記のように、複雑なJSONであっても直感的にマッチャーを書くことができます。

マッチャーとして

  • 与えられたオブジェクトや文字列がJSONであることを判定するbeJSON
  • 特定のオブジェクトを含むかを判定するbeJSONIncluding
  • 2つのオブジェクトが一致することを判定するbeJSONAs

の3種類を提供しています。

expect("{\"name\": \"Pikachu\"}").to(beJSON())
expect(["name" : "Pikachu", "no" : 25]).to(beJSONIncluding(["name" : "Pikachu"]))
expect(["name" : "Pikachu", "no" : 25]).to(beJSONAs(["name": "Pikachu", "no" : 25]))

また、上記の例にあるとおり

  • 正規表現NSRegularExpression
  • 型マッチングのようなもの

にも対応していて、かなり柔軟に比較することができます。

このライブラリは、rspec-json_matcherに大変影響を受けてます。

github.com

Swiftでライブラリを作る

今回のモチベーションの一つとして、Swiftのライブラリを最新のエコシステムを用いてしっかり作ってみたいというものがありました。

Swiftのライブラリ作成の知見は枯れておらず、有名OSSを眺めてみてもあまり統一されていなかったので、今回作成した知見をまとめてみました。

f:id:gigi-net:20160521182333p:plain

しっかりテストして、こういう風にバッヂを貼りまくると、ちゃんとしたライブラリっぽくなります。

ライブラリ作成にあたって、3月に行われたtry! Swiftの「Creating a Swift Library」というセッションが大変参考になってオススメです。

以下の書き起こしやリポジトリを参考にしてみてください。

try! Swift ライブラリの開発 #tryswiftconf Day2-8 - niwatakoのはてなブログ

github.com

ディレクトリ構造

まずSwiftのライブラリを作るにあたって、ディレクトリ構造からして悩むのですが、結論から言えば、最小構成で以下のような感じにすれば良さそうです。

実装はSources/LibraryName以下にソースコードTests/LibraryName以下にテストコードを配置するのがベストのようです。

├── Cartfile
├── Carthage
│   └── Checkouts
├── JSONMatcher.xcodeproj
├── Sources
│   └── JSONMatcher
│       ├── *.swift
│       └── Info.plist
└── Tests
       └── JSONMatcher
           ├── *.swift
           └── Info.plist

テスト

f:id:gigi-net:20160521185739p:plain

テストは必ず記述しましょう。XCTestをそのまま使っても良いですが、今回は前述のNimbleを用いて記述しています。

今回は使用していませんが、さらにRspecっぽく書きたい方はQuickを併せて使っても良いでしょう。

テストの実行には簡単なShellスクリプトを用意して、xcodebuild testを実行してます。

マルチプラットフォーム

Swift向けのライブラリは、iOSOSX、tvOS、watchOS、Linuxの5つの環境向けに作ることができます。

今回はiOSOSX、tvOS向けに対応させてみました。

watchOSではそもそもXCTestが使えないため、今回は対応できず、Linux対応は面倒そうなのでやっていません。

マルチプラットフォーム対応は簡単で、以下のようにプラットフォーム別にターゲットとスキーマを作成しましょう。

f:id:gigi-net:20160521191145p:plain

最初はiOS向けのみに実装し、テストがある程度揃ってきたら、テストを実行しつつ、マルチプラットフォーム化をするのが楽でした。

CI

CI as a Serviceとして、Travis CIやCircle CIが使えます。好きなものを使いましょう。

CIでは以下のような事を行っています。

  • 各プラットフォーム向けのテストの実行
  • カバレッジのレポート
  • swiftlintの実行
  • CocoaPodsの検証

また、先日、Travis CIでのPublicリポジトリ向けのキャッシュの提供が開始されました。Swiftのライブラリはビルド時間が長くなりがちなので、依存ライブラリのビルドに時間がかかる場合は利用すると良いでしょう。

カバレッジ

ついでにテストカバレッジを取得しましょう。Xcode7からはテストスキーマのチェックを入れるだけで、簡単にカバレッジを計測できるようになりました。

f:id:gigi-net:20160521191244p:plain

計測したカバレッジは、CIからcodecov.ioというサービスに送っています。codecovはSwift対応が楽で大変便利です。

f:id:gigi-net:20160521191005p:plain

Coverallsでの表示も可能ですが、公式でサポートされていないようでした。

パッケージ管理ツール

現在、Swiftのパッケージ管理ツールとして、CarthageCocoaPodsSwift Package Manager(SPM)の3種類がよく知られています。

対応するための作業はそれほど多くないので、とりあえず全てに対応させてます。

Carthage

基本的に上記の通り、プラットフォーム別にスキーマを作ってあげると、良い感じで対応できます。

今回のように依存関係があるライブラリの場合はCartfileに依存環境を記述しましょう。

Carthageの設計思想は、Xcode標準のスキーマを使ったシンプルなビルドシステムであり、Carthageに対応して作ると良い感じの構造になるので、意識して設計すると良さそう。

CocoaPods

pod spec createなどを実行すると、podspecのひな形ができるので良い感じで記述していきます。

また、CIでpod lib lintなどを回すようにしておくと、podspecの検証ができます。

完成したPodは公式のリポジトリに登録すると良いでしょう。

SPM

Carthage対応ができていればPackage.swiftというファイルを置くだけでなんとなく対応できます。

ほとんどユーザーはいないと思うのでちゃんと動くかどうかはあんまり確認してません。

静的解析

静的解析ツールとして、Realmが提供しているSwiftLintを使って、Travis CI上で回しています。

github.com

SwiftLintはデフォルトの設定だと厳しすぎるので、.swiftlint.ymlという設定ファイルで挙動をカスタマイズすることができます。(参考

どの警告を抑止すべきか、開発元のRealmでの利用例を踏襲しておけば問題ないと思ったので、主にここの設定を参考にしています。

まとめ

ざっくりと開発で培った知見をまとめてみました。参考になれば幸いです。

また、テストケースでJSONを扱う需要がある方は是非JSONMatcherをご利用ください。Pull Requestも歓迎です。

github.com