Hituzi Ando's Blog

日々のアプリ開発についてや雑記など。

Railsアプリで使っているライブラリのバージョンを一覧表示する

こんにちは、アプリ開発をしている@HituziANDOです。

受託開発で仕事を受けたりしていると、提出するドキュメント(設計書など)に、ソフトウェアバージョンなどの記載を求められることがあります。

今日もRailsで作ったアプリケーションで使用しているライブラリ一覧を記載する必要がありました。毎回やり方を調べている気がするので、コマンドをメモしておきます。

bundle installでインストールしたgem

コマンド
bundle exec gem list
出力例
actioncable (5.2.3)
actionmailer (5.2.3)
actionpack (5.2.3)
actionview (5.2.3)
activejob (5.2.3)
activemodel (5.2.3)
activemodel-serializers-xml (1.0.2)
activerecord (5.2.3)
...

yarnでインストールしたパッケージ

コマンド
yarn list --depth=0

(依存関係の深さは0にしていますが目的に合わせて変更)

出力例
yarn list v1.21.1
├─ @babel/code-frame@7.5.5
├─ @babel/core@7.6.4
├─ @babel/generator@7.6.4
├─ @babel/helper-annotate-as-pure@7.0.0
├─ @babel/helper-builder-binary-assignment-operator-visitor@7.1.0
├─ @babel/helper-call-delegate@7.4.4
├─ @babel/helper-create-class-features-plugin@7.6.0
├─ @babel/helper-define-map@7.5.5
...

あとは出力されたリストを適宜加工してドキュメントに貼り付ければ完了です。

【iOS】半強制アップデートの仕組みをカジュアルに実装する

この記事では、アプリを起動したとき、新バージョンがリリースされていることをユーザに伝え、アップデートを促す仕組みの実装方法を紹介します。

強制アップデートはゲームアプリなどでよく見る仕組みですね。強制力の強いものだと、アップデートするまで、アプリを利用できなくするものもありますが、必ずしもアップデートできる通信環境に居るとは限らないため(地下鉄に乗っているときなど辛い)、アップデートを促すまでに留めた半強制くらいが僕は好みです。

アプリのバージョンに乖離が生じると、最新機能を提供できないのはもちろんのこと、既存機能を改修しづらくなってきます。例えば、DBのマイグレーションを何世代かに渡って行っているとき、バージョンが飛んでいると思わぬバグを踏んだりします。

アップデートは放っておいてもされていくものと思っていましたが、計測してみると意外にも乖離していることが分かりました。僕が開発しているiOSアプリのBlue Sketchの例を見ても、バージョンのばらつきが見受けられます(離脱ユーザも含まれているため、正確な数字ではないですが)。

ver. インストールされている割合
1.0.4 3%
1.0.5 9%
1.0.6 51%
1.0.7 5%
1.0.8 32%

Blue Sketchのインストールバージョンの分布

この差を埋めるため、以下のようなAppStoreクラスを実装しBlue Sketchにも導入しています。

import Foundation
import Alamofire

typealias LookUpResult = [String: Any]

enum AppStoreError: Error {
    case networkError
    case invalidResponseData
}

class AppStore {

    private static let lastCheckVersionDateKey = "\(Bundle.main.bundleIdentifier!).lastCheckVersionDateKey"

    static func checkVersion(completion: @escaping (_ isOlder: Bool) -> Void) {
        let lastDate = UserDefaults.standard.integer(forKey: lastCheckVersionDateKey)
        let now = currentDate

        // 日付が変わるまでスキップ
        guard lastDate < now else { return }

        UserDefaults.standard.set(now, forKey: lastCheckVersionDateKey)

        lookUp { (result: Result<LookUpResult, AppStoreError>) in
            do {
                let lookUpResult = try result.get()

                if let storeVersion = lookUpResult["version"] as? String {
                    let storeVerInt = versionToInt(storeVersion)
                    let currentVerInt = versionToInt(Bundle.clyr.version)
                    completion(storeVerInt > currentVerInt)
                }
            }
            catch {
                completion(false)
            }
        }
    }

    static func versionToInt(_ ver: String) -> Int {
        let arr = ver.split(separator: ".").map { Int($0) ?? 0 }

        switch arr.count {
            case 3:
                return arr[0] * 1000 * 1000 + arr[1] * 1000 + arr[2]
            case 2:
                return arr[0] * 1000 * 1000 + arr[1] * 1000
            case 1:
                return arr[0] * 1000 * 1000
            default:
                assertionFailure("Illegal version string.")
                return 0
        }
    }

    static func open() {
        if let url = URL(string: storeURLString), UIApplication.shared.canOpenURL(url) {
            UIApplication.shared.open(url)
        }
    }
}

private extension AppStore {

    static var iTunesID: String {
        "<YOUR_ITUNES_ID>"
    }

    static var storeURLString: String {
        "https://apps.apple.com/jp/app/XXXXXXX/id" + iTunesID
    }

    static var lookUpURLString: String {
        "https://itunes.apple.com/lookup?id=" + iTunesID
    }

    static var currentDate: Int {
        let formatter = DateFormatter()
        formatter.calendar = Calendar(identifier: .gregorian)
        formatter.locale = .current
        formatter.dateFormat = "yyyyMMdd"
        return Int(formatter.string(from: Date()))!
    }

    static func lookUp(completion: @escaping (Result<LookUpResult, AppStoreError>) -> Void) {
        AF.request(lookUpURLString).responseJSON(queue: .main, options: .allowFragments) { (response: AFDataResponse<Any>) in
            let result: Result<LookUpResult, AppStoreError>

            if let error = response.error {
                result = .failure(.networkError)
            }
            else {
                if let value = response.value as? [String: Any],
                   let results = value["results"] as? [LookUpResult],
                   let obj = results.first {
                    result = .success(obj)
                }
                else {
                    result = .failure(.invalidResponseData)
                }
            }

            completion(result)
        }
    }
}

App Storeでリリースされているバージョンの確認には、iTunes Search APIを利用しています。このAPIを使うと、App Storeで配信されているアプリの情報を無料で取得することができます。また、通信ライブラリはAlamofire v5を使用しています。

iTunes Search APIを使うことで、アプリが実際にApp Storeから配信されるようになった段階で、取得できるアプリ情報のバージョンが自動的に最新のものになるため、自前で配信バージョンを管理するサーバを立てる必要がありません。最新バージョンの更新し忘れとかも起きません。

<YOUR_ITUNES_ID>の部分にはバージョン情報を取得したいアプリのiTunesIDに置き換えてください。iTunesIDはブラウザでApp Store上のアプリのページを開いたときのURLに表示されています。同様にstoreURLStringも置き換えてください。

f:id:hituziando:20200209154257p:plain
iTunesIDを調べる

上記の実装では1日1回バージョンの更新チェックをするようになっています。バージョン比較は、major.minor.patch形式のバージョン文字列をドットで分割しInt型に変換(versionToInt(_:)メソッドを見てください)したもので比較しています。上記のやり方ではminorとpatchは0~999の1000ステップしか値を取れませんが十分でしょう。

AppStoreクラスを使うときはViewControllerで以下のようなメソッドを作っておき、viewDidAppearUIApplication.willEnterForegroundNotificationをオブザーブしたメソッドなどで呼び出します。

private extension ViewController {

    func checkVersion() {
        AppStore.checkVersion { (isOlder: Bool) in
            guard isOlder else { return }

            let alertController = UIAlertController(title: "新しいバージョンがあります!", message: "アップデートしてください。", preferredStyle: .alert)
            alertController.addAction(UIAlertAction(title: "アップデート", style: .default) { action in
                AppStore.open()
            })
            alertController.addAction(UIAlertAction(title: "キャンセル", style: .cancel))
            self.present(alertController, animated: true)
        }
    }
}

以上の実装で、新しいバージョンがリリースされたことをユーザに伝えることができるようになりました。半強制アップデートの仕組みを導入したことでアップデートが促進されるか楽しみです。

Real TODO ver.2.2.0がリリースされました!

f:id:hituziando:20200203170338p:plain

個人開発しているiOSアプリ『Real TODO』のver.2.2.0がリリースされました!

Real TODOとは

シンプルなToDoリストアプリです。いつまでに何をやるかだけにフォーカスしたToDoリストが作れます。タスクリストは期限が近い順に自動的に並ぶので、上から順番にタスクをこなしていけます。

アップデート内容

今回の主なアップデートは以下の2点です。

  1. タスクごとに通知のオン/オフが設定できるようになりました
  2. 期限日がわかりやすくなりました

1. タスクごとに通知のオン/オフが設定できるようになりました

今まではアプリ全体での通知設定しかできませんでしたが、新バージョンではタスクごとに通知のオン/オフ設定ができるようになりました。これでアラームの必要がないタスクは通知オフにできます。

なお、現状はタスクの期限時刻の2時間前に通知される仕様ですが、将来的には10分前、1時間前など任意の時間に通知設定ができるようにするつもりです。機能の実装自体は簡単ですが、UIをどうするかで悩んでいます。

f:id:hituziando:20200203173354p:plain
アプリ画面1

2. 期限日がわかりやすくなりました

個人的にこのアプリを使ってて、期限日がわかりにくい、特に曜日の表示がほしいと思い、以下のスクショのように日時の表記を修正しました。フォントサイズ自体は小さくなりましたが、太字黒色にしたことで結果的に見やすくなりました。こうして見ると、曜日の表示って重要だなと思います。曜日があるのと無いのとで認識が全然違う。

f:id:hituziando:20200203172738p:plain
アプリ画面2

その他、設定ボタンを小さくしたり(設定画面は普段あまり見られない画面のため、歯車アイコンは控えめなサイズにしよう!)など、軽微な修正を含んでいます。

Real TODOはApp Storeで配信中です。よかったら下記リンクからインストールしてみてください!

Real TODO

Real TODO

  • Hituzi ANDO
  • 仕事効率化
  • 無料
apps.apple.com

2020年1月のアプリ収益は1,631円でした。

2020年1月の個人開発しているアプリ収益は1,631円でした。

内訳は以下の通りです。

App PF 収益(円) ソース
Real TODO iOS 1,150 動画リワード広告収入
メモアプリ Android 481 バナー広告収入

Real TODOについて

1月はReal TODOの売上が伸びました。詳細分析はこれから(分析対象のデータをまだ収集できていない)ですが、Real TODOの総ダウンロード数は260DLくらいで、実際利用しているユーザ数は17人程です。利用率としては7%くらいなので、ちょっと低いですね。世の中にToDoリストアプリは腐るほどあるので、この分野で数字を伸ばすのはかなり厳しいですが、それでも継続して使ってくれる人が増えてきている感じはするのですが…圧倒的に母数が少ない。

メモアプリについて

メモアプリの現在の有効なインストール数は367で、毎日60人程に使われています(有難い!)。利用率は16%で、ほぼ横ばいです。

*有効なインストール数とは、アクティブなインストールデバイス数(過去 30 日間に使用されたデバイスのうち、このアプリがインストールされているデバイスの数)です。

f:id:hituziando:20200201112123p:plain
MemoApp - Google Analytics - 2020/01

こちらの記事(https://www.eda-inc.jp/post-3428/)では、

アプリをインストール後もそのアプリを使い続けるユーザーの割合を調べた「継続率」も報告されています。それによるとグローバル平均は21%で、約8割のアプリはインストール後7日経つと使われなくなってしまうということがわかります。

と記載されており、継続率はグローバル平均の21%には届かず、また「ツール」カテゴリにおいては19%ということから、ほぼ実態に近い数字になるかなと思います。逆に言うと、今の母数のままでは、これ以上大幅な増加は見込めないということです。

まとめ

1,600円/月の売上が発生すると仮定すると、19,200円/年なのでApple Developer Programの年会費分よりは稼げることになります。

売上10倍になればハイスペックiPhoneが買え、100倍になればハイスペックMac PCも買えます。

当面、目指すところは継続して使ってくれるユーザ数を100倍にすることですね。メモアプリは、有効なインストール数36,700DL/アクティブなユーザ数6,000人、Real TODOは、利用率を13~14%まで上げ、有効なインストール数13,000DL/アクティブなユーザ数1,700人が目標です。

その他のトピック

昨年末のQiitaのアドベントカレンダーのネタで作ったMacアプリ Blue SketchMac App Storeの「仕事効率化」カテゴリで131位に(瞬間的ではありますが)ランクインしました。

f:id:hituziando:20200201173826p:plain
Mac App Storeの「仕事効率化」カテゴリのランキング 2020/01/31

意外とインストールされているなと思っていましたが、Storeからの導線でしょうか。ただ、継続率は著しく低い。ランキングにあるからインストールしてみたものの、なんだこの糞Appってなって使われなくなるのが目に見えます。けっこうインストールされるならば、ブラッシュアップして収益性を考えても良いかもしれない。

メモアプリver2.1.1をリリースしました。

f:id:hituziando:20200127225206p:plain

個人開発しているAndroidアプリ『メモアプリ』のバージョン2.1.1をリリースしました。

メモアプリとは

シンプルなメモ帳アプリです。 ステータスバーに常駐しているため、ステータスバーからすばやく起動して、パッとメモすることができます。 瞬間的に思いついたアイデアを残したり、ToDoリストや買い物メモとしても使えます。

アップデート内容

今回のアップデートは、フィルターボタンの位置の修正と軽微なものです。

f:id:hituziando:20200127230840p:plain
アプリ画面

従来のボタンの位置では、メモの大部分と被ってしまい、読みづらくしていました。今回の修正では、ボタンを右側に配置し、ボタンサイズも少し小さくしたため、従来よりもメモが読みやすくなっていると思います。ボタンのデザインも少しカッコよくしました。

メモアプリ(英名: ToDo List)はGoogle Playで配信中です。よかったら下記リンクからインストールしてみてください!

play.google.com

コンビニで売っているほうじ茶チーズティーにハマっています。

f:id:hituziando:20200125181332j:plain
ほうじ茶チーズティー

最近、と言っても今週からハマっているのが、↑写真の『ほうじ茶チーズティー』(販売者: 株式会社エルビー)です。セブンイレブンで売られています(他のコンビニではまだ見たことがない)。

昨年、タピオカミルクティーの次に来るのはチーズティーと言われているとか言われていないとかで、何回か飲んだことがあるのですが、そのチーズティーがコンビニで売られているのを見つけ、つい買ってしまいました。

その味は、一言でいうと、めちゃ美味しい!です。僕の奥さんも美味しいと言って飲んでいます。

プロセスチーズが入っているようですが、チーズ感はあまり無く、味がしつこくないので、とても飲みやすいです。逆に、チーズ臭さというかチーズ感を求めている人には物足りないかもしれません。

値段は194円(税込)くらいなので、THE ALLEYとかで買うよりは断然安いです。

毎朝、出社前にセブンに寄って買って、職場で飲んでいます。前述したように、味がしつこくないので、朝からでも全然飲めます。(昨日は売り切れで買えませんでした。やはり密かに人気が出ている!?)

しばらくはコレを飲んでプログラミングする日が続きそうです。

/tmpのパーミッションが原因だった(?)

2日間くらい以下の事象に悩まされていました。とても時間を浪費した気がするので、また同じ事象が生じたとき用に備忘録を残しておきます。

いずれもいつの間にかできなくなっていました。ちなみに、使っているPCはMacBook Air (Retina, 13-inch, 2018)でOSバージョンはmacOS 10.15.2です。

結論から言うと

タイトルにもあるように、/tmpのパーミッションが原因だったようです

曖昧な表現なのは、色々やっていて、いつの間にか治った感があり、結局、/tmpのパーミッションが原因だったか確認できていないためです。けっこう焦っていたのでBefore/Afterの状態遷移を確認していなかったです…

事象

VPN接続できない!

これは非常に焦りました。VPN接続できないと仕事に支障をきたすためです。

最初は接続先の設定が変わったのかな、と疑いましたが、他の人は接続できているのと、iOS版のCisco AnyConnectアプリを使えば自分のアカウントでも接続できたため、これは自分のMacBookに原因があるのだなと切り分けはできました。

App StoreiOSアプリをアップロードできない!

このiOSアプリは個人開発しているアプリなので、緊急性は低かったのですが、早くリリースしたいという思いもあり、アップロードできないことにイライラしていました。

f:id:hituziando:20200124001051p:plain
App Store Connect Operation Error

ERROR ITMS-90725は、iOS12.1以上をターゲットにビルドされたアプリでないとアップロードできないというエラーなのですが、いや、Deployment Target=iOS12.1でやっているし…

Xcodeの再起動、クリーンビルド、Provisioning Profileの更新など思いつくことは一通りやってみたのですが、いずれもダメでした。

Photoshopが起動できない!

起動しようとすると、100%応答なしになる。何故だ…

やってダメだったこと

  • アプリの再起動
  • MacBookの再起動
  • ディスクユーティリティのFirst Aid
  • セーフモードでの起動からの再起動
  • SMCリセット

Macがおかしいかな、というときに試すことはやってみましたが、いずれも効果なしでした。

突然、光が見えてくる

事象が解消されず悶々としていましたが、他の仕事も進めないといけないため、その作業をしているとき、偶々、以下のMySQLが起動できないとうエラーに遭遇しました。

ERROR: for mysql  Cannot start service mysql: failed to create OCI runtime console socket: mkdir /tmp/pty250188720: permission denied: unknown
Encountered errors while bringing up the project.

mkdir /tmp/pty250188720 の権限が無いというエラーなので、以下のコマンドで権限を与えてやりました。

sudo chmod 1777 /private/tmp

# /tmpが無いときは作ればいいみたい
sudo mkdir /private/tmp
sudo chown root:wheel /private/tmp
sudo chmod 1777 /private/tmp
sudo ln -s /private/tmp /tmp

これでMySQLは無事起動できました。

この時、ふと思ったのです。/tmpって上記の事象のときにも使ったりするんじゃないかと。そこで試してみたところ、全事象がキレイに解消されているではないですか! (これか、これが原因なのか!?)

/tmpのパーミッションなんて自分で変更することはまず無いので、いったいいつ誰が変更したのか…

結局、何かよくわからないですが、もしまた同じような事象が発生したとき、チェックポイントの一つとして覚えておこうと思います。