20200317のiOSに関する記事は6件です。

XcodeGenのちょっとしたTips

XcodeGenの地味なTipsについてまとめました。

XcodeGenのBuild Configurationsの階層について

XcodeGenのビルド設定は以下の階層になっています。これを最初に知っておくとスムーズです。
公式ドキュメントの目次の構成がproject.ymlの構成のそれなので、そのまま参考にすると良いかと思います。

# Project全体の設定
settings:
  # Projectの全Build Configurationsに反映される設定
  base:
  # Projectの各Build Configurationごとの設定
  configs:
    Debug:
<中略>
# Targetごとの設定
targets:
 {Target名}:
    # Targetの全Build Configurationsに反映される設定
    base:
    # Targetの各Build Configurationごとの設定
    configs:
      Debug:

cacheを利用する

xcodegen --use-cacheでXcodeGenのキャッシュが効くようになります。

キャッシュファイルは --cache-path のオプションで指定しない限り、~/.xcodegen/cache/{xcodeprojファイルのpathのMD5ハッシュ値}に保存されます。

  • ファイル構成(追加・削除)
  • project.yml

に変更がない場合、キャッシュが効きます。
実際は、一時的に生成したcacheファイルと実在するキャッシュを比較して差分がなければ何もしない処理になってます。

キャッシュの判定処理はここ

project生成後に何かしらのコマンドを実行する

postGenCommandのオプションで指定ができます。(例えばpod installなど)
これと上記のキャッシュを利用すると、ローカルのproject構成に変更がない場合、xcodegenコマンドも実行されないし、合わせてpod installも実行されないので省エネです。
例えばgit checkoutを頻繁に繰り返す時など地味にこの設定が効くと思います。

※公式でもオススメされてます (https://github.com/yonaskolb/XcodeGen/blob/master/Docs/FAQ.md#can-i-use-cocoapods)

options:
  postGenCommand: pod install

一部の値だけBuildConfig間で共有する

単純にprojectファイルを編集する場合だと複数のターゲットでコピペが発生してしまうところを、Setting GroupsTarget Templateなど使って、共通化できる点もXcodeGenの魅力かと思います。

それ以外にもyamlの記法を使って一部のパラメータだけ共有するようなこともできます。
例えばyamlのアンカー記法を使うと変数を参照するような書き方もできます。

ATarget:
  CURRENT_PROJECT_VERSION: &a_build_number 1
  <中略>
BTarget:
   CURRENT_PROJECT_VERSION: *a_build_number

他にも何かあれば追記します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オンラインSwiftコード生成ツールをつくった

概要

ブラウザで利用できるSwiftコード生成ツールを作成しました。
https://shtnkgm.github.io/SwiftCodeGenerator/

↓こんなの

SwiftでTDDを進めるにあたり、Mockとなるインスタンスの生成コードを書くのが大変だっため、Webツールを作成しました。
SourceryやPure Swiftでやる方法も検討しましたが、Vue.jsでコードも書きたかったため、Web技術で作成しました。

できること

以下のような型定義を入力フォームにコピペすると、

struct Book {
    let price: Int
    let title: String
}

以下のようなコードを出力します。

メンバーワイズイニシャライザ

extension Book {
    init(
        price: Int,
        title: String
    ) {
        self.price = price
        self.title = title
    }
}

ファクトリメソッド(適当な値でインスタンスを生成するmakeメソッド)

extension Book {
    static func make(
        price: Int = 0,
        title: String = ""
    ) -> Book {
        return Book(
            price: price,
            title: title
        )
    }
}

Codableに準拠するための実装

extension Book: Codable {
    enum CodingKeys: String, CodingKey {
        case price
        case title
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        price = try container.decode(Int.self, forKey: .price)
        title = try container.decode(String.self, forKey: .title)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(price, forKey: .price)
        try container.encode(title, forKey: .title)
    }
}

Equatableに準拠するための実装

extension Book: Equatable {
    static func == (lhs: Book, rhs: Book) -> Bool {
        return
            lhs.price == rhs.price &&
            lhs.title == rhs.title
    }
}

構成

コード生成処理はクライアントサイドで行うため、かなりライトな構成です。

試してみてください

β版で不具合や足りない点もあるかと思いますが、活用いただけたら幸いです。

Swift Code Generator
https://shtnkgm.github.io/SwiftCodeGenerator/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

まだStoryboardの多言語対応で消耗してるの?ローカライズの最強ベストプラクティス対応法

こんにちは。もぐめっとです。

Qiita初投稿です。

先日、大先輩のfmtonakai大師匠からすごい目からウロコのstoryboardのローカライズ対応について教えてもらいました。
普段は自分のブログに書いているのですが、あまりにも目からウロコ過ぎたのでQiitaで共有しておきます。

storyboardにローカライズのキーを指定できるようにする

UILabel+Extension.swift
extension UILabel {
    @IBInspectable
    private var localizedKey: String? {
        get { fatalError("only set this value") }
        set {
            if let newValue = newValue {
                text = newValue.localized()
            }
        }
    }
}
String+Extension.swift
extension String {
    func localized() -> String? {
        return NSLocalizedString(self, comment: "")
    }
}

こう書いておくだけで、Android Studioのようにstoryboardからキーを設定してローカライズすることができるようになります。

image.png

すごい!画期的!!

ローカライズのキーtypoを防ぐ その1

このままだと、typoとかしたときにローカライズのキーがそのまま表示されてしまうのでローカライズ漏れに気づけるように改良してみました。

String+Extension.swift
extension String {
    private static let localizedEmptyKey = "##not exists##"
    func localized() -> String {
        let string = NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: String.localizedEmptyKey, comment: "")
        if string == String.localizedEmptyKey {
            fatalError("not exists localized key")
        }
        return string
    }
}

キーが無かった場合には強制的にアプリを落としてしまうことによってキーの漏れに気づけるようになりました。

ローカライズのキーtypoを防ぐ その2

その1の対応だと、画面を表示したときでないとtypoに気づけません。

そこで、ビルド時にキーのチェックをしてtypoに気づけるようにさらに改良してみました。

Build Phasesに下記スクリプトを追加するだけ!

RunScript.sh
#!/bin/bash

for file in `\find . -name \*.storyboard`; do
    IFS=$'\n'
    for xmlKey in `\grep 'keyPath="localizedKey"' ${file}`; do
        localizedKey=`echo $xmlKey | sed -e 's/.* keyPath="localizedKey" value="\([0-9a-zA-Z_-]*\)".*/\1/g'`
        for localizedStringFile in `\find ${SRCROOT} -name Localizable.strings`; do
            grep "\"${localizedKey}\" =" $localizedStringFile > /dev/null 2>&1
            if [ $? != 0 ]; then
                echo "not exists key '${localizedKey}' in ${localizedStringFile}"
                exit 1
            fi
        done
    done
done

storyboardで設定されているlocalizedKeyをひっぱてきてLocalizable.stringsのファイルと突き合わせて存在しなければエラーを吐き出します。

まとめ

Android Studioに比べるとxcodeで至らぬところというのはまだまだありますが、今回の対応でLocalizable.stringsに文言を集約することができるようになり、よりローカライズがしやすくなってとってもウロコな方法でした。

storyboardでのローカライズはこの方法でやって、コードでの動的なローカライズについてはR.swiftやswiftgenなどで対応していって適材適所に使ってローカライズしていけるといいと思います。

今回の検証コードはこちらにおいてあります。

Special Thanks fmtonakai

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

`array.sorted(by: key)` 的なことがしたかったんだ

概要

キーを指定するだけでサクッとソートしてくれるようなextensionを作ってみた

実装

Sequence+KeySort.swift
extension Sequence {
    public func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
        sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
    }

    public func sorted<T: Comparable>(by keyPath: KeyPath<Element, T?>) -> [Element] {
        sorted {
            guard let l = $0[keyPath: keyPath],
                let r = $1[keyPath: keyPath] else { return false }
            return l < r
        }
    }
}

Usage

struct Person: CustomStringConvertible {
    var id: Int
    var name: String

    var description: String { name }
}

let people: [Person] = [
    .init(id: 3, name: "Bob"),
    .init(id: 1, name: "Emma"),
    .init(id: 4, name: "Amelia"),
    .init(id: 2, name: "George"),
]

print(people.sorted(by: \.id))   // -> ["Emma", "George", "Bob", "Amelia"]
print(people.sorted(by: \.name)) // -> ["Amelia", "Bob", "Emma", "George"]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Background URLSession

この記事は

iOSで利用可能なバックグラウンド処理の1つである「URLSessionのバックグラウンドモード」 についてのまとめと挙動の検証です。

機能概要

Background URL Session

  • 特徴
    • URLSessionの実行モードの1つ
    • 通信を開始した後バックグラウンドに移行しても、継続して通信処理を実行させ続けることができる
    • アプリがフォアグラウンドのままでも処理を実行することはできる
  • 実行タイミング
    • タスクの実行命令を出した直後
  • 実行可能時間
    • 環境によって変わる(明記されている公式ドキュメントを見つけることができなかった)
      • UIApplication.shared.backgroundTimeRemaining で取得できる
      • 検証環境(iPhone11Pro iOS13)では30secだった
  • 所感
    • Backgorund Task Completion を使ってもある程度同じようなことは実現できるので、正直あまり使い所が思い浮かばなかった
    • 後述する Discretionary Background URL Session の方が使い所がありそう

Discretionary Background URL Session

  • 特徴
    • Background URL Sessionの実行オプションの1つ
      • URLSessionConfigurationの isDiscretionary をtrueに指定することで、このモードにすることができる
    • 通信の開始タイミングを遅延させることができるので、即時で必要でない処理を先延ばしできる
    • 実行リクエストはフォアグラウンドで行うが、タスクの実行自体はアプリとは別のバックグラウンドプロセスで行われる
    • アプリが停止状態の時に処理が完了しても、システムがバックグラウンドでアプリを再開または起動してくれる
      • この挙動を実現するには sessionSendsLaunchEventsがtrueになっている必要がある
      • ※ ユーザーによって明示的にアプリがkillされていた場合は実行されない
  • 実行タイミング
    • 正確に指定することはできず、ある程度の実行開始条件を事前に与えておくことしかできない
    • 与えられた条件を考慮して、システムが最適なタイミングを判断して処理を実行する
  • 実行可能時間
    • Background URL Sessionと同じ
  • 所感
    • アプリがフォアグラウンド状態の時に、システムリソースを消費させてまで行いたくない処理を実行するのに適している
    • 確認した限りでは簡単にデバッグする方法は特に提供されていなさそうだった
      • 「スケジューリングしたあと実行されるのを待つ」しかなさそうなので、テストが辛そう

サンプルコード

https://github.com/chocoyama/BackgroundSamples/blob/master/BackgroundSample/Views/URLSessionView.swift
https://github.com/chocoyama/BackgroundSamples/search?q=handleEventsForBackgroundURLSession&unscoped_q=handleEventsForBackgroundURLSession

URLSessionDownloadTask を利用しています

Background URL Session の場合

バックグラウンドのセッションを作成・実行する。この時、クロージャではなくdelegateで設定を行う。

let config = URLSessionConfiguration.background(withIdentifier: UUID().uuidString)
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)

session.downloadTask(with: URLRequest(url: url)).resume()

ダウンロード処理が完了すると、下記のDelegateメソッドが呼び出される。
ダウンロードされたデータは、引数に受け渡される location のURLに配置されたファイルから参照することができる。
このデータはメソッドの終了と共に利用できなくなるので、メソッド外でも利用したい場合は別のファイルに退避させるなどの対応が必要になる。

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
    let jsonString = try! String(contentsOf: location)
    NotificationHelper.postLocalNotification(with: Message(body: jsonString))
}

Discretionary Background URL Session の場合

実行が遅延されるため、実行時にはすでにアプリが停止されている可能性がある。
そのため、フォアグラウンド時に生成したセッションがすでに失われている可能性があり、セッションを再生成しないとリクエスト時に設定していたdelegate処理を実行することができない。
※ 処理自体はバックグラウンドの別プロセスで実行されているので、正しくセッションの復帰を行えば、設定したdelegate処理を実行させることができる。

以下の対応を行うことで、通信完了時などに呼び出されるdelegate処理をバックグラウンド時でも実行させられる。

  1. URLSessionの復帰
  2. システムへの復帰処理完了通知

1.URLSessionの復帰

UIApplicationDelegateには、 handleEventsForBackgroundURLSession というメソッドが定義されている。
このメソッドはバックグラウンドで通信処理が完了した後、システムがアプリを起動して呼び出すもの。
引数としてセッションIDが受け渡されるので、これを用いて再度URLSessionを起動してセッションの再生成を行うことができる。

※ URLSessionの再生成処理は必ずしもこのメソッド内で行う必要はない。
別の起動ロジックの中で生成時と同一のSessionIDでURLSessionを起動している箇所があれば、そちらで再生成を担保することも可能。

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    self.backgroundCompletionHandler = completionHandler

    // 必要に応じてここで再生成する
    // let config = URLSessionConfiguration.background(withIdentifier: "some unique identifier")
    // let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
}

また、ここで受け渡される completionHandler は、実行後にURLSessionのdelegateメソッド(didFinishDownloadingToLocation など)が呼び出されることになる。
そのため、URLSessionの再生成処理が終わった段階で呼び出す必要がある。
別のクラスなどでURLSessionの再生成を行っている場合は、一旦プロパティなどに保持しておくことで後からこの完了ハンドラを呼び出せるようする。
その後、再生成が完了したタイミングで呼び出すことで、想定した動作にすることができる。

2. システムへの復帰処理完了通知

特定のURLSessionに関する全てのイベントが実行されたあとは、 NSURLSessionDelegateurlSessionDidFinishEvents が呼び出される。
このタイミングではURLSessionの再生成処理が終わっているので、保持しておいた handleEventsForBackgroundURLSession の完了ハンドラを実行する。
完了ハンドラが呼び出されたあとは didFinishDownloadingToLocation が呼び出されるので、通常のBackgroundURLSessionと同一の処理を実行すれば良い。

func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    DispatchQueue.main.async {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
        appDelegate.backgroundCompletionHandler?()
        appDelegate.backgroundCompletionHandler = nil
    }
}

検証

準備

検証環境
iPhone11Pro iOS13.3.1 (実機)
iPhone11ProMax iOS13.3.1 (Simulator)

ローカルに簡易的なAPIサーバーをたてて重たいAPIをシミュレートしながら検証を行った。

// Express
router.get('/', function(req, res, next) {
  setTimeout(() => {
    res.send(JSON.stringify({'name': 'SampleName'}));
  }, 10000);
});

結果

↓のサンプルコードで実行した結果です
https://github.com/chocoyama/BackgroundSamples/blob/master/BackgroundSample/Views/URLSessionView.swift

フォアグラウンドモードで通信開始

  • 実行直後にバックグラウンドに移行した場合、バックグラウンド状態では処理が停止された
  • ただし、すぐにフォアグラウンドに復帰させると、中断されていた処理が再開する挙動になった

バックグラウンドモードで通信開始

  • 実行直後にバックグラウンドに移行しても、バックグラウンド状態で処理が継続された

バックグラウンドモード & isDiscretionary=trueで通信開始

  • 実行直後にバックグラウンドに移行した時、処理が遅延されたことが確認できた(すぐに実行されなかった)
  • バックグラウンドに移行後、フォアグラウンドに復帰しても処理は実行されなかった
  • スケジューリングをしたあと、しばらく待つと通信処理が実行されたことが確認できた(実機検証)

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリのDL数を爆速で伸ばそうとした話

概要

個人開発のiOSアプリのダウンロード数を伸ばしてみようと思い、いろいろなことにチャレンジした結果をまとめています。
結論から言うとそんなに伸びませんでした
しかし、やったことをそのままにしておくのは勿体無いので、記事としてまとめておきます。
少しでもお役に立てれば幸いです。

この記事の主なターゲット

私と同様、個人でiOSアプリを開発していて、DL数をさらに伸ばしたい方を対象にしています。
すでに何万DLもされているようなアプリにはあまり効果がないかもしれません。
およそ1日100DL未満くらいのアプリ開発者の方がターゲットです。

本記事で取り上げるアプリ

アプリ名:GPS Measure

https://apps.apple.com/jp/app/gps-measure/id1439677549

簡単に紹介すると、GPSを用いて移動速度や移動距離をサクッと計測するためのアプリです。
出来るだけiOSライクなデザインを意識して、標準アプリのように使えることを心がけています。無料です。

背景

このアプリを初回リリースしたのが、2018年10月22日。およそ1年5ヶ月前になります。
そこから1年間(2018/10/22~2019/10/21)でのDL数はなんと2688件でした。1日平均、約7.36件。

みなさんに気軽にDLしてもらいたくて無料にしてみたのに、、、
類似アプリよりも高機能なのに、、、
デザインも悪くないのに、、、(主観)

本当に悔しかったです。
なんとかもう少しDL数を伸ばしたいと思い、いろいろなことにチャレンジしてみることにしました。

やったこと

DL数を伸ばすために以下のことをやりました。
各項目について、紹介していきます。
・問題点の洗い出し
・ダークモード対応
・iPadの画面分割機能対応
・使い勝手の向上
・ユーザーからの意見対応
・多言語対応
・SKStoreReviewControllerを用いたレビュー誘導
・AppStoreの最適化


●問題点の洗い出し

問題なくして改善はありえません。
ダウンロード数を伸ばす試みとして、まず問題点を洗い出し、どんなことをしたらいいか検討しました。

Step1:目標の設定と現状の把握

まずは目標の設定と現状の把握を行いました。
考えた結果、特に理由はないのですが目標を「DL数1日平均100件以上」と定めてみることにしました。
先述の通り、現状は「平均7.36件」でしたので、差分の「93ダウンロード」が目標とのギャップになります。
このギャップを「問題点」とし、それを無くしていく「解決策」を考えました。

Step2:問題点の精査と分類

問題をより細かく分解し、ギャップが生まれてしまっている要因を洗い出しました。
そして、その要因を分類して、こねくりまわして、以下のマトリクスを作りました。
(注意:私はこの分野に関してはど素人です。あくまでも自分の考えを整理するための表なので、間違っている可能性が高いです。)
matrix.jpeg
・存在価値の低いアプリ:AppStoreの検索結果の最下層にいるアプリ。機能もチープで誰もDLしないようなアプリ。
・知る人ぞ知るアプリ:知名度は低いが魅力が詰まっているアプリ。コアなユーザーだけが使ってくれている。DL数は少ない。
・目障りなアプリ:やたら広告を出しているアプリ、また謎にAppStore上位にあるアプリ。いろんな人の目に止まるが、ユーザーの求めているものではなかったり、見た目や機能が乏しいことからDLまではイマイチ結びついていない。母数が大きい分DL数はそこそこある。
・DLしてもらえるアプリ:いろんな人の目にとまり、かつ、機能を適切に理解してもらえ、多くの人にDLしてもらえるアプリ。

Step3:立てた仮説とその検証

マトリクスの左下(現状)から右上のエリアにアプリを持ってくるために、以下の2つに取り組むことにしました。
・魅力度を向上させる
・知名度を向上させる
「この2つを達成できれば、きっとDL数が増えるのではないか」というのが立てた仮説になります。
これ以降の対応内容は、基本的にこの仮説を実行するための手段です。


●ダークモード対応

(対応する仮説:魅力度向上)

この半年間、アプリ開発者の皆様が悩んできたメイントピックだと思います。
まだ必須対応とまでは言われていませんが、類似アプリの中でダークモードに対応しているものが少なかったので、差別化を狙ってダークモード対応を行いました。(勉強の意味も兼ねてたりします)
以下、自分がやったやり方を簡単に紹介します。

OS標準の色

こちらの対応方法は想像より簡単でした。
まず、OSが提供している標準のカラーセットに対しては、大抵「System xxx color」という色がXcode11から提供されています。
例えば、背景色でしたら以下のものが提供されています。
backgroundcolor.png
基本は、これらの色に変えていけばOKだと思います。
赤や青などの色もこのSystem xxx colorが用意されているので、そちらに変えるのがおすすめです。

アプリで独自に定義している色

こちらは一手間かかります。
まず、Assets.xcassetsに行き、New Color Setを選択して新しいカラーセットを作成します。(ここではOriginalRedColorという名前にします)
newcolor.png

次に、AttributesからAppearancesを選択し、「Any, Dark」を選択します。
iOS12以前のモデルに関してはダークモードという概念がないのでAnyの色が選択され、iOS13以降ではLightとDarkで切り替わります。
Any, Darkにしておけば、iOS13以降のライトモードではAny側の色になります。
特別理由がなければAny, Darkで良いと思います。
anydark.png

最後にそれぞれのAppearanceに対して適切な色を選びます。
下の例では極端に色を変えていますが、Appleのガイドラインを見る限りでは、若干濃くするくらいが正解な気がします。(どなたか適切な色の作り方をご存知の方はぜひ教えてください!)
appearance.png
このように設定しておくことで、OS標準のカラーセットと同じようにOSのモードに合わせて動的に変わる独自のカラーセットが出来上がります。


●iPadの画面分割機能対応

(対応する仮説:魅力度向上)

これは必須対応案件なので本題から少し外れてしまうのですが、2020年4月までに以下の3つに対応しなくてはいけないとAppleからアナウンスされています。
・ Adopt Launch StoryBoards
・ Support any size
・ Support Split Screen Multitasking1
参考→WWDC2019 Modernizing Your UI for iOS 13

私の場合、「Launch Storyboard」と「Support Any Size」には対応してあったので、まだ対応していなかった残りの「Support Split Screen Multitasking」(iPadの画面分割機能)に今回対応しました。

結論だけ言うと、
・Targets>GeneralのDevice Orientationの全てにチェック
・Require full screenのチェックを外す
これをすると、すぐに対応できました。
splitScreen.png
Split Screen Multitaskingに対応すると、iPadにおいて様々な画面サイズで動作することになります。
レイアウト崩れがないかは要チェックです。

これに対応したおかげで、iPadでの使い勝手が上がったと思うので、iPadユーザーも取り込めるのではないかと期待しています。


●使い勝手の向上

(対応する仮説:魅力度向上)

アプリ開発においてUIやUXについて考えることは非常に大切です。
このアプリは、元々シンプルなデザイン&機能なのですが、より使い勝手を向上できるところはないか徹底的に考えました。
その結果、地図の拡大画面でできることを増やしてみて、使い勝手を上げられないか挑戦してみることにしました。
前のバージョンまでは、常に画面の中心が現在地になるように自動的に追尾させる仕様でした。
しかし、この仕様では今までの移動経路を確認しづらかったので、ユーザーが地図をスライドさせたら自動追尾機能をオフにして、またオンにしたら追尾再開するような仕様に今回変えてみました。

他にも AppleのHuman Design Guidelineや、GoogleのMaterial Designのスペーシングに関するガイドラインなどを参考に、使いやすいUIを取り入れてみました。


●ユーザーからの意見対応

(対応する仮説:魅力度向上)

このアプリをこれまで使ってくれていたユーザーに率直な意見を聞いてみました。
その結果、アプリのコンセプトがブレない、以下の要望に対応することにしました。
・北を「磁北」と「真北」で切り替えられるようにしてほしい
・距離単位に「海マイル(海里)」を追加してほしい

余談:ユーザーの意見を全て取り入れるべきではない

本題から少し逸れますが、私個人のポリシーを記載しておきます。
ユーザーの意見の中には、大幅な改修や機能追加を必要とするものもありましたが、それらは当アプリのコンセプトとは異なっていたため一旦なしにし、コンセプトからブレない範囲での機能追加に絞りました。
なんでもかんでもユーザーの意見を取り入れてしまうのは、危険だと私は考えています。
いわゆる「何でもできるアプリ」は「何にも使えないアプリ」だと思っているため、機能をできるだけシンプルにし、ターゲットユーザーとユースケースを明確にしています。
シンプルなアプリかどうかは、「〇〇な人用の、xxするためのアプリ」と一言で言えるかどうかを判断基準にしています。


●多言語対応

(対応する仮説:魅力度向上、知名度向上)

今までのバージョンでは、日本語と英語の2ヶ国語にしか対応していませんでした。
より多くの言語に対応することで魅力度と知名度の両方の向上が期待できると考え、今回のアップデートでは、新たにスペイン語、フランス語、ドイツ語、中国語(簡体字)に対応してみました。
この言語の選定は、WWDCでAppStore Labという、AppStoreの専門チームの人とマンツーマンで話せる場所にて、アドバイスをいただいた言語を基準にしています。

ちなみにXcodeのローカライズの仕組みを使えば、文言だけでなく画像や色なども言語に合わせて変更することができます。
このアプリでは画像をアイコン以外に用いていないので、文言だけの多言語対応をしました。


●SKStoreReviewControllerを用いたレビュー誘導

(対応する仮説:魅力度向上、知名度向上)

新たな試みとして、レビュー誘導の仕組みも導入してみました。
SKStoreReviewControllerを使えば、簡単にアプリ内からレビューしてもらうことができます。

ただしこれを表示できるのは年間3回までなので、出しどころが鍵を握ります。
私の場合、以下のタイミングで誘導することで、高評価を得ようと企みました。
・5回以上計測を行った、かつ
・2日以上利用した、かつ
・計測を完了した タイミング
低評価をつけるユーザーはそもそも何回も計測しないと推測しており、星1がつけられるくらいなら最初からレビュー誘導させない作戦です。


●App Storeの最適化

(対応する仮説:魅力度向上、知名度向上)

App Storeで見つけてもらえないと、あらゆるユーザーからのダウンロードは見込めません。
しかも、私が作ったアプリは完全に後出しで、類似アプリがいくつもあるような状態です。
そのため「スピードメーター」みたいな一般的なキーワードでは、何回スクロールしてもなかなか出てきませんでした。
これを改善するために、以下の取り組みを行いました。

検索キーワードの多言語化

少しでも上位に表示されるように、検索キーワードの多言語化を行いました。
こうすることで、日/英以外のユーザーが検索した時でも検索にヒットして見つけてもらえる可能性が上がると考えています。

スクリーンショット

目に止まるような工夫として、スクリーンショットにも手を加えています。(ただの画面キャプチャにはしない)
このアプリの場合、スクリーンショットの背景色をオレンジ(ライトモードでもダークモードでも目立ちやすい色)にし、横画面のスクリーンショットにしています。

こうすることで、スクロールした時にパッと目に止まる可能性が高いと考えたからです。
また、ダークモードの画像もスクリーンショットに載せてみました。ないよりはましかなという程度です。

アプリ紹介の多言語化

アプリを見つけてもらっても、自分のわかる言語でないとインストールしてもらえる確率がグッと減ってしまします。
今回のバージョンでは、日英合わせて6ヶ国語に対応していますのでそれぞれの言葉でスクリーンショットやアプリの紹介文を作成しました。
当たり前のことなのですが、言語数に比例して用意するスクリーンショットの枚数が増えていきます。
必要なスクリーンショットの枚数 = 紹介したいスクリーンショット数 × 4種類のサイズ × 言語数
なので注意してください。

結果

これらの改善策に加えて、細かな不具合修正などを行い、気合の入ったVersion2.2.0を2020年2月13日にリリースしました。
リリースに際して、純粋に改善活動の効果だけを確認したかったので、以下のようにリリース告知を行いました。

リリースから1週間:特に何もしないで、どれだけ変化するか確認する
1週間後:各種SNSで発信を行い、DL数がどれだけ変化するか確認する

以下、リリース直前の30日間と、リリース後の30日間を比較した結果です。
(2020/3/15時点でのオプトイン率2は44%)

DL数などの変化

リリース前1週間とリリース後1週間の比較
・プロダクトページ閲覧数:334 → 333(微減)
・DL数(Appユニット数):103 → 132(28%UP)

Before
(2/6 - 2/12)
Before.png
After
(2/13 - 2/19)
After.png

リリース前30日間とリリース後30日間の比較
・プロダクトページ閲覧数:1281 → 2162(69%UP)
・DL数(Appユニット数):434 → 817(88%UP)

Before
(1/14 - 2/12)
before.png
After
(2/13 - 3/13)
after.png

ええ、なんともしょっぱい結果でした。
特にSNSで発信を行わずに、しれっとアップデートだけした最初の1週間は、インプレッション数・コンバージョン率が落ちるという有様でした。。。

当初の予定では、「5000ダウンロード 前期比999%UP」みたいな爆速の改善をキメて、Qiitaに記事を投稿する予定だったのですが、現状ではこんな感じです。
ただSNSで告知後、右肩上がりに転じて、徐々に1日平均のDL数も伸びてきているので、いつか目標の1日平均100DLいくことを期待します。

多言語対応の効果(国/地域別の変化)

国別の結果は以下の通りです。

Before
(1/14 - 2/12)
After
(2/13 - 3/13)
beforeCountry.png afterCountry.png

地域別の結果は以下の通りです。
region.png

こちらもまた微妙です。相変わらず日本一強ですが、今までほとんどDL数のなかったフランスやドイツからも徐々にDLされるようになりました。多言語対応の結果かと思われます。
特にヨーロッパ地域の前期比317%UPは、アツいです。

レビュー数の変化

SKStoreReviewControllerの導入結果としましては、Version1.0をリリース後16ヶ月で通算13件だった評価が、この記事を執筆しているタイミングで21件まで増加しました。
リリースから1ヶ月で8件増えているので、こちらも効果があったと言えそうです。
おかげさまで、評価も4.4とそこそこ高評価をいただいております。
star.png

この評価が中長期的にみて、DL数増加に影響すると考えています。(高評価を持続できてればという条件付きですが、、、)

AppStoreの最適化の成果

これは完全にAppStoreのアルゴリズムなので理由は定かではないですが、今まで検索結果の下の方にいたこのアプリが、徐々に上の方に来はじめました。
「スピードメーター」で検索した時、私のアプリは50番目くらいに表示されていたのですが、最近だと15~20番目あたりまで来ています。
検索結果が上位に来たからDL数が伸びたのか、それともDL数が伸びたから検索結果が上位に来たのか。
完全にニワトリとタマゴ問題ですが、努力した甲斐が少しはあったかもしれません。
(事前に色々なキーワードで調べて、順位をメモっておけばよかったと後悔)

考察

さて、当初掲げていた、
・魅力度を向上させる
・知名度を向上させる
という仮説は、どうだったのか?

それぞれの仮説は以下の指標の変化と結び付くと思います。
・魅力度→コンバージョン率が向上したか?
・知名度→インプレッション数が向上したか?

結果を見てみますと、それぞれ以下のように変化しています。
・コンバージョン率:2.32%→3.49%(1.17ポイントUP)
・インプレッション数:24596→31981(30%UP)

この変化がどれだけのものなのか、比較材料を持っていないため判断できないのですが、とりあえずは掲げていた仮説を立証するための手段を行ったことで、向上させることができたのでよかったです。

記事の締めとしてはイマイチ弱いのですが、以下の考察を持って締めくくりたいと思います。

これらのことを行うとコンバージョン率とインプレッション数をあげることができ、その結果アプリのDL数を微速ながら伸ばすことができると言えそう。
・よっぽどバズらせて知名度を向上させでもしない限り、DL数は爆速で伸びたりしない

その後の経過観察

本記事の反応が良ければ、その後どうなったのか更新したいと思います。


  1. Support Split Screen Multitaskingについては、Appleの公式の見解が「強くお勧めします」という表現に変わってきています。(https://developer.apple.com/jp/news/?id=01132020b

  2. 診断データおよび利用情報をApp開発者と共有することに同意したユーザの割合 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む