tofucodes diary

にほんごのほう

BitbucketのコードをHerokuに自動デプロイする方法が超簡単だった

最近仕事が暇すぎて業務時間中に個人プロジェクトのコード書いてコミットしてたりしたんですが

プライベートレポジトリじゃないんでGithubのContribution activity(草の下の方とかに出るやつ)を同僚や上司に見られたらあまり良くないよなぁと思い

GithubのオープンレポジトリからBitbucketのプライベートレポジトリに移行しました。

元々はGithubのレポジトリとHerokuを連携させて自動デプロイの設定をしてたのですが

BitbucketとHerokuの自動デプロイ連携を今回やってみたので備忘録的なやつ。

(まさかこの直後にGithubがプライベートレポジトリを無料解放するとは、この時の僕は知る由もなかった...)

blog.github.com

ではGithubのプライベートレポジトリが無料になったこの時代に、BitbucketとHerokuの自動デプロイ設定をしていきましょう。

ゴール

bitbucketにコミットすると自動的にpipelineがアプリケーションをビルドしてHerokuにデプロイする。

f:id:tofucodes:20190115225929p:plain
bitbucketのpipeline画面

f:id:tofucodes:20190115225940p:plain
pipeline #4の詳細画面

f:id:tofucodes:20190115230218p:plain
Heroku管理画面のactivity履歴

手順

  1. bitbucket-pipelines.yml の作成
  2. レポジトリに環境変数の設定

1. bitbucket-pipelines.yml の作成

f:id:tofucodes:20190115233442p:plain
bitbucketのUI上でpipeline作成

参考までにこちらが私の bitbucket-pipelines.yml です。

image: php:7.1.3

pipelines:
  default:
    - step:
        name: Deploy to production
        deployment: production
        caches:
          - composer
        script:
          - apt-get update && apt-get install -y unzip git
          - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
          - composer install
          - git push https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APP_NAME.git HEAD
  • bitbucketのデフォルトのPHPバージョンは7.1.1でしたが僕のプロジェクトにはPHP7.1.3が必要だったので7.1.3にしてます。
  • それが原因か不明ですが、以下のようなエラーになるのでapt-getでgitもインストールしてます。
+ git push https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APP_NAME.git HEAD
bash: git: command not found

2. レポジトリの環境変数を設定

f:id:tofucodes:20190115234702p:plain
bitbucketのUI上で環境変数の設定

試してませんが、Repositoryの環境変数ではなくて、その下にあるDeploymentsの変数の方でも動作するかもしれません(し、そちらの方が適切かもしれません)

Deploymentsの変数の説明

Variables that can only be used in deployments to a specific environment.

HEROKU_API_KEYの作成方法は公式ドキュメントを参照で。

devcenter.heroku.com

iPhoneのWalletアプリにオリジナルのPassを登録する方法+α

iPhoneのWalletアプリ便利ですよね。僕も最近はコンビニでの支払いや自動販売機、電車の改札もWalletで済ませています。(個人的には使ってないですがポンタカードがWalletに登録できることを発見した時はテンション上がりました)

今回はそんなWalletアプリにオリジナルのPassを登録する方法を簡単にまとめてみました。 この記事では以下の内容について簡単に触れます。

  • iPhoneのWalletアプリにオリジナルのPassを登録する方法
  • PassがiBeaconに近づいたらロック画面に通知を表示する方法
  • Pass(Wallet)にPush通知を送信する方法

developer.apple.com

オリジナルのPassを登録する

Passの作成方法

developer.apple.com

Passを作成するには最終的に.pkpassという拡張子のファイルを生成することになります。

1. Pass Type IDの作成

f:id:tofucodes:20180730232356p:plain

2. Pass用のデータフォルダ作成

適用な場所に以下の構造のフォルダを作成します。名前はなんでも大丈夫です。

Sample.pass/

3. pass.jsonの作成

先ほど作成したフォルダにpass.jsonというJSONファイルを作成します。Passの設定は全てこのpass.jsonに記載されます。

{
    ...
    "passTypeIdentifier" : "先ほど作成したPass Type ID",
    "teamIdentifier" : "DeveloperアカウントのTeam ID",
    ...
}

Team IDはMember Centerで確認できます。

f:id:tofucodes:20180730234119p:plain

その他のキーの一覧

Top-Level Keysは必須のキー、Lower-Level Keysは任意のキーのようです。

developer.apple.com

developer.apple.com

4. .pkpassファイルの生成

.pkpassの生成にはsignpassというコマンドラインツールが必要になります。まずはAppleのダウンロードリンクからsignpass用のXcodeプロジェクトをダウンロードします。

ダウンロードしたらXcodeでそのプロジェクトを開きビルドを行うことでsignpassコマンドラインツールがビルドされて利用できるようになります。

signpassを作成することができたら以下のようにPass用のフォルダを指定してコマンドを実行します。Sample.pkpassが生成されたら成功です。

$ ./signpass -p Sample.pass

あとは作成したSample.pkpassiPhoneにダウンロードするだけです。 ダウンロードの方法としては、EメールやSafariを使う方法や自分のiOSアプリを利用する方法などいくつかあります。 自分はアプリでやる方法しか試してないのですが、PassKitフレームワークを使って簡単に実装することができます。 その他の方法については以下のリンクをご参照ください。

developer.apple.com

Passのデザイン

developer.apple.com

Passには現在のところ以下の5つのタイプが存在しています。それぞれに可能なデザインが決まっているので、自分の作成するPassに合わせてデザインを決めます。例えば背景をかっこよくグラデーションにしたいと思ってもできるのはEvent ticketsだけ、というようなことがあったりするので注意です。

  1. Boarding passes
  2. Coupons
  3. Event tickets
  4. Store cards
  5. Generic

iBeaconに近づいたらロック画面に通知を表示する

Beaconと連携させることで例えば以下のような便利な機能を作ることができます。

  • お店に近づいたらクーポンをロック画面に表示する
  • 空港に到着したら航空券をロック画面に表示する
  • ジムのドアの前でジムのカードキーをロック画面に表示する

ちなみにiBeaconではなくロケーションを利用することでも可能ですが、ロケーションの設定は1つのPassに最大10個までしか登録できません。

一方beaconの設定はというと、UUIDは同じく10個までしか登録できないものの、beacon端末のmajorとminorの値を変更することで同じUUIDのbeacon端末を作成することができるため、実質10個以上のbeaconを検知することができるようになっています。

Wallet Developer Guide: Pass Design and Creation

詳細なドキュメントは上記のRelevance Information Displays Passes on the Lock Screenの項に記載されています。

pass.jsonに以下の設定を追加します。

...
"beacons": [
    {
      "proximityUUID":"iBeacon端末の識別子",
      "relevantText":"ロック画面に通知する時に表示するテキスト"
    }
],
...

以上だけです。iBeacon端末を持っていない人はMacをiBeacon端末にして動作確認できます。

私は以下の記事で紹介されているGithubのプロジェクトをそのまま利用させていただきました。(感謝)

dev.classmethod.jp

Pass(Wallet)にPush通知を送信する

Walletに登録済みのPassをPass提供者側から更新するためには、サーバからPush通知を送信する必要があります。

更新処理全体の流れは公式ドキュメントをご参照ください。

curlを例に送信方法は以下のようになります。

curl -v -d '{"aps":""}' -H "apns-topic:<Pass Type ID>" \
  -H "apns-expiration: 1" -H "apns-priority: 10" \
  --http2 --cert <path>/Certificates.pem:<password> \ 
  https://api.push.apple.com/3/device/<device token>

注意点は、--certで指定する証明書はAPNsの証明書ではなくPass Type IDを作るときに生成した証明書を利用することです。

私はずっとAPNsの証明書を使って全然Push通知が届かず無駄な時間を費やしてしまいました。。

その他にもWalletのPassへのPush通知独自の注意点などが以下のstackoverflowによくまとめられているのでご参考までに。

stackoverflow.com

KotlinConf 2018 - Shaping Your App's Architecture with Kotlin and Architecture Components

KotlinConf 2018のビデオでAndroidアプリの設計について面白いものがあったのでざっくりですがまとめてみました。

一昔前の設計のアプリをArchitecture ComponentsCoroutinesを用いて再設計している実際の経験に基づいた話でとても参考になりました。

www.youtube.com

全体設計

f:id:tofucodes:20181106083844p:plain

UI, Domain, Dataのレイヤードアーキテクチャ。これは最近まあよく見るので特に目新しさはないですね。

  • Data層:Repository, DataSource (Local/Remote)
  • Domain層:UseCase
  • UI層:ViewModel, Activity/View, XML

レイヤー間はCoroutinesでやり取りしてます。学習コストや利用の容易さからRxJavaではなくCoroutineを採択したようです。

f:id:tofucodes:20181106210703p:plain

f:id:tofucodes:20181106210645p:plain

Data層

f:id:tofucodes:20181106091855p:plain

Data層を構成するのは以下の3要素。

  • Remote DataSource:(HTTP)リクエストを構築してサーバからデータを取得
  • Local DataSource:ディスクにデータを保存
  • Repository:上記2種類のDataSourceを利用してデータの取得と保存; 任意でインメモリにデータをキャッシュ

インメモリキャッシュは専用のシングルトンクラスとか作ったりすることが多かったんですが、Repositoryでインメモリキャッシュを持つという設計は目から鱗でした。こうすることでレイヤードアーキテクチャの他の層からインメモリキャッシュを参照するようなコードも防止できるし理にかなってる気がします。ただRepositoryのプロパティに単純に持つとしたらRepositoryのライフサイクルがどうなってるのか気になります。

また設計にはあまり関係ないですが、生成されるオブジェクトの数を減らす(?)テクニックとしてinline classが紹介されていました。 下の写真の例では、1つのRepositoryに第2引数だけが異なるpostCommentという同名メソッドを定義する方法として、第2引数のLong型の変数をそれぞれ別のinline classにしています。 つまりプリミティブ型の変数に名前をつけて区別するようなものでしょうか(知らんけど)。

f:id:tofucodes:20181106084005p:plain

Domain層

f:id:tofucodes:20181106091833p:plain

UseCaseの責務は

f:id:tofucodes:20181106084046p:plain

  • チームのルールとして1つのUseCaseには1つだけのpublicメソッド、他は全てprivateメソッド
  • その1つのpublicメソッドはinvokeメソッドにして関数オブジェクトとして扱う
  • ユーザのログイン状況の確認などはUseCaseの責務(UseCaseがLoginRepositoryを扱う)

UI層

f:id:tofucodes:20181106213417p:plain

ViewModelの責務は

  • UIで表示されるデータを公開(LiveData)
  • ユーザアクションに基づいてUseCaseのアクションを実行
  • Coroutinesの開始とキャンセル

ViewModelはActivityとXMLの2箇所から参照される。 ViewModelとXMLの間のデータのやり取りはデータバインディングを用いる。

f:id:tofucodes:20181108211102p:plain

  • ViewModelは関連するいくつかのUseCaseや他の引数をコンストラクタで受け取ってimmutablityを保つ
  • ViewModelProvidersで生成するViewModelはデフォルトでは引数を受け取れないため、Factoryを拡張する
  • 全てのViewModelには対応するFactoryを作成する

動画では、ViewModel自身とstoryIDのimmutabilityを保つために同じActivityクラスが複数インスタンス存在するとしても、それぞれ別インスタンスのViewModelを持つ、というニュアンスのことを言ってる?ような気がするんだけど、これはViewModelのそもそもの目的からちょっと外れる気がするのでどういうことかよく理解できない。分かる人教えて欲しい。

f:id:tofucodes:20181106084134p:plain

  • Activityからの編集を防止するためにViewModelではMutableLiveDataをprivateにしてImutableなLiveDataを宣言して公開する
  • ImutableなLiveDataのgetterはMutableLiveDataをそのまま返却する

f:id:tofucodes:20181106084201p:plain

  • URLを開いたりトーストの表示などは、独自のEvent型のLiveDataで扱う
  • Event型を利用することで、再度observeされた時に重複してイベントが発火しないようにする
  • Event型の詳細については以下の別ブログ参照

medium.com

まとめ

  • よく見るレイヤードアーキテクチャ+Coroutinesを採択
  • インメモリキャッシュはRepositoryの責務
  • UseCaseは最小単位で実装して関数オブジェクトとして扱うといい感じ
  • ViewModelは独自のFactoryクラスを実装してUseCaseやその他データをコンストラクタで受け取ってimmutableに保つ
  • 一度きりのUIイベント等をViewModelで管理する手段として独自のEvent型のLiveDataを利用する

こんなところでしょうか。 動画の中では他にも、Data層のRepositoryがKotlinで書かれたDomain層からだけじゃなく古いJavaコードやUI層のコードからも参照される状況の対応策や、複雑なデータ構造のデータを生成するための拡張実装についてなど、有益そうな情報がたくさんありました。

XCTestでFirebase Realtime DatabaseのAPIをモックしてレスポンスを偽造する

仕事でFirebase Realtime Databaseを利用していてユニットテストを書くためにモックについて調べてみるとこちらの記事に出会いました。

medium.com

モック以外の内容も書かれており素晴らしい内容ですね。とても参考になります。

今回はもう少しお手軽にFirebase Realtime Databaseのレスポンスをモックする方法を考えてみました。

複雑なことせずになんとかならんのか、という方のお役に立てれば何よりです。

テスト対象コード(サンプル)

class FirebaseDatabaseClient {

    private let databaseReference: DatabaseReference

    init(with databaseReference: DatabaseReference) {
        self.databaseReference = databaseReference
    }

    func readSample(completion: @escaping ([String]) -> Void) {
        self.databaseReference
            .child("sample")
            .observeSingleEvent(of: .value) { snapshot in
                var sampleList: [String] = []
                // 省略...
                completion(sampleList)
        }
    }
}
  • DatabaseReferenceのインスタンスをコンストラクタで受け取る
  • "sample"というパスのデータをobserveSingleEventで1度だけ取得
  • レスポンスのsnapshotをごにょごにょしてcompletionハンドラに引き渡し実行

といったような実装になっています。

テストコード

import XCTest
import FirebaseDatabase
@testable import Sample_App

class FirebaseDatabaseClientTests: XCTestCase {

    func testReadSample() {
        let client = FirebaseDatabaseClient(with: MockDatabaseReference())
        let expectaion = expectation(description: #function)

        client.readSample { sampleList in
            XCTAssertTrue(sampleList.contains("something"))
            expectaion.fulfill()
        }
        wait(for: [expectaion], timeout: 0.5)
    }
}

// MARK: - Override Firebae Realtime Database

private class MockDatabaseReference: DatabaseReference {

    override func child(_ pathString: String) -> DatabaseReference {
        return self
    }

    override func observeSingleEvent(of eventType: DataEventType, with block: @escaping (DataSnapshot) -> Void) {
        let snapshot = MockSnapshot()
        DispatchQueue.global().async {
            block(snapshot)
        }
    }
}

private class MockSnapshot: DataSnapshot {

    override var value: Any? {
        return ["key_1": "value_1", "key_2": "value_2"]
    }
}

1つ1つ軽く解説します。

テストケース

    func testReadSample() {
        let client = FirebaseDatabaseClient(with: MockDatabaseReference())
        let expectaion = expectation(description: #function)

        client.readSample { sampleList in
            XCTAssertTrue(sampleList.contains("something"))
            expectaion.fulfill()
        }
        wait(for: [expectaion], timeout: 0.5)
    }

まずはテストケースから。1行目でテスト対象のクラスを作成しますが、その際に引数としてFirebaseのDatabaseReferenceクラスのモックオブジェクトを渡します。モックオブジェクト自体の解説は後述します。 またデータベースからのデータ取得は非同期であるはずなのでXCTestExpectationを用います。

DatabaseReferenceのモック

private class MockDatabaseReference: DatabaseReference {

    override func child(_ pathString: String) -> DatabaseReference {
        return self
    }

    override func observeSingleEvent(of eventType: DataEventType, with block: @escaping (DataSnapshot) -> Void) {
        let snapshot = MockSnapshot()
        DispatchQueue.global().async {
            block(snapshot)
        }
    }
}

こちらが先ほども出てきたFirebaseのRealtimeDatabaseのモックオブジェクトの定義です。今回はテスト対象のクラスでobserveSingleEventを利用しているのでその関数と、child関数もオーバーライドしています。

child関数をオーバーライドする理由としては、アプリコードの方でデータベースの特定のパスのデータを取得する場合にchild("path")というAPIが実行されると、テスト対象クラスのコンストラクタに引き渡したDatabaseReferenceオブジェクトとは別のインスタンスが使われてしまってモックすることができません。

なので今回はchildが何度実行されようともモックオブジェクトを返すようにして、どんなpathのデータを取得しようともモックのDatabaseReferenceクラスが利用されるようにしています。もしpathによって返却するデータや処理を変えたい場合は、以下のようにオーバライドメソッドの中でpathStringを見て別のオブジェクトを返却したりとかでできるかもしれません。

    override func child(_ pathString: String) -> DatabaseReference {
        switch (pathString) {
        case "hoge": return HogeDatabaseReference()
        case "fuga": return FugaDatabaseReference()
        }
    }

DataSnapshotのモック

private class MockSnapshot: DataSnapshot {

    override var value: Any? {
        return ["key_1": "value_1", "key_2": "value_2"]
    }
}

最後に実際のレスポンスのクラスであるDataSnapshotのモックオブジェクトの定義です。DataSnapshotのvalueプロパティは本来readonlyなのでそのままではvalueを自分好みにすることができません。そこでvalueのプロパティをオーバーライドして、テストで利用したいデータを返却します。

このモックオブジェクトを先ほどのDatabaseReferenceクラスのモックオブジェクトのobserveSingleEventで利用することで、Realtime Databaseから指定したデータが返却されているように偽造することできるようになります。

MapKitの地図をGoogleMap風にカスタマイズする

簡単に現在地の周辺や特定の場所を表示したりするだけならGoogle Map SDKを使えば良さそうなんですが、

アノテーションを充実させたり、検索機能が必要だったりするとMapKitを使う方がベターだったりします。

(知ってる限りではGoogle Mapでの検索はPlace APIで課金が発生する)

とはいえUberみたいなイケてる地図を表示したいという要求がデザイナーから来ることも無きにしも非ず。

そういう場合は、後述する方法でMapKitのメリットを享受しながら見た目だけGoogle Map風に変更する、のが一つの手かと思います。

方法

MapKitの地図はオーバーレイの画像をMKTileOverlayで指定することができるようになっているのでそれを加工します。

MKTileOverlay - MapKit | Apple Developer Documentation

私は今回調べるまで聞いたこともなかったんですが、タイルオーバーレイというのは地図のタイル(小さい単位の区域)を表示する際にタイルのデータを(サーバから)取得して地図の上に表示する仕組みのようです。

タイルのデータの取得先は自分でホストしたり以下のようなプロバイダーからAPIで取得したり、といった感じ。

Tile servers - OpenStreetMap Wiki

Google Maps APIs Styling Wizard

このタイルオーバーレイの仕組みを利用してGoogle Map風のタイルを取得するSDKがこちら👏

github.com

Googleが提供しているGoogle Maps APIs Styling Wizardを利用してタイルデザインのJSONファイルをまずは自分で作成しSDKに渡します。

SDKはそのJSONファイルのパースして、Googleのタイルサーバへとリクエスト、取得したタイルの表示までしてくれるようです。

mapstyle.withgoogle.com

Snazzy Maps

Styling Wizardを利用して自分好みのタイルデザインを作ることも可能ですが、

クールでセクシーなデザインがまとまっているサイトがあるので、そちらから気に入ったものを選ぶこともできます🤩

snazzymaps.com

参考

色々と調べる中で以下のブログを見つけ、まさにこれだ!となりました。感謝。

medium.com

Xcode 10でビルドしたアプリがiOS 9.0, 9.1, 9.2 でクラッシュしまくる件について

先日ようやっとiOS 12Xcode 10リリース後初のiOSアプリリリースをキメたんですが、

その後最新バージョンでクラッシュが急増する事態に。。。

クラッシュログを確認するとiOS 9でしか発生していない模様。

不思議だな〜と思いつつコード追ってみても原因がよく分からない。

諦めてググってみたところどうやら同様のクラッシュ事件でstackoverflowが盛り上がってる。

stackoverflow.com

質問やそれに対するコメントをざっくりまとめてみると、

  • Xcode 10 GMでビルドしたアプリがiOS 9でクラッシュするぜ
  • Appleがバグと認定してる(バグチケットの作成を促している)
  • iOS 9.3未満で発生する
  • 何年もコードと画像を変更してなかったアプリでXcode10にした途端[UIImage imageNamed:]でクラッシュする
  • Xcode 9でビルドしたら正常に動作する

well done,but It‘s unpractical for us.I think it is the xcasset's problem always.Xcode 10 release note and wwdc said xcassets has been optimized,it is a joke

hahaha

Swift 4.2 対応したばっかだしXcode 9に戻すのは気が引ける&iOS 9ユーザ多くないし静観するか〜と思っていた矢先、朗報。

Xcode 10.1 beta 2 Release Notes | Apple Developer Documentation

Resolves an issue that affected app compatibility with iOS 9.0, 9.1 and 9.2. Apps containing asset catalogs built with Xcode 10 whose deployment target was set to iOS 9.0, 9.1 or 9.2 would produce content incompatible with the runtimes of those iOS versions. Rebuilding the application with Xcode 10.1 resolves this issue. (44535967)

アセットカタログを含むデプロイターゲットiOS 9.0, 9.1, 9.2アプリがXcode 10でビルドされると、ランタイムと互換性のないコンテンツが生成される。Xcode 10.1で解決する

めでたし、めでたし。

モバイルアプリエンジニアのポートフォリオサイト「Project Showcase BETA」を試してみた

先日メールボックスにこのようなメールが。

Hey Toru,

I was searching for mobile developers by diving into the Swift stargazers on Github and came across to your profile. I see that you are a productive developer, especially CleanArchitectureChat is a great example. I believe you may find it useful to have a portfolio website to showcase your projects which I can help with.

スターゲイザーでは決してないのでどんなBotで引っかかったんやとは思いつつ、

How? I'm a founder of a startup that develops an online portfolio creator, called Project Showcase. It is especially designed and built for mobile developers. If you have any interest on mobile application development or if you have any application in the Play Store or App Store, you can exhibit them in your portfolio. It's so easy to use, you should give it a try.

Project Showcaseというサービスの宣伝でした。アプリエンジニアのポートフォリオって今までありそうでなかった気がするので気になって登録・利用してみたので紹介です。

projectshowcase.me

管理画面はこんな感じ。

f:id:tofucodes:20180928221855p:plain

アプリエンジニア専用ということはなさそうですが、AppStoreとPlayStoreからアプリデータを取得してプロジェクトとして登録できる機能が中心となっているので、アプリエンジニアにとってはぽちぽちとするだけで今まで自分が作ったアプリがずらっと並んで見応えあるサイトになると思います。

ただ現状はアプリ検索に制限があります。

AppStoreに関してはiTunesの検索APIを利用してるのですが"country"パラメータをクエリに含んでいないためデフォルトのUSがアプリの配信対象に入ってないと検索に引っかかってくれません。

PlayStoreに関してはGoogleAPIを提供していないようで、Project Showcaseチームが自分たちでホストしてるAPIを利用してるようです。なので中のロジックは皆目検討つきませんが、こちらも日本で配信してるアプリは検索結果に出てきてくれませんでした。

まだBETAなのでこれからの改善や機能追加に期待ですね。

こちらが私のポートフォリオです。

projectshowcase.me