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 がローカルで動くというのがイケてて良い。

fastlane deliver を使ってコマンド1発で Waiting For Review までもっていく

$ bundle exec fastlane release

このコマンドを叩くだけでリリースビルド、iTunesConnect へバイナリ提出、IDFA 情報など入力して審査待ちの状態まで持っていけるようにした。

Deliverfile

こんな感じに書いてる。

app_identifier "com.myapp"
username "appleid@hoge.com"

force true
skip_screenshots false
skip_metadata false
skip_binary_upload false
automatic_release true
submit_for_review true
overwrite_screenshots true
ignore_language_directory_validation ['fonts']

submission_information({
  export_compliance_encryption_updated: false,
  export_compliance_uses_encryption: false,
  add_id_info_uses_idfa: true,
  add_id_info_serves_ads: true,
  add_id_info_tracks_install: false,
  add_id_info_tracks_action: false,
  add_id_info_limits_tracking: true,
})

submit_for_review true にして、 submission_information を書けばいいっぽい。あと force true にしないと申請情報の確認画面が出てしまう。
encryptionadd_id_info は普段手動でぽちぽちしてる状態になるようにした。

他の option 一覧は fastlane/app_submission.rb にある。

Fastfile

deliver で has completed processing まで持っていくので、その後 dsyms を iTunesConnect から DL して Crashlytics に Upload できる。

  desc "Deploy a new version to the App Store"
  lane :release do
    match(type: "appstore")

    increment_build_number(build_number: "#{Time.now.strftime("%Y%m%d%H%M")}")

    gym(scheme: ENV["RELEASE_GYM_SCHEME"])

    deliver

    upload_symbols_to_crashlytics
    
    refresh_dsyms

    payload = {"Git Commit" => changelog}
    slack(
      channel: ENV["SLACK_CHANNEL"],
      message: ":itunesconnect: Successfully uploaded a new App Store build",
      payload: payload,
      default_payloads: default_payloads
    )
  end

Fastfile はソース公開しているのでこっちで全体像を見れる: fastlane-example/Fastfile

おわり

寝る前に bundle exec fastlane release 叩いて、寝ながら Youtube 見てたら Waiting For Review の通知きて最高。

人間だもの

読んだ: 自戒 - <y>

わかる。

自分の書くコードはクソでエンジニア向いてないからエンジニアやめたいって思うけど俺にはプログラミングしかないからコード書くことで生きていくしかないから勉強しないといけないけど勉強しても理解度が低いし人生なんのために生きているんだプログラムを書くために生きているのかいや違う人生楽しむために生きているのに俺は強迫観念や劣等感にかられ勉強や仕事をしているが果たしてそれは生きているというのか死んだほうがいいのではパン屋さんになりたい何が圧倒的成長だ成長している人間より人生楽しんでいる人間のほうが絶対にいいみんな仕事できるし人生楽しそうだしすごい人間死んだら終わりだし何をがんばっているんだなどということを1日3回くらい考えていてそしてまた今日も1日が終わる。

でもいいじゃない、つらいときはつらいし楽しいときは楽しい。
べつにコンビニのバイトでも生きていけるし、そんなに頑張らなくてもいいじゃない、人間死ぬときは死ぬ、人間だもの。

個人アプリを Xcode9 Swift4 対応した時にやったこと

個人アプリ2つを Xcode9 GM で動くようにした。
これは私のアプリの場合なので、違うプロジェクトでは不要なことや、他にもっとやらないといけないことがあると思う。

やったこと

1. Xcode9 DL

Xcode - Apple Developer から DL してインストール。
Xcode8もまだ使うので、8と9を同居させるため Xcode9GM.app にリネームした。

2. xcode-select で 9GM を使うようにする

$ sudo xcode-select -s /Applications/Xcode9GM.app/Contents/Developer/
$ xcode-select -p
/Applications/Xcode9GM.app/Contents/Developer

3. bundle update

念のため bundle update して cocoapods や fastlane を最新にした。

4. ライブラリ更新

$ pod update
$ carthage update --platform iOS --no-use-binaries

ここで Xcode 9 でプロジェクトをビルドすると動くようになった。 と言ってもこの時点ではまだ Swift3.2 のはなし。

5. Swift 4

メニュー > Edit > Convert > To Current Swift Syntax… を選ぶ、そうすると Xcode が自動でマイグレーションしてくれる。
マイグレーションしてくれたのはほとんどが dynamic@objc をつけるものだった。

あとはこんなところを手動で直した。

- let attributes = [NSFontAttributeName: UIFont.fontAwesome(ofSize: 20)] as [String: Any]
+ let attributes = [NSAttributedStringKey.font: UIFont.fontAwesome(ofSize: 20)]

- dismiss(animated: true, completion: { [weak self] _ in
+ dismiss(animated: true, completion: { [weak self] in

これだけで Swift4 のビルドは通った。 しかし起動したらエラー。

6. UIVisualEffectView を修正

let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .extraLight))
- effectView.addSubview(UILabel())
+ effectView.contentView.addSubview(UILabel())

UIVisualEffectView は contentView に subview を追加しないといけなくなった。
これは実行時エラーなのでコンパイルは通ってしまう。

これを直したら起動した。

7. warning 対応

Swift3 @objc InterfaceOff にした。
また、 substring が使えなくなったり CGFloat(truncating: String) を使うように修正した。

7. Runtime Error

これは Swift4 対応とは特に関係ないと思われる。

アプリは問題なく動いているのだが、 UIApplication あたりでなんか出てた。

runtime: UI API called from background thread: -[UIApplication delegate] must be used from main thread only
runtime: UI API called from background thread: -[UIApplication applicationState] must be used from main thread only
runtime: UI API called from background thread: -[UIApplication keyWindow] must be used from main thread only
runtime: UI API called from background thread: -[UIApplication supportedInterfaceOrientationsForWindow:] must be used from main thread only

画像で見るとこんな感じ。

f:id:star__hoshi:20170917232741p:plain

ググったら Firebase が古い時のエラーのようで、 Firebase を新しくすれば消える。

  pod 'Firebase' , '~> 4.0'
  pod 'Firebase/AdMob' , '~> 4.0'
  pod 'Firebase/RemoteConfig' , '~> 4.0'

これで pod update すれば OK。

8. 新しい Google Analytics, Firebase 修正。

Firebase を新しくしたら依存する Google Analytics の書き方も変わったみたいなので、 Bridging-Header.h を直した。

+ #import <Google/Analytics.h>
- #import <GoogleAnalytics/GAI.h>
- #import <GoogleAnalytics/GAIFields.h>
- #import <GoogleAnalytics/GAILogger.h>
- #import <GoogleAnalytics/GAITracker.h>
- #import <GoogleAnalytics/GAIDictionaryBuilder.h>

あと Firebaes の FIR のプレフィックスを消す必要があった。

9. おわり

これで Simulator で Run したらエラーも消えてめでたし ㊗️

UITabBar の背景をすりガラスのまま Dark にする

普通の UITabBar は明るいすりガラスだけど、これを UIBlurEffect(style: .dark) と同じように暗くしたい。

Light Dark
f:id:star__hoshi:20170829220502p:plain f:id:star__hoshi:20170829220551p:plain
override func viewDidLoad() {
    super.viewDidLoad()

    tabBar.backgroundImage = UIImage()
    let dummy = UIToolbar(frame: tabBar.bounds)
    dummy.barStyle = .black
    tabBar.insertSubview(dummy, at: 0)
}

UITabBarController の viewDidLoad にこんな感じでかくと、これで Dark な UITabBar を作れる。透明な画像を UITabBar の背景に設定して、その下に Dark な UIToolbar を追加するハックっぽいやり方。

参考: ios - UITabBar with blur - Storyboard - Stack Overflow

また転職することにした

10ヶ月ほど前に意気揚々と 退職エントリ を書いたくせに、1年経たず退職となってしまった。

最初に断っておくと、前職よりは100倍マシだった。
けど悩みや欲望というのは無限に湧いてくるわけで、色々考えて転職することにした。

結局なんでやめんの

何か大きな出来事があったとかではなく、色々あった。

開発を外注して納期厳守だから突貫工事でリリースまではやるけど後は内部の人がメンテしてねという開発スタイルで渋い気持ちになったり、渋い割に給料が良くねえなと思ったり、 iOS エンジニアが 1 人しかいないので雑務が多かったり外注管理おじさんになったり、一人でプルリクみて一人でマージして寂しい気持ちになったり、ジョイントベンチャーだと自分たちじゃどうしようもないことあって辛いな〜と思ったりした。

「アプリなんてなければいいのに」という発言もあってだいぶショックを受けた。これは Apple の制限でできないことが多いことに対する文句なのだが、会社の体質をよく表していると思う。
アプリは「A は快適に使えるけど B は出来ない、 B をしたいなら Web を使ってくれ」というように、何かの機能に特化するのがよくある考え方だと思うけど『お客さんに出来ないと言えない』ので、アプリにも全ての機能を乗せようとする。しかし工数は限られるので WebView でそのページを開くだけ、みたいなことになる。そしてアプリ上の WebView がうまく動かないと問い合わせが来る。もうアプリない方が良くね?ユーザも混乱してるよ?全部 Web でやれば?ってなるじゃないですか。

やっていくぞ!!!!!!!!みたいに勢いある人がいてもつらくて、今まで色々開発を経験してそのノウハウを還元したい人とか、言われたことを素直にやる人にとってはいい会社なのかなと感じた。

ちょっと悲しいのが、自分が辞めるときに退職理由とか聞かれなかったこと。以前辞めた人も同じく聞かれなかったらしい。
新しい人を採用するぞ!と採用活動をするのはいいけど、今いる人を出さないことが一番大事なんじゃないのかな、と思う。 最近 1 on 1 が始まったらしいので、それで内部環境が改善されていくことを願っている。

別に悪い話ばかりじゃなくて、 iOS エンジニア 1 人しかいないから開発から CI 環境整備までやったので圧倒的成長ができた。辛いこともあったけど色々なことを経験できたし、入社して良かったと思ってる。

前職で不満だった上長のご機嫌取りとかする必要もないし、クラウドサービスも自由に使えたし、開発する時間も増えた。

一緒に働く人はいい人が多くてワイワイやれて楽しかったし、自分の前に入った2人の先輩には特にお世話になりました、感謝しています。

教育という事業は素晴らしいと心から思うし、子供たち、学校の先生のために頑張っていってほしい。

次は

今まではひたすらサービス開発をしていたので、そうじゃなくて開発者がサービス開発に集中できる環境づくりの手助けをしたいなと思って iOS の技術基盤をやることになっている。といっても別に iOS じゃなくても良くて、 Android の人が足りないならやりたいしサーバ側が足りないならそっちをやってもいい。

技術基盤というとメモリやOSなども詳しくないとダメなのでは?という気がして戦々恐々としている。 Objective-C も全然出来ないので不安である。サービス開発をメインやってきた人という目線で技術基盤として貢献できればと思っている。

前回の事業はドッグフーディング出来なかったけど次のはできるので使って改善していきたい。

ほしい物リスト

www.amazon.co.jp

fastlane pem で Push 通知に必要な p12 ファイルを生成する

iOS 用の Push 通知 証明書を生成したい。 真心を込めて手動で生成することもできるが、ここでは fastlane pem でコマンドで生成する。

fastlane pem

fastlane/pem at master · fastlane/fastlane

詳細は上の README みればやり方書いてる。

gem install fastlane

$ gem install pem

pem

  1. コマンドラインpem と打つ
  2. Apple ID 聞かれるので入力する
  3. Apple ID パスワード聞かれるので入れる
    • fastlane を信用する、不安なら OSS なのでコードを読むべし
  4. bundle id を聞かれるので該当のアプリの bundle id を入れる
  5. 証明書の期限が 30 日以内の場合、p12 ファイルが吐かれる
    • –force オプション使うと 30 以上でもファイルが吐かれるっぽい

下記のようなログと流れになる。

$ pem
+------------------+-------+
|  Summary for PEM 1.4.0   |
+------------------+-------+
| development      | false |
| generate_p12     | true  |
| force            | false |
| save_private_key | true  |
| output_path      | .     |
+------------------+-------+

[14:56:45]: To not be asked about this value, you can specify it using 'username'
Your Apple ID Username: Your Apple ID Username
[14:56:53]: Starting login with user 'Your Apple ID Username'
-------------------------------------------------------------------------------------
The login information you enter will be stored in your Mac OS Keychain
You can also pass the password using the `FASTLANE_PASSWORD` environment variable
More information about it on GitHub: https://github.com/fastlane/fastlane/tree/master/credentials_manager
-------------------------------------------------------------------------------------
Password (for Your Apple ID Username): *************
[14:57:01]: Successfully logged in
[14:57:02]: To not be asked about this value, you can specify it using 'app_identifier'
The bundle identifier of your app: jp.starhoshi.hoge
[14:57:18]: Existing push notification profile 'jp.starhoshi.hoge' is valid for 135 more days.
[14:57:18]: You already have a push certificate, which is active for more than 30 more days. No need to create a new one
[14:57:18]: If you still want to create a new one, use the --force option when running PEM.

あとは p12 ファイルを Amazon SNS とかにあげれば OK !