Xcode の Other Swift Flag で -D STAGING を指定したのに #if STAGING が有効にならなかった

Xcode で環境別にコンパイルするために Staging の Configuration を作ったんだけど、 -D STAGING を指定しても #if STAGING が false になっていた。

f:id:star__hoshi:20171127214536p:plain

なんでかな〜 って設定眺めていたら、 Active Compilation Conditions が Debug になっていたのが原因だった。

f:id:star__hoshi:20171127215036p:plain

これを STAGING にしたら #if STAGING がうまくいった。

f:id:star__hoshi:20171127215120p:plain

Rails 開発で GitHub に Push して CircleCI のテストが通ったら Heroku に deploy する

CircleCI を使って、テストが通ったら Heroku に deploy するという流れを構築する。

サンプルリポジトリ: starhoshi/rails-circleci

環境

  • GitHub
  • Rails 5.X
  • Heroku
  • CircleCI 1.X
    • ここでのサンプルは 2.0 ではない

ソースコードstarhoshi/rails-circleci

まず rails new

$ gem install rails
$ rails new . --api -d=postgresql
$ rails g controller

これで Rails Project + users_controller が作成される。
また、コメントアウトされている test のコメントを外し、通るようにする。

test/controllers/users_controller_test.rb

require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest
  test "the truth" do
    assert true
  end
end

GitHub に Push

GitHub に Repository を作って、 Push しておく

CircleCI と Repository の連携

CircleCI に GitHub でログインしておく。
CircleCIでRails4アプリをHerokuへデプロイする - Qiita にあるように、 ssh key の登録が必要かもしれない。

Projects > Add Project > rails-circleci を Set Up する。

f:id:star__hoshi:20171126170222g:plain

そうすると勝手に CircleCI が回り出して、テストが成功する。

Heroku

Heroku App 作成

こんな感じで作成する。

f:id:star__hoshi:20171126170458p:plain

API Key

https://dashboard.heroku.com/account から API Key を取得

f:id:star__hoshi:20171126170540p:plain

CircleCI の Heroku Deployment 設定

https://circleci.com/account/heroku に取得した API Key を設定する。

f:id:star__hoshi:20171126170746g:plain

これで Heroku / CircleCI の設定が完了。

circle.yml

deployment:
  production:
    branch: master
    commands:
      - heroku maintenance:on --app rails-circleci
      - git push git@heroku.com:rails-circleci.git $CIRCLE_SHA1:refs/heads/master
      - heroku run 'rake db:migrate; rake db:seed_fu' --app rails-circleci
      - heroku maintenance:off --app rails-circleci

こんな感じで作成。 --app と git@heroku.com:rails-circleci.git は自分の環境に合わせる。

git push すると CircleCI が動いて Heroku Deploy される

git push すると自動で CircleCI が動いて、 test が実行されたあとに deploy される。

f:id:star__hoshi:20171126171100p:plain

これでめでたく当初やりたかったことができた。

test がコケたらどうなる?

Not Deploying になって、 deploy がされないことがわかる。

f:id:star__hoshi:20171126171248p:plain

bundle update のプルリクエストを毎日自動で作成する

Gemfile.lock を最新に保つため、bundle update を毎日自動でできるようにしたい。
Tachikoma.io というサービスもあるみたいだけど、 private repo は有料っぽいので自作した。

勝手に update されてアプリケーションがバグると困るので、

  1. Gemfile.lock を更新したプルリクエストを作る
  2. プルリクエストを人間が確認してマージ

という手順で行う。

先人の知恵

ここら辺を参考にした。

環境

  • CircleCI 1.X
    • 1年前に構築した環境なので 2.0 ではない...
  • GitHub Private Repository
  • Heroku
  • Rails 5.X

流れ

  1. Heroku Scheduler が CircleCI を叩く
  2. CircleCI で bundle update を実施
  3. Gemfile.lock 更新があれば CircleCI が GitHub に PR を作成
  4. PR を人間が確認してマージボタンをポチ

1. 前準備

2. rake task 作成

lib/tasks/bundle_update_pull_request.rake を作成する。

# frozen_string_literal: true
namespace :bundle_update_pull_request do
  desc 'pull request if bundle updated'

  task check: :environment do
    project = 'repo-team/repo-rails'
    branch = 'master'
    token = 'CircleCI Token'
    params = '{"build_parameters": {"BUNDLE_UPDATE": true}}'

    header = { 'Content-Type' => 'application/json' }
    client = Net::HTTP.new('circleci.com', 443)
    client.use_ssl = true
    response = client.post("/api/v1/project/#{project}/tree/#{branch}?circle-token=#{token}", params, header)
  end
end

CircleCI の Nightly Builds を叩くだけの task。
params に '{"build_parameters": {"BUNDLE_UPDATE": true}}' を渡していて、 CircleCI ではこのパラメータをみて bundle update するのか判断する。

この task は以下のコマンドで実行できる。

$ bundle exec rake bundle_update_pull_request:check

3. circle.yml

circle.yml をこんな感じで書く。

test:
  post:
    - >
      if [ -n "${BUNDLE_UPDATE}" -a "${CIRCLE_BRANCH}" = 'master' ] ; then
        bundle update
      fi

deployment:
  master:
    branch: master
    commands:
      - >
        if [ -n "${BUNDLE_UPDATE}" ] ; then
          bash script/circleci/create_pull_request_if_needed.sh
        fi

2 で params に渡した BUNDLE_UPDATE が true だったら bundle update 実行 + PR 作成するようになっている。

4. create_pull_request_if_needed.sh

script/circleci/create_pull_request_if_needed.sh を作成する。

#!/bin/bash

export BRANCH=bundle-update-`date -u "+%Y%m%d"`
if [[ -n `git status -sb 2> /dev/null | grep Gemfile.lock` ]] ; then
  git config --global user.email git@git.com
  git config --global user.name 'git'
  git add Gemfile.lock
  git commit -m 'Bundle update'
  git branch -M $BRANCH
  git push origin $BRANCH
  bundle exec ruby script/circleci/create_pull_request.rb
fi

3 で実行された bundle update で、 Gemfile.lock に差分があれば PR を作成するだけ。

5. create_pull_request.rb

script/circleci/create_pull_request.rb を作成。

# frozen_string_literal: true
require 'octokit'

client = Octokit::Client.new(access_token: 'GitHub Personal access tokens')
client.create_pull_request(
  'repo-team/repo-rails',
  'master',
  ENV['BRANCH'],
  'Bundle update',
  ''
)

これで PR が作成される。

ここまでで bundle exec rake bundle_update_pull_request:check で Bundle update の PR が自動で作成されるようになった。

6. Heroku Scheduler で定期実行

ここでは Heroku を使っているけど、定期実行できればなんでも良い。

f:id:star__hoshi:20171126030819p:plain

動かしてみる

こんな感じで GitHub に毎日 PR がくるようになります。

f:id:star__hoshi:20171126031005p:plain

これを人間が目視で確認して問題なければマージしましょう。

しかし、目視といっても version が変わったことしかわからないので、これを実際に運用するにはテストがしっかりと書かれていて、そのテストが壊れてないことを確認したらマージするようにしないとダメですね。

MusicPlayer で音楽を再生しようとしたら __abort_with_payload というエラーが出た

import MediaPlayer

...
    override func viewDidLoad() {
        super.viewDidLoad()
        let player = MPMusicPlayerController.systemMusicPlayer
        player.setQueue(with: MPMediaQuery.songs())
        player.play()
    }

これで音楽が再生できたんだけど、再生開始した瞬間にアプリがクラッシュした。どうやら権限が足りていないようだ。

端末の音楽情報を参照するには Info.plist に privacy を書かねばならない。

 <key>NSAppleMusicUsageDescription</key>
    <string>Reference music information to play music.</string>

こんな感じで NSAppleMusicUsageDescription に対してアクセスしたい理由を書けば OK。
(PropertyList で見る場合は Privacy - Media Library Usage Description になっている。)

参考: ios - App crashes when running on iPhone with violations as exception - Stack Overflow

TestFlight でリリースごとにアプリを配布していたらそれに助けられた

TestFlight という、アプリのベータ版などを配布する Apple 純正のサービスがある。

TestFlight - Apple Developer

これのいいところは「リリースするアプリと同じアプリをテストできる」というところ。
実際にユーザが使うアプリと同じもので動作確認ができるし、同じアプリファイルで Apple は審査を行うのでバグがあったら Self Reject もしやすい。

なので私はいつも

  1. iTunesConnect にアプリファイルを提出
  2. TestFlight で配布 & Apple 審査申請
  3. TestFlight で動作確認
  4. Apple 審査完了
  5. リリース

という流れでリリースをするようにしている。
(この流れは fastlane で自動化している fastlane-example/Fastfile, fastlane deliver を使ってコマンド1発で Waiting For Review までもっていく)

助けられた

先日アプリがクラッシュっするバグを出してしまって、そのバグの原因はアプリの過去バージョンに起因するものだった。
クラッシュする原因は以下の2つ。

  1. ver 3.0.0 が初回インストールだったユーザ
  2. ver 1.X.X -> ver 3.X.X に飛び越えてアップデートしたユーザ

今回 TestFlight に助けられたのは 2 のパターンの原因特定 / デバッグのところ。

TestFlight は 90 日過去バージョンをインストールできる

最初はクラッシュする原因が全然わからなかったんだけど、 TestFlight を見てたら過去バージョンがズラ〜っと並んでいて、なんとなく ver 1.X.X のをインストールした後に ver 3.0.1 をインストールしたらクラッシュして原因特定できた。

原因特定できた後も修正確認のために ver 1.X.X をインストールして、 ver 3.1.0 をその後インストールして動作が問題ないか、というテストができた。

もし TestFlight がなかったらデバッグが大変になってて、 ver 1.X.X をインストールするためには Xcode 8.X を持ってきて... というところから始めないといけなくなる。 AppStore からも基本的に過去 ver は DL できないし。

TestFlight で配布していたおかげで過去バージョンからのマイグレーションテストも手軽にできて助かった。

Apple 特急審査 2017

ここ最近は Apple の審査が早いけど、致命的なバグなどを出してしまったら急いでレビューして欲しい、そういう時のために apple expedited review というのが用意されている。

これを使うと優先的にアプリをレビューしてくれる。
しかしこれには注意が必要で、使いすぎてはいけないのでマジでやばい時にだけ使うべき。

今回はマジでやばいバグを生み出したので特急審査を出した。

f:id:star__hoshi:20171121021826p:plain

* マジでやばいバグの図

やり方

1. Contact Us

iTunes Connect にログインして右下の Contact Us をクリック

f:id:star__hoshi:20171121021855p:plain

2. Request an Expedited App Review

  1. App Review
  2. App Store Review
  3. Request Expedited Review

を選択すると特急審査へのリンクが表示されるので、 Request an Expedited App Review をクリック。

f:id:star__hoshi:20171121022059p:plain

3. Contact the App Review Team

なんか突然今っぽいデザインになる。
内容をいい感じに入力して、 Explanation に説明を書く。
ここはなぜ特急審査が必要なのか真面目に書いた方がいいと思う。

自分は Apple への連絡はいつも英語 + 日本語で投げるようにしている。(英語に自信がないので...)

f:id:star__hoshi:20171121022132p:plain

最後に Send を押すと「問い合わせ受け付けたよ!」みたいな画面が出ておわり。
なお、問い合わせ完了メールなどは来ないもよう。

あとは迅速に審査が終わることを祈るのみ、しかし日本時間15時に特急審査願いを出して12時間経っても音沙汰なし。
普通のレビューと大して変わらないのでは...?

iTunes Connect の段階的リリースを試してみた

段階的リリースとは

WWDC2017 で発表された機能。 1%, 2%, 5% ... 100% と限られた人から段階的に自動アップデートされていく。

新機能のテストや、バグが起きてないかなどの確認で使えそう。

注意

これは 自動アップデート が対象なので、新規インストール/手動アップデートされると最新版がインストールされる。

やってみた

3 日目の浸透率

自動アップデートされるのは 5% だけ。

f:id:star__hoshi:20171117021037p:plain

でも iTunes Connect の分析画面では 30% くらいいってそうだ...。

f:id:star__hoshi:20171117021115p:plain

同じく Firebase を見ても 30% くらいいってる。

f:id:star__hoshi:20171117021151p:plain

感想

確かに100%リリースに比べると浸透率は穏やかなんだけど、自動アップデート以外の人は普通にアプデされるしあまり意味なさそう...。
ちゃんとやるなら Firebase の A/B Testing とか Remote Config でやった方が全然良い、これはないよりマシかな〜くらいの機能。

新機能リリースでバグがないか不安で段階的リリースを使ったんだけど、3日目になってクラッシュ報告が特にないので100%リリースに変更した。まあやっぱりないより便利だ。

UIView の Swipe イベントは実行して Tap は後ろの View に流したいけど出来なさそう

とある View の上に透明な UIView を載せて、 Swipe だったら透明な View でイベントをハンドリングして Tap だったら後ろの View にスルーしようとしたけど出来なさそうだった。

どういうことか

f:id:star__hoshi:20171114210748p:plain

こういう View があって、右にスワイプしたら数値を200にして、左にスワイプしたら数値を100にしたい。でもボタンタップや TextFeild の入力はできるようにしたい。
この View の上に透明な View を被せて Swipe イベントを取って、 Tap イベントだったら後ろの TextField やボタンをタップできるようにすれば良さそうじゃんって最初は思った。

でも出来なさそう。

View のイベントを後ろにある View に流す方法

1. isUserInteractionEnabled = false

isUserInteractionEnabled = false にすると透明な View のタップイベントがスルーされ後ろにある View をタップできる。
しかしこれだと Swipe もスルーされてしまうのでだめ。

2. hitTest:withEvent:

hitTest を override して、 return nil しても後ろの View にイベントを流せる。しかし Swipe も反応しなくなってしまうのでダメ。

if event?.type == .swipe { } ができればよかったが、UIEventType は touches, motion, remoteControl, presses の4種類しかなく、 Tap もスワイプも touches になってしまいダメだった。

3. point(inside:with:)

hitTest と似てる point というものあって、これはタップの判定位置を広くしたり狭くしたりできるっぽい。
ここで false を返すと後ろの View にイベントを流せる。
(しかしそういう用途には hitTest を使うべきで point は純粋にタップ範囲の変更に使った方が良さそう。)

結局どうしたか

透明な UIView を上に貼って、 Tap だったら後ろに流すというのは難しそう。
なので、透明な UIView の中にボタンや TextView を addSubview した。

class SwipableView: UIView {
    let leftSwipeGesture = UISwipeGestureRecognizer()
    let rightSwipeGesture = UISwipeGestureRecognizer()

    override init(frame: CGRect) {
        super.init(frame: frame)

        leftSwipeGesture.direction = .left
        rightSwipeGesture.direction = .right
        addGestureRecognizer(leftSwipeGesture)
        addGestureRecognizer(rightSwipeGesture)
    }

    required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
        let swipableView = SwipableView()
        contentView.addSubview(swipableView)

        swipableView.leftSwipeGesture.rx.event.subscribe { event in
            // left event handler ...
        }
        aIVSwipeView.rightSwipeGesture.rx.event.subscribe { event in
            // right event handler ...
        }

        let textField = UITextField()
        swipableView.addSubView(textField)
        let button = UIButton()
        swipableView.addSubView(button)

これで Swipe した時はそれぞれハンドリング出来て、 textField や button のイベントはそれぞれちゃんと扱える。

最初からこうしろよ、という話なのだがこれをすると既存の View 構造を変えないといけないので気が重かった、透明な View を上に貼り付けるだけなら既存の View 構造は変えなくて済むので...。

遠回りだったかもしれないけど hitTest や pointinside の知識が深まってよかった。

Firebase Cloud Function の関数を削除する

Function を消したいけど、 Firebase Cloud Function 上では削除メニューなどが見当たらない。 その場合は GCP の方から関数を削除する。

  1. https://console.cloud.google.com/projectselector/home/dashboard にアクセス
  2. 画面左上の「プロジェクトを選択」から該当のプロジェクトを選択
  3. 画面左メニューの Cloud Functions に遷移
  4. Function 一覧から削除したい関数を見つけて、右にある ・・・ から削除を選択

で削除できる。

監視している中で対象を update して無限ループが発生してしまった場合などに便利。

export const cartIsSubmitted = functions.database.ref('/v1/user/{userID}/name').onWrite(event => {
    // …
    event.data.adminRef.update({name:newName})
})

追記

Firebase Cloud Function をローカルで実行する

Cloud Function をローカルで実行する方法を用意してくれている: ローカルでの関数の実行  |  Firebase

手順はドキュメントに書いてある通りで簡単にできる。

firebase ライブラリの更新

$ npm install --save firebase-functions@latest
$ npm install -g firebase-tools

ローカルデプロイ

まず Function を作る

これは https://my-firebase-url/sample?text=text にアクセスするとその text を返却するだけのもの。

index.js:

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

exports.sample = functions.https.onRequest((req, res) => {
  const text = req.query.text;
  console.log(text);
  response.status(200);
});

ローカルデプロイ

$ firebase experimental:functions:shell
i  functions: Preparing to emulate functions.
✔  functions: sample
firebase >

これでデプロイ成功で対話モードになっているので、コマンドを入力する。
request の記法で動く。

firebase > sample.get('?text=text');
Sent request to function.
info: User function triggered, starting execution
info: text

RESPONSE RECEIVED FROM FUNCTION: 200, {"text":"text"}
info: Execution took 8 ms, user function completed successfully

こんな感じでローカルデバッグできる。

メリット

Cloud にデプロイするのちょっと時間がかかるし、ログ見るのも Cloud Function のログを見ないといけないのでだるいけど、ローカルデバッグだと関数書き換えたらリアルタイムデプロイ & ログがその場で見えるので手探りで開発するときには便利。
あと Cloud Function で外部 API と通信しようとするとお金がかかるけど(プラン変更が必要)、ローカルデプロイだと外部 API と通信もできた。

Cloud Function がローカルで動くというのがイケてて良い。