iOSでインジケータのようなくるくる回るアニメーションを造る - CGAffineTransform

はじめに

インジケータを自作する方法です。
標準で搭載されているものでも十分事足りるのですが、アプリの色を出したいときにインジケータの演出もこだわりたいですよね。

提供中のTIME HACKERにも自作のインジケータを導入した方ので、以下のキャプチャのようなものを自作しました。

f:id:project-unknown:20181103200007g:plain

今日は、その際のやりかたです。
パラパラ漫画のような作り方でも良いのですが、上記キャプチャを見てもらえばわかるように、一部だけエンドレスに回転させる程度なら、CGAffineTransformを利用します。

CGAffineTransform

CGAffineTransformは、UIViewの拡大・縮小・回転などを実装するのをサポートしています。

これを用いて、回転するUIViewを作ります。

class IndicatorView: UIView {

    @IBOutlet weak var indicatorPin: UIImageView!

    func start() {
        
            UIView.animate(withDuration: 1.0, delay: 0.0, options: .curveLinear, animations: {
                self.indicatorPin.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
                self.indicatorPin.transform = CGAffineTransform.identity
            }, completion: { completed in
                self.start(title: title)
            })
        }
}

indicatorPinに回転したい画像をセットし、startメソッド内の、UIView.animateメソッドを使って回転させます。

animationsの中身は、以下のように指定することで、1周分の回転を指定します。

self.indicatorPin.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
self.indicatorPin.transform = CGAffineTransform.identity

最後のcompletionで自分自身を再帰呼び出し、1周回ったら次の周へ・・・とエンドレスに呼び出しています。

また、途中で止めたい場合は、stop用のメソッドなどを作ると良いかもです。

完成形

上記含めて、自作Indicatorの簡単な完成形です。
outletのところは、xibと接続してください。

class IndicatorView: UIView {

    
    /// Indicatorの動作フラグです
    ///  - true: Indicatorの動作を停止します
    ///  - false: Indicatorの動作を開始します
    var finished = true
    
    @IBOutlet weak var indicatorPin: UIImageView!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        loadNib()
    }
    
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        loadNib()
    }

    
    /// Indicatorを開始します
    ///
    func start() {
        finished = false
        running()
    }
    
    
    /// Indicator animationを実行します
    func running() {
        if finished == true {
            return
        }
        
        UIView.animate(withDuration: 1.0, delay: 0.0, options: .curveLinear, animations: {
            self.indicatorPin.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
            self.indicatorPin.transform = CGAffineTransform.identity
        }, completion: { completed in
            self.running()
        })
    }
    
    func stop() {
        finished = true
    }

    private func loadNib(){
        let view = Bundle.main.loadNibNamed(String(describing: type(of: self)), owner: self, options: nil)?.first as! UIView
        view.frame = self.bounds
        self.addSubview(view)
    }
}

最後に、これを使いたいViewControllerのStoryboard等に設置して、以下のように呼び出すだけです。

{IndicatorViewのインスタンス}.start()
{IndicatorViewのインスタンス}.stop()

Kotlin:時計アプリを作成

はじめに

Kotlinで時計アプリを作ってみます。

ゴールは以下の機能を有するアプリを作ることに定めます。

  • シミュレータ上で日付・時間を表示する。
  • 時間はリアルタイムで更新される。

んで、下記の内容を把握できるかなぁ〜と思っています。

  • 関数の作り方などの基本的な内容をマスターする。
  • 日付・時間の操作方法をマスターする。
  • 画面のリアルタイム更新の方法をマスターする。

では、早速挑戦です!

プロジェクト作成の主な設定は以下のような感じです。
基本的にデフォルトの設定になります。

設定名 設定内容
アプリ名 clock
include Kotlin support チェックを入れる
Activity name MainActivity
Layout name activity_main

画面を設定する。

画面ですが、次のファイルを開くと編集可能です。

f:id:project-unknown:20181008184035p:plain:w350

app>res>layout>activity_main.xmlを開きます。

デフォルトでtextViewが一つありますので、これのIDだけ変更しておきます。 IDは「dateView」としておきます。

f:id:project-unknown:20181008211650p:plain

時間を取得する方法

時間の取得は、 「Calendar」を利用します。

val calendar = Calendar.getInstance()
// 時・分・秒を設定する。
val hour = calendar.get(Calendar.HOUR)
val minute = calendar.get(Calendar.MINUTE)
val second = calendar.get(Calendar.SECOND)

// dateViewのテキストを変更
val test = findViewById<TextView>(R.id.dateView)
test.text = "${hour}時${minute}分${second}秒"

結果は・・・
7時10分11秒と表示されました。
できれば、19時と表示したいので、下記のように修正しました。

val calendar = Calendar.getInstance()
// 時・分・秒を設定する。
val hour = calendar.get(Calendar.HOUR_OF_DAY)
val minute = calendar.get(Calendar.MINUTE)
val second = calendar.get(Calendar.SECOND)

// dateViewのテキストを変更
val test = findViewById<TextView>(R.id.dateView)
test.text = "${hour}時${minute}分${second}秒"

HOUR:午前、午後の何時(0〜11)を返す。
HOUR_OF_DAY:1日の何時(0〜23)を返す。

ここまでは簡単ですかね〜

リアルタイム更新

リアルタイム更新が曲者です。

timer関数を使えばいいと思うのですが、

f:id:project-unknown:20180923172732p:plain

引数が多くて、わかりにくい!
何を設定すればいいの!

調査した結果、引数の説明はこんな感じです。

引数 説明
name タイマーを実行しているスレッドに使用する名前。
daemon trueの場合、スレッドはデーモンスレッドとして起動されます
(デーモンスレッドのみが実行されているときにVMは終了します)
initialDelay タイマー作成後、指定したミリ秒後に処理を開始する。
period 指定したミリ秒後間隔で処理する。
action 定期的に実行する処理

この中で必ず指定しないといけないのは、3つ

  • name
  • period
  • action

指定した結果は、こんな感じです。

// nameとperiodを指定、{}内が全部actionの指定
timer(name = "testTimer",period = 1000) {

      val calendar = Calendar.getInstance()
      // 時・分・秒を設定する。
      val hour = calendar.get(Calendar.HOUR_OF_DAY)
      val minute = calendar.get(Calendar.MINUTE)
      val second = calendar.get(Calendar.SECOND)

      // dateViewのテキストを変更
      val test = findViewById<TextView>(R.id.dateView)
      test.text = "${hour}時${minute}分${second}秒"

}

これを実行した結果がこれ!

f:id:project-unknown:20180923195000p:plain

ええ!!!!

「アプリ名 keeps stopping」
ビルドは成功したのに、Stopしてしまいました。
原因はこいつでした。

val test = findViewById<TextView>(R.id.dateView)
test.text = "${hour}時${minute}分${second}秒"

Androidには、
「画面に関する処理はメインスレッドから行わなければならない」
という仕様があるためのようです。

timerのname引数の説明を読むと、
「タイマーを実行しているスレッドに使用する名前」
となっているため、timerメソッドを利用した段階で別スレッドを用意してしまい、その中で画面に関する処理を記載したため、停止してしまったようです。

ハンドラを用意する。

ハンドラはスレッド間の通信を行うことができます。

timerのスレッドで処理した内容をハンドラでメインスレッドに送信してあげます。

つまり、こんな感じで書き換えました。

// ハンドラのインスタンス作成
val hander = Handler()
        
        timer(name = "testTimer",period = 1000) {

            val calendar = Calendar.getInstance()
            // 時・分・秒を設定する。
            val hour = calendar.get(Calendar.HOUR_OF_DAY)
            val minute = calendar.get(Calendar.MINUTE)
            val second = calendar.get(Calendar.SECOND)

            // ハンドラで処理したい内容をメインメソッドに送信
            hander.post {
                val test = findViewById<TextView>(R.id.dateView)
                test.text = "${hour}時${minute}分${second}秒"

            }
        }

ハンドラを用意して、timerスレッド内でメインスレッドで処理してほしい内容をメインメソッドに送信します。

これにより、画面に関する処理がメインメソッド上で処理され、停止することなく処理されるようになります。

実行結果は以下の通りです。

f:id:project-unknown:20180923203644p:plain:w350

まとめ

リアルタイム処理を学ぶつもりが、ハンドラを勉強することになってしまいました。

ハンドラって「イベントハンドラ」とかいろんなところで聞いていたんですが、よくわかってなかったんだってわかりました。

Kotlin:Macでkotlin開発環境を構築(AndroidStudio)

Kotlinで開発するため、AndroidStudioをMacにインストールしてみます!

開発環境って綺麗に構築できないことが多いですよね。
そのため、今回はエラーや失敗など、全部記録!
綺麗にいかない開発環境構築を記載します!

事前に入手した情報

KotlinでAndroidのアプリが開発できるようになった。
Android Studioで開発が可能、以上!
つまり、事前知識ほぼ皆無です。(Androidアプリの開発経験もなし)

Android Studioをダウンロード

さっそくAndroidStadioをGoogleで検索! 次のサイトがヒットしました。

Android Studio のインストール  |  Android Developers

ダウンロードサイトではなかったけど、公式のインストールガイドのようです。

しかし、その内容にビックリ!

Mac で Android Studio を利用する際に JDK 1.8 を使用していると、
安定性が低下することが知られています。
これらの問題が解決されるまでは、古いバージョン(JDK 1.6 以前)の
 JDK をダウンロードすることで安定性を改善することができます。

なんだと!?JDKってJavaの開発キットだから、つまり古いJava使えってことになる。
できれば、バージョンダウンとかはしたく無い!

とりあえず、自分のMacのJDKを確認です!

ターミナルを開いて、

Java -version

と入力、「java version "1.8.0_151"」とバージョンが判明・・・
できれば、このバージョンを使いたい!

いろんなサイトを見ても、JDKの話は記載がありませんでした。
安定してないかもしれないけど、このバージョンのままで行きましょう!
では、早速以下のサイトからダウンロード開始です!

developer.android.com

「DOWNLOAD ANDROID SYUDIO」ボタンからAndroidStudioをダウンロードします。

f:id:project-unknown:20180721174001p:plain

「android-studio-ide-173.4819257-mac.dmg」ってファイルがダウンロードできました。

インストール


「android-studio-ide-173.4819257-mac.dmg」を実行すると、こんな感じになりました。

f:id:project-unknown:20180721175417p:plain:w350

早速、Androidstudioをアプリケーションに入れます。
少し待つと、AndroidStudioがアプリケーションファイルに入りました。

f:id:project-unknown:20180721175925p:plain:w200

では早速、Android Studioを実行してみます!


まず、以前のバージョンの設定を引き継ぐか聞かれました。

f:id:project-unknown:20180721180321p:plain:w350

これは素直に「Do not import setting」を選択です。


次に出てきたのは、これです。

f:id:project-unknown:20180825183713p:plain:w350

「Server's certificate is not trusted」って出てきました・・・。なんでしょうこれ・・・

翻訳すると「サーバーの証明書が信頼されていない」ですって!
ですが、信頼してもいいじゃないですか!ってことで「Accept(同意)」をクリック!


なんとか先に進みました。

f:id:project-unknown:20180825194352p:plain:w350

とりあえず「Next」

f:id:project-unknown:20180825195434p:plain:w350

install Typeを選択ですが、初心者は迷わず「Standerd」を選択です。

f:id:project-unknown:20180825200559p:plain:w350

次にUIの選択です。 これは単純に好きな方を選びましょう(後で選択可能か確認!) ちなみに、私は「Darcula」を選択しました。

Errorでグダグダに・・・


次に「verify setting」と表示されました。

これは設定確認みたいです・・・ って、ここでまさかのエラーが・・・

f:id:project-unknown:20180825202512p:plain:w350

「an error occurred while trying to computer required packages」ってなんだよ!

いろいろ調べてみたら、「Finish」を押したら次の処理の画面に遷移して、問題なかった的な英語の記事を発見したので、私もとりあえず進んでみます。

f:id:project-unknown:20180825203238p:plain:w350

・・・だめじゃん!
「Failed to determine required packages」
「必要なパッケージの決定に失敗しました」ですって!
なんじゃそりゃ!しかもPreviousボタンが非活性・・・ 戻れないじゃんか!
・・・進んでみますか!

「Welcome to Android Studio」だって!
やったね!!!って、よくみてみると

f:id:project-unknown:20180908150156p:plain:w350

なんか、びっくりマークが出てます・・・。
びっくりマークをクリックして詳細を表示してみました。

f:id:project-unknown:20180908150638p:plain:w350

「Failed to determine required packages」
さっきのエラーじゃないか!

前途多難過ぎますね・・・

エラー解決へ


結局、必要なコンポーネントやパッケージはわからないかったです。
ですのでWelcome画面の下の方にある、Configure>SDK Managerで必要そうなSDKをダウンロードしていこうと思います。

f:id:project-unknown:20180908154543p:plain:w350

どうやら、「Android SDK Local」に値が設定されて それならば、Android SDKを自分でインストールしてみます。

HomeBrewをインストールしていたので、下記のコマンドでインストール開始です!

brew cask install android-sdk

どうやら、成功したようです。
では、早速、「Android SDK Local」のEditをクリック設定だ〜〜〜

f:id:project-unknown:20180908155757p:plain:w350

もしかして、余計なことしなくても、Android SDKが普通にインストールできるっぽいぞ・・・ 余計なことやってしまったのか?

AndroidStudioを動かしたところ、また、インストール画面が表示されてました。
とりあえず、やり直してみるか〜と先ほどと同じようにボタンをぽちぽちやりました。

そして、問題の「verify setting」ですが・・・

f:id:project-unknown:20180908160207p:plain:w350

なんか、エラーが解消されている!
AndroidSDKをインストールしたことで解消されたのかもしれませんね。

これで心置きなく、インストール作業が実行できます!
「Finish」ボタンをクリックすると、今度はコンポーネントのダウンロードが始まりました!!

f:id:project-unknown:20180908162402p:plain:w350

なんとかなったっぽいです。
では、Finishさせて、やっとWelCome画面です!

f:id:project-unknown:20180909122638p:plain:w350

今度は!マークがないので、正しくインストールができたみたいです。

プロジェクトを作成

では早速、Androidのプロジェクトを作ってみます。

f:id:project-unknown:20180909122924p:plain:w350

適当なプロジェクト名を入力して、
あと、kotlin使うから、「Include Kotlin suppurt」にチェックを入れます。

f:id:project-unknown:20180909123116p:plain:w350

ここでは、Androidのバージョンをどこまでサポートするか選択できるようです。 ここは初心者なので、デフォルトでいきます。

f:id:project-unknown:20180909123239p:plain:w350

今度はアクティビティの選択、デザインのテンプレートを選ぶようですので、とりあえずデフォルト!

f:id:project-unknown:20180909124731p:plain:w350

メインアクティビティの名前を決定します。 まぁ、これもデフォルトにします。

f:id:project-unknown:20180909125324p:plain:w350

コンポーネントのインストールを行うようですが、 デフォルトだけあって、全てインストール済みのようです。

では、OKなのでFinish!

f:id:project-unknown:20180909125420p:plain:w350

buildが始まりました。

f:id:project-unknown:20180909125537p:plain:w350

おお!
ついにメイン画面が現れました!

まとめ

Android Studioのインストールをやってみましたが、
思ったより難しくはなかったような気がします。

正直、エラーが発生して、1からやり直しが発生すると思っていたのですが・・・。

Javaのバージョンが気になりますが、現在のところ、特に問題はありません。

これから、Kotlin開発頑張ります!

XCode10でビルドできなくなった時にやったこと - Multiple commands produce error

XCode10がリリースされ、早速手元にあるXCode9でビルドしたアプリもXCode10用にビルドしようとした際に以下のエラーが発生しました。

error: Multiple commands produce '/Users/{アカウント名}/Library/Developer/Xcode/DerivedData/{アプリ名}-hievebmhhbbmlybyrcuahkuwpdgk/Build/Products/Debug-iphonesimulator/{アプリ名}.app':
1) Target '{アプリ名}' has create directory command with output '/Users/{アカウント名}/Library/Developer/Xcode/DerivedData/{アプリ名}-hievebmhhbbmlybyrcuahkuwpdgk/Build/Products/Debug-iphonesimulator/{アプリ名}.app'
2) That command depends on command in Target '{アプリ名}': script phase “[CP] Copy Pods Resources”

原因は、XCodeのbuildシステムのデフォルトが変更された事によります。
buildシステムについては、以下から確認できます。

f:id:project-unknown:20180923190113p:plain

f:id:project-unknown:20180923190145p:plain

上記のように、XCode10からは、New Build Systemがデフォルトになっています。
(XCode9までは、Legacy Build Systemに該当するものがこれに当たります)

これに対する対処法は、2通りあります。

  1. Product Nameで${TARGET_NAME}を使うのをやめる
  2. Legacy Build Systemを採用する

結論を言うと、私は今回の対応で2を選択しました。
今回は、それぞれの対応方法についでご紹介します。

対応方法1 Product Nameで${TARGET_NAME}を使うのをやめる

おそらくこれが推奨されるやり方です。

Build Settings -> Product Name

ここに${TARGET_NAME}で設定されているものを、環境変数を使うのではなくて、手打ちでProduct Nameを入力します。

f:id:project-unknown:20180923190543p:plain

今回、私がやったのはTimeHackerアプリの設定を行ったので、「TimeHacker」と入力しました。

後は、WidgetやWatchでも同様の事を実施して、Buildするだけです。

…が、私の場合、おそらくCocoaPodsのパッケージ依存か何かでうまく動きませんでした。

対応方法2 Legacy Build Systemを採用する

冒頭で紹介したWorkspace Settings...の設定をLegacy Build Systemに変更します。
これでこれまでのBuild(XCode9までのBuild)が実行されます。

f:id:project-unknown:20180923190113p:plain

f:id:project-unknown:20180923190850p:plain

最後に

New Build SystemはSwift製と言うのと、今後、こちらをデファクトスタンダードになっていくと思われます。
Podsやパッケージが対応されたら、New Build Systemにうつしていこうと思います。

Apple Watch - Watch OS4 でCoreData等を用いた開発 - Swift4

Apple Watch (Watch OS4)

この記事では、Apple Watch (以降Watch OS)の開発にあたっての私なりの開発メモを記します。
また、何の断りがなければ、Watch OSはWatch OS4の事を指しています。

環境としては、

  • XCode 9.4.x
  • Watch OS 4 (Watch Kit2)

での開発です。
特にCoreData等のリソース周りの資料があまり見つからなかったので、そこを重点的に記載しています。

また、今回の記事はTIME HACKERでApple Watch対応を行っている時に、主に私が悩んだ所ですので、CoreDataとのデータのやり取りを主にフォーカスしています。

これまでのWatch OS (初代Watch Kit)との違い

Watch OS開発は、これまで(初代Watch Kit)とは、特にリソースの取扱方法が異なってます。

その違いは、以下のAppleが展開している図を見ると明らかです。

f:id:project-unknown:20180902001023p:plain

これまでは、Apple WatchはiPhone上で動作させること前提の仕組みとなっていた為、CoreDataやUserDefaultsをApp Groupsの設定さえ行っていれば、共有リソースとしてアクセスすることが出来ました。

しかし、Watch Kit 2からは、Apple Watch上で独立して動作する事になり、
Apple Watchから共有リソースへのアクセスができなくなりました。

新しいWatch OSでのCoreData等のリソースアクセスの考え方 Watch Connectivity

UserDefaults等で取り扱う、一時的な設定等は、Apple Watchでそのまま使えば良いでしょう。
しかし、iPhone上のアプリとデータを共有する場合は、Apple Watch上のアプリからiPhoneアプリへデータの送受信を行うと言う考え方で取り扱う事になります。
このときに、Watch Connectivityを用いて送受信を行います。

図で言う、枠で囲った所がそれに当たります。

f:id:project-unknown:20180902001752p:plain

しかし、これには注意点があります。

これまでは単純に同一デバイス内のリソースファイルへアクセスしていたので、それなりに高速に取り扱うことが出来ましたが。
Apple WatchとiPhoneとは物理的にも分断されている通り、Sessionでのデータのやり取りになります。
当然、画像ファイル等のやり取りを行うと、その分データの送受信に時間がかかるので注意が必要です

画像ファイルをWebから取得して利用するのではなく、予めアプリに組み込んだものを利用するのであれば、iPhoneとApple Watch両方に同一画像を埋め込んでおくのが妥当だと思います。

では、実際にiPhoneとWatch OSとでリソースのやり取りを行うサンプルを造っていきます。

データ送受信部分

まずは、データの送受信を行う所を造ります。
イメージは、繰り返しになりますが、以下の図の用に考えます。

f:id:project-unknown:20180902003023p:plain

iPhone, Watch OS共にデータの送受信を行う為のSessionをこさえます。

iPhone App側の実装

以下のようにWatchManagerクラスを用意します。

WatchManagerは以下の役割を担います。

  • Watch OSとのSession
  • Watch OSから来たEventの種別を判定 (後述)

また、WatchManagerはSessionを取り扱う為、念の為シングルトン設計にしています。

import WatchConnectivity

class WatchManager: NSObject, WCSessionDelegate {

    let wcSession = WCSession.default
    
    private static let instance = WatchManager()
    
    public class var shared: WatchManager {
        
        return instance
    }
    

    func initWCSession() {
        if WCSession.isSupported() {
            wcSession.delegate = self
            wcSession.activate()
            print("[INFO] session activate")
        } else {
            print("[INFO] session error")
        }
    }

}

extension WatchManager {
    
    /// メッセージを受信した時の処理
    ///
    /// - Parameters:
    ///   - session:
    ///   - message:
    ///   - replyHandler:
    func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
        
        var result: [String: Any] = ["success" : false]
        replyHandler(result)
        
    }
    
}

/// Session Delegate
extension WatchManager {
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        print("[INFO] The session has completed activation.")
    }
    
    func sessionDidBecomeInactive(_ session: WCSession) {
        print("[INFO] The session has got into inactivation.")
    }
    
    func sessionDidDeactivate(_ session: WCSession) {
        print("[INFO] The session has deactivated")
    }
    
    func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
        print(userInfo)
    }
    
    func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
        print("[INFO] try to send message to watch")
        sendMessage()
    }
}

やっていることは、WCSessionオブジェクトを生成し、delegateを自分自身にしているだけです。
これを例えば、AppDelegate等で

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        WatchManager.shared.initWCSession()
    }
}

と、呼んであげるとWatch OS用のSessionを貼ることが出来ます。
後は、Watch OSからEventが飛んでくると、適宜Watch Manager内のDelegateが呼び出されます。

上記サンプルで

    func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
    }

このコードだけ、中身に軽い処理を入れているのは、今回のサンプルでは、主にこのDelegateを用いる為です。念の為。

Watch OS側の実装

iPhone App側の実装は出来たので、Watch OS側の実装を行います。
今回のサンプルでは、Watch OSが起動した際に、iPhone AppへSessionを開始するコードとなっています(この際はloadDataメソッドをコールします)。

import WatchKit
import Foundation
import WatchConnectivity

class InterfaceController: WKInterfaceController, WCSessionDelegate {
    
    let wcSession = WCSession.default
    
    override func awake(withContext context: Any?) {
        super.awake(withContext: context)
        
        // WCSessionの開始
        if WCSession.isSupported() {
            wcSession.delegate = self
            wcSession.activate()
        }
        
    }
    
    override func didAppear() {
        super.didAppear()
        
        print("[Info] didAppear")
        if wcSession.isReachable {
            print("[Info] reachable")
            loadData()
        } else {
            print("[Info] not reachable")
        }
    }
    
    override func willActivate() {
        super.willActivate()

    }
    
    override func didDeactivate() {
        super.didDeactivate()
    }

    func loadData() {
        let message = [ "message" :  "test"]
        wcSession.sendMessage(message, replyHandler: { replyDict in
            print("[INFO] responce action data")
            print(replyDict)

        }, errorHandler: { error in
            print(error)
        })

    }
}

// MARK: - Session Delegate
extension InterfaceController {
    
    func sessionReachabilityDidChange(_ session: WCSession) {
        print("[Info] sessionReachabilityDidChange")
        if wcSession.isReachable {
            print("[Info] reachable")
            loadData()
        } else {
            print("[Info] not reachable")
        }
    }
    
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        print("[Info] The session has completed activation.")
    }
    
    func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
        
        // iPhoneからのデータを受け取る
        print("[Info] didReceiveMessage")
    }
}

上記コードの、

        wcSession.sendMessage(message, replyHandler: { replyDict in
            print("[INFO] responce action data")
            print(replyDict)

        }, errorHandler: { error in
            print(error)
        })

この部分で、iPhone App側に通信を送っています。
ここで、例えばWatchManagerのメソッドを以下のように変更します。

    func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
        print(message)
        var result: [String: Any] = ["success" : false]
        replyHandler(result)
        
    }

これで実行すると、 以下の情報が出力されます。

[ "message" :  "test"]

ここまでで、iPhone AppとWatch OS Appとの接続の部分が出来ました

Watch OSとiPhoneとでCoreData等のデータのやり取りを行います。

ここに来て、やっと表題のお話が出来ます…。

これまでは、Watch OS AppとiPhone Appとの通信部分を造ってきたので、
この流れの上で、リソースデータのやり取りを行います。

といっても、設計はごく単純で、
すべてのデータのやり取りはiPhone側で行い、Watch OS側は適宜データの送受信のEventを送るだけです。

最初の方に載せた図で言うと、以下のようにApple Watchから来たEventのレシーバからCoreDataへアクセスし、データが必要であれば、返却する仕組みです。

f:id:project-unknown:20180902005106p:plain

前提として

今回のデータの送受信を行うにあたり、以下のデータ構造をDictionary形式でやり取りします。

Apple Watchからデータを取得するリクエストを投げる時

[
   "kind" : 1,
]

kind : 1はデータ取得を行いたい識別子です。

Apple Watchからデータを更新した際にリクエストを投げる時

[
    "kind" : 2,
    "data" : Any
]

この場合は、kind : 2として、一緒に更新したいデータを送信します。

では、コードの方を見てみます。

iPhone App側の実装

これまで書いてきた、WatchManagerのメソッドを以下のように変更します。

    func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
        
        var result: [String: Any] = ["success" : false]
        
        guard let kind = message["kind"] as? Int else {
            result["success"] = false
            
            replyHandler(result)
            return
        }
        
        switch kind {
        case 1:
            result["data"] = /* CoreDataからとってきたデータ */
            result["success"] = true

            return
        case 2:
            /* CoreDataのデータ更新 */
            result["success"] = true
            
            return

        default:
            replyHandler(result)
            return
        }
    }

kindで、処理の識別を行い、適宜CoreDataの操作を行います。
最後に結果をreplyHandlerに渡して上げるだけです。

Watch OS側の実装

iPhone Appでデータを受信して、適切なレスポンスを返せるようになったので、
今度はWatch OS側からEventを送信する方法です。

まずは、データを受け取る所ですが、
loadDataメソッドを以下のように改変します。

    func loadData() {
        let message = [ "kind" : 1]
        wcSession.sendMessage(message, replyHandler: { replyDict in
            print("[INFO] responce action data")
            print(replyDict)
            /* replyDictにCoreDataから取得した情報が入っているので、以後適切な処理を行う */
        }, errorHandler: { error in
            print(error)
        })

    }

データ受信の際は、kind : 1でSession通信を行えば、iPhone側でCoreDataにアクセスし、データを返却してくれます。

次に、データ更新は、上記loadDataと殆ど同じですが、念の為載せておきます。
データ更新ように、saveDataメソッドを追加しています。

    func saveData() {
        var message: [String: Any] = [:]
        message["kind"] = 2
        message["data"] = data
        
        wcSession.sendMessage(message, replyHandler: { replyDict in
            print(replyDict)
        }, errorHandler: { error in
            print(error)
        })
    }

これで、適切なタイミングでsaveDataを呼び出せば、保存したいデータをCoreDataに保存することが出来ます。

詰まった所

終わりに、私が盛大に詰まった所として…。
当たり前のように、iPhone AppのWatch ManagerでreplyHandlerを使っていますが、
Apple WatchでSession処理を行う際に、適切なDelegateをiPhone App側で実装していないと通信エラーとなります。

この詳細は、以下にまとめておきましたので、もし詰まってしまった場合の参考にしてください。

www.project-unknown.jp

最後に

Watch OSは他のWidget等と違って、これまでのWidget等と違って、考え方を大きくシフトしないとなかなか造りにくいです。
しかし、WebAPIを用いた実装を行っているんだと、考えると一気に考え方が楽になります。(Appleはここまで考えているんでしょうね・・・さすがや)

最後に宣伝となりますが、
この記事は、現在TIME HACKERでApple Watch対応を行っており、その際の備忘録として記したものです。

もう少しでリリースが見えている所ですので、是非とも皆さんお使いになってくださいね!

img

Watch OS 2 - WCErrorCodeDeliveryFailed が出てiPhoneとWatchでデータのやり取りが出来ない時の対応

はじめに

Watch OS 2からは、Apple WatchがiPhoneアプリから独立して動くことになったので、これまでとは違った書き方が必要です。

この対応を行っている最中に、以下の様なエラーが出て詰まってしまったので、その備忘録を示します。

エラーの内容 WCErrorCodeDeliveryFailed

Watch Extension[3994:434267] [WC] -[WCSession _onqueue_notifyOfMessageError:messageID:withErrorHandler:] CFEDC3E4-FE0F-439D-B884-1CB2EC349DED errorHandler: YES with WCErrorCodeDeliveryFailed
Error Domain=WCErrorDomain Code=7014 "Payload could not be delivered." UserInfo={NSLocalizedDescription=Payload could not be delivered.}

実際に書いていたコードの中身

iPhone側、AppleDelegate

let wcSession = WCSession.default

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    initWCSession()
}
func initWCSession() {
    if WCSession.isSupported() {
        wcSession.delegate = self
        wcSession.activate()
        print("session activate")
    } else {
        print("session error")
    }
}

Watch Extension側

let wcSession = WCSession.default

override func awake(withContext context: Any?) {
    super.awake(withContext: context)
        
    // WCSessionの開始
    if WCSession.isSupported() {
        wcSession.delegate = self
        wcSession.activate()
            
        sendMessageToApp()
    }
        
}

func sendMessageToApp(){

    guard wcSession.isReachable else {
        return
    }

    print("sendMessageToParent()")
        
    let message = [ "toParent" : "OK" ]
        
    wcSession.sendMessage(message, replyHandler: { replyDict in
        print(replyDict)
    }, errorHandler: { error in
        print(error)
    })
}

今回上述しているエラーは、このprintで表示したエラーの中身です。

対応方法

今回の原因は、メッセージを受信する(iPhoneのAppDelegate側)方のレシーバーの処理が足りていませんでした。

AppDelegateに以下のメソッドを追加する事で、レシーバーがメッセージを受信できるようになるので、正常にデータを送信することができるようになります。

func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
    replyHandler(["reply" : "OK"])
}

TIME HACKER Version 1.1.0 ウィジェット対応と新規アイコンを追加しました

いつもTIME HACKERをご利用いただきありがとうございます。

今回のバージョン1.1.0で以下の対応を行いました。

  • ウィジェットでも記録できるようになりました。
  • ご要望のあったアイコンを追加しました。

ウィジェットでも記録できるようになりました。

ウィジェットにTIME HACKERを表示していただく事で、アプリを起動しなくても行動の記録が取れるようになりました。

これにより、更に日々の行動が記録しやすくなりましたので、是非ともご利用ください!

f:id:project-unknown:20180821000617p:plain

ウィジェットに表示されるアクションは、アプリに登録してあるアクションの上から6番目までが表示されます。

また、記録を計測している場合は、以下のような画面となります。

f:id:project-unknown:20180821000820p:plain

このように2件までの記録中のアクションが表示されます。

また、ストップボタンを押すことで、計測を終了することも出来ます。

ご要望のあったアイコンを追加しました

f:id:project-unknown:20180821001325p:plain

今回は育児系のアイコンのご要望を戴いたので、6種類のアイコンをご用意致しました!
是非ご利用ください!


まだTIME HACKERをお使いでなければ、是非ともこの機会にTIME HACKERをご利用ください!

img


ヘルプページ
ご意見・ご要望
TIME HACKER関連記事

Firebaseのエラーへの対処 - Terminating app due to uncaught exception 'com.firebase.core', reason: 'Default app has already been configured.

はじめに

iOSのFirebaseでは、以下を二重に呼ぶ事は禁止されています(クラッシュします)

FirebaseApp.configure()

この時のクラッシュの内容は以下のようになります。

Terminating app due to uncaught exception 'com.firebase.core', reason: 'Default app has already been configured.

しかし、TodayExtension等、回避しづらいケースがあり、その解決法です。

FirebaseApp.app()を使う

要はFirebaseApp.configure()は、FirebaseAppのオブジェクトを生成し、設定を行う事ですので、
インスタンスが生成されていないかをチェックすれば回避出来ます。

import Firebase

class TodayViewController: UIViewController, NCWidgetProviding {

    override func viewDidLoad() {

        // 省略

        if FirebaseApp.app() == nil {
            FirebaseApp.configure()
        }

        // 省略
    }


}

TIME HACKER 伝わる不具合の報告方法とは

はじめに

どうも、どうまずです。
今回の新アプリのTIME HACKERでは、テストを中心にプロジェクトへの参加をさせてもらいました。

テストで一番大変だったのは・・・不具合の報告です。

  • メールで不具合を報告したのに、伝わらない。
  • 不具合一覧の内容が何を書いているのかわからない。

そんな場合に参考にして下さい。

不具合報告の大変さ

f:id:project-unknown:20180729194226p:plain

なんで、不具合報告って大変なんでしょうか・・・
今までの不具合報告の内容から、何個かパターンを出して見ました。

画面名、機能名を適当に書いている

例えば、TIME HACKERを例にしますと、


レポート画面でレポートが表示されない。


こんな感じの不具合報告です。
この報告で何が一番困るかというと、
「レポート画面ってどこのこといっているの?」
って話になります。

TIME HACKERをお使いの方はわかるかと思いますが、TIME HACKERには

  • レポート-行動履歴
  • レポート-アクション毎
  • レポート-時間管理

と3つのレポート画面があり、どの画面をさしているのかよくわからないのです。

簡単な話かもしれませんが、意外と出来ていない場合が多い失敗報告例です。

不確定要素が含まれる不具合報告

場合によっては、不具合解消までの時間が大幅に伸びるパターンです。
これは、過去私が会社で報告された内容です。


他に何も作業していないのに、A機能の処理が遅延した。


という報告ですが、ログを確認すると、裏でDBバックアップをやっており、遅くなっていたという問題でした。
このように確認していないけど、さも確認したかのような報告がされてしまうと、不具合の解析が大幅に遅れ、最悪迷宮入りです。

不具合を報告するときは、確実な情報を伝えましょう。

  • 理論的に動かない
  • 仕様的に動かない

という場合、ログなどで「動いていないこと」を確認しましょう。
だって、理論的に仕様的に完璧だと思って動かして、発生した不具合なのですから。

また、SEならば、

  • ユーザからの報告

これも不確定要素です。
ユーザはシステムの細部までわかっていない場合が多いので、誤った内容になることが多いです。
そのため、ユーザからの報告はヒアリングしたり、実際に動かして確認を取らないといけません。

文章の不具合報告

これは実際にTIME HACKERで私どうまずが報告した内容を例に見て見ましょう。


レポート時間管理の下部の日付(6/15)をタップして、
リスト形式で「確保したいアクション」を表示している画面に移動して、
レポート時間管理画面に戻ると、日付が6/16になっている。


・・・自分でも分かり難い。
不具合を発見した当初はこれでも分かりやすい報告をしたつもりなのですが・・・

この報告は複数の動作をした結果、日付が正しくなかったと言っています。
しかし、複数の動作を1つの文にしてしまったため、どんな動作をしたのか分かりにくいです。
また、1つの文の中に不具合の内容も書いているので、結局何が不具合なのか分かりません。

もちろん、この不具合報告は「何いっているかわからない」という結果になってしまいました。

そのため、下記のように書き換えました。


1.レポート時間管理画面で日付をタップして6/15に変更
2.レポート時間管理画面のグラフの下の日付6/15をタップ
3.時間がリスト形式で表示される画面に遷移
4.レポート時間管理画面に戻る
5.レポート時間管理画面の日付が今日日付(6/16)になってる。

5の日付は6/15が正しいと思います。


箇条書きにすることで、以下のメリットがあります。

  • 動作を1つずつ記載できる。
  • 不具合の再現がし易い。
  • 機械的に事実を淡々と記載することができる。

文章にすると、文章に含まれる意味や内容を読み解く必要があります。
箇条書きにすることで、事実のみを淡々と報告していることが伝わり、余計な労力は不要となります。

テキストハレーションを生まないようにする。

テキストのやりとりは相手の顔が見えません。
そのため、余計なハレーション(悪影響)を及ぼすことがあります。

これも実際の例です。

レポート時間管理のグラフが正しく表示されない不具合があり、その報告をslackで正しく表示されない画像を送付して、


こんな風になっちゃったよ


と、何気なく送ってしまったのです。
その後、


これは、煽ってるの?


と返信があり、全力で謝罪しました。

不具合報告は、嫌な言い方をすると、
相手の失敗を見つけて、指摘する、かなりネガティブな報告なのです。
かなり注意して報告しましょう。

あと、新人の時は、バグという言葉を使いたがる傾向にあるようですが、

「バグってる」

という報告は、悪く言うと「お前、ミスってんぞ」と同意義です。
軽々しく使うとテキストハレーション発生ですから要注意です。
(新人のときはバグと思っても自分が知らない仕様があると思って、「仕様か確認して下さい」と言った方がいいです。)

再テストの結果は早めに

不具合報告が終え、ソースを修正してもらったら、修正が正しく行われているか再テストをして、やっと不具合修正が完了です。

「ソース修正しました」

という報告を受けた側は、「修正終わったんだ〜」くらいに捉えてしまうことが多いです。

しかし、修正した側は

  • 他に想定外や考慮不足の点がないか
  • 修正した結果、他の不具合を発生しないか
  • そもそも、修正した箇所とは別のところで不具合が発生してるんじゃ

と、気が気ではありません。

速やかに再テストを実施し、結果を報告しましょう。

結論

f:id:project-unknown:20180729194352p:plain

不具合報告をするときは、次のことを気をつけましょう

  • 画面名・機能名は正確に記載する。
  • 不確定要素は記載しない。(確認してから記載する。)
  • 箇条書きで機械的に記載する。
  • 不具合報告はテキストハレーションを起こしやすいから要注意する。
  • 再テストの結果は速やかに報告する。

これだけで、かなりわかりやすい内容になると思います。
ぜひ参考にしてみて下さい。

最後に・・・

記載したようなハレーションや問題が数々ありましたが、
「いいものを作りたい」という目的を共有し、メンバー一丸となり完成させたTIME HACKERをぜひ使ってみてください!

www.project-unknown.jp

う〜のん小話

f:id:project-unknown:20180727021019p:plain

※このいちご大福は生きているので
間もなくいちごが取り込まれ、ただの大福になります

デザイン担当のぽぽたです。

プロジェクトのマスコット担当のう〜のんですが
実は...う〜のんはいちごオレでできていま、した。初期設定では。
当時の私の大好物だったもので...。

今現在その設定は(いちごオレの飲み過ぎにより満足してしまったため)なくなりましたが
絶対甘いんだろうな...と、う〜のんに対する食欲を抑えられません。

う〜のん語録

うなな:バナナ
うまいらる : おいしいもの、おやつ、食べること


いちご大福 6個入り


その他のデザイン・イラスト関連の記事

その他のう〜のんに関する記事は、以下のページから見れますよ!

www.project-unknown.jp

う〜のんがこれまで登場した作品

ぱたぱたう〜のん

f:id:project-unknown:20180510010406p:plain:w300

www.project-unknown.jp

TIME HACKER メイキング

TIME HACKERリリースしました!

img

TIME HACKERをめでたくリリースすることができました。
まだの人は是非この機会にお試しください!

今回は、TIME HACKERを作るにあたり、その制作過程について記載していこうと思います。
今回の記事では、

  • どんなUX思想で作ったのか?
  • 設計思想はどうしたのか?
  • アプリ実装面での設計は?
  • マネタイズについて

上記を、書ける範囲で載せましたので、 これからアプリを作ろうとしている人へは、制作過程の参考に、
同業者の方へは、苦難の共感が得られれば幸いです。

TIME HACKERを作ることとなった動機

時間が欲しい

世の中のサラリーマンの殆どがそうだと思いますが、とにかく時間が無い。
平日に、スキルアップのための勉強や、アプリ造りを行いたくても、家に着くのが日付をまたがる時間帯になる為、中々実施することができません。

休日にやろうと思うのですが、、いざ休日になったら平日の疲れを癒やすために、ガッツリ休んでしまう等、自分のやりたいことがやれない毎日でした。

もう社会人生活も10年を超えてきて、一向に改善しない毎日に諦めの気持ちすら抱いていた際に、一冊の本と出会いました。

ライフハック大全

ふと手にとった、以下の「ライフハック大全―人生と仕事を変える小さな習慣250」という本が私の中で革命的でした。

何個もあるライフハックは、殆どが私の血肉になる位に、良い知識を仕入れる事ができたのですが、その中でも、最も私の中でためになったのは、

自分の1日の行動ログを取って、無駄に過ごしているところは無いか?を見返すことで時間を捻出する術

これにものすごく感銘を受け、その時のツイートが以下。

実際にこのライフハックを実施してみました。
私は1日の中で、以下の時間をもっと取りたいと考えています。

  • 読書をして自己啓発と勉強
  • PCを使ってプログラムや新技術の勉強
  • 個人アプリ、ゲームの開発

この観点を元に、1日の行動ログを記録し、時間を生み出すことが出来ないか分析してみます。
(メモ帳に走り書きしていた為、時間が入っていたり入っていなかったりしてます。)

  1. 8:00 起床
  2. 出る準備 20分 8:20
  3. 家を出てからバスにならぶ 10分 8:31
  4. バス乗車時間 13分 8:45
  5. 電車待ち時間 16分 9:02
  6. 特急 30分 乗り換え後、○○から△△ 20分 計50分 9:50
  7. 以降会社
  8. エレベーター待ちと朝の準備 16分 10:06
  9. タバコ 5分
  10. インシデント対応 41分 10:53
  11. 今日のタスクプランニング 9分 11:00
  12. ○○ミーティング 24分 11:27
  13. トイレとタバコ 15分 11:45
  14. 今年度のプロジェクト目標について議論 15分 12:00
  15. 昼飯と移動 30分 12:30
  16. タバコ 15分 12:45
  17. 目標についての議論 15分 13:00
  18. 溜まっていたSlackやり取り 21分
  19. タバコ 15分 13:40
  20. 雑務 15分
  21. 新規企画のKPI試算 vol1 45分 14:40
  22. タバコ 10分
  23. 新規企画のKPI試算 vol2 48分 15:45
  24. タバコ10分
  25. 部下面談 36分
  26. タバコ 10分
  27. ○○チーム向けプレゼン資料作成 10分
  28. △△MTG 40分
  29. ☓☓MTG 1時間30分
  30. タバコと帰る準備 10分 19:30
  31. ○○まで移動 20分
  32. △△まで 35分
  33. ジムで運動 1時間
  34. ジムの風呂 30分
  35. 買い物 10分
  36. バス停まで移動 5分
  37. バス待ちとバス乗車 18分
  38. タバコ 5分
  39. 家移動2分
  40. 家に着いて落ち着くまで 7分
  41. ダラダラ過ごす 2時間
  42. 風呂とダラダラ 30分
  43. 就寝 1:17

この日を振り返って見ると、例えば、以下を改善できそうです。

タバコ時間を読書時間に企てる

タバコの時間をタバコ + スキルアップのための読書(Kindle)を行うとしたら、80分読書時間が確保出来ます。
全てのタバコの時間を企てるのは難しいのと、読書の開始時には思い返しの時間も必要な事を鑑みても、50分は読書時間が確保できます。

移動時間を読書・勉強時間に企てる

また、バスの時間がトータル31分。電車の時間は特急が65分、在来線が40分。
バスと在来線は基本立つので、ここを読書に当てると71分。

特急は座ることが出来ますが、片道30分程度なので開発するには時間が足りなく見えるので、PCを使った勉強時間に企てると、65分勉強時間が確保できます。

ここまで考えると、家に帰るまでに、以下の時間を確保することがわかります。

  • 読書時間を121分
  • 勉強に65分

ただ、この勉強・読書時間は断片化した時間なので、効率は悪いですが、それでも家に変えるまでに、これだけの事が行えるので、

家では、アプリ開発に集中することができる。いやアプリ開発だけしていていいのです。

これまでだと、「勉強もしないと」「本も読まないと…」と考えがよぎって中々集中出来なかったのですが、
一日の無駄な時間、スキマ時間に十分に勉強を行えているので、家でやる必要が無いのです。

そして、家でダラダラとする時間を2時間から半分削るだけで、1時間確保出来ます。
更にストイックに削れば1時間半〜2時間確保できるでしょう。

ここまで見てわかるように、

1日の時間を細かく記録する事と、分析する事は、忙しいビジネスマンに取って、ものすごく有益である

ことがわかります。

そして、有益で有ることがわかった次は、もっと楽に記録したい・分析したいと思い、今回の「TIME HACKER」アプリに繋がります。

私自身の生活の質を向上させるために、どうしてもこの手のアプリが欲しかったのと、私の中でしっくり来るアプリがStoreに並んでいなかったので、何としてでも手に入れたかった。

気がつけば、全ての開発や勉強を停止して、着手していました。

(なんだかんだで7ヶ月も造ってた…)


仕様検討

コアバリューを決める

いつもは凄い悩む所だったのですが、ここは作り出した動機が動機なので、すぐに固まっていました。

アプリ制作初期は

無駄な時間を管理し、自分のスキルアップに企てたい時間を確保する

で、最後までここは変わらなかったのですが、
もっと範囲を広げて、以下を最終的なコアバリューとしました。

自分の時間を管理し、生活改善のサポートをするアプリ

ターゲット層を考える

ターゲット層は、女性でも・男性でもありませんし、
特段ペルソナも考えていません。

TIME HACKERでは、以下のユーザーをターゲットとして捉えています。

  • 気づいたら時間ばかり過ぎている人
  • 忙しくて自分の時間が確保出来ない人
  • 自分のスキルアップの時間を確保したい人
  • 日々の生活をもっと有意義に送りたい人

UXを検討する

大体のアプリに言えることですが、
このアプリはUXが特に肝となります。

  • 何より、毎日、しかも小刻みにアプリを立ち上げて行動のログを計測しないといけない。
  • それには、ユーザーのアプリ上の動作だけではなく、生活のリズムを一瞬でも妨げてはいけない。
  • 特に、行動ログを記録忘れた場合は、一気に記録するモチベーションが下がります。なので記録忘れを防がなければならない。
    • (結局、ここのサポート部分は1stリリースからは見送りました。)

上記をいろいろ考えてUXを造っている際に、取っていたメモが以下です。

f:id:project-unknown:20180528223801p:plain

f:id:project-unknown:20180528223811p:plain

結局、当時考えていた機能の半分くらいしか実現できていないのですが…。

UIを検討する

ここもUX並にメチャクチャ苦労しました。
なんだかんだで、2ヶ月近く、あーでもないこーでもないをやっていたんじゃないかしら。

ちなみに、最初期の頃に考えていたUI

f:id:project-unknown:20180713010102p:plain:w400

全く違いますね…。
今見ると、個人的に結構しんどいUIですが、当時はこれでいける!!と信じて止みませんでした。
結局、2日位、寝かせてから再度この画像を見て、くっそだせぇって思って、やり直しました
その時の温度感だけで決めるのではなく、一度寝かせるのは大事ですね。

次に考えたのが、以下のUIです。
文字情報を見るのではなくて、行動をアイコン化し、なれたら無意識で記録ができる事を狙っています。
また、無意識で押せる事を狙って、アイコンも結構大きめに設定してます。

f:id:project-unknown:20180713010403p:plain:w400

だいぶ、今の原型が見え隠れしていますね。
ここから、紆余曲折を経て、以下の様な画面で落ち着きました。

f:id:project-unknown:20180713010500p:plain:w400

マネタイズを検討する

TIME HACKERで、現在実装済みのマネタイズをご紹介します。
今後のマネタイズは敢えて載せません。
お金の匂いがプンプンする機能が追加されたら、「あっ、マネタイズに走ってるな」とでも思ってください。

  • アプリトップにバナー広告
  • 一定の回数計測を行った際のインタースティシャル

バナー広告

バナー広告は、KeyHolderでも実装しているのと、KeyHolderでも操作の邪魔にならないところに設置していますが、結構馬鹿にならないくらいの収益を上げてくれます。
UXの所でも述べていますが、TIME HACKERは、UXがとにかく命です。ユーザの行動を阻害する事をしようものなら、記録が面倒になり一気に使わなくなります
なので、バナー広告を設置するにあたり、アプリの操作を阻害する事はしないような画面設計にしています。

インタースティシャル広告

KeyHolderでは、インタースティシャル広告を出さずに本当に公開しました。
というか、KeyHolderはシンプルを極めすぎてインタースティシャル広告を入れる余地が無く、入れようものならアプリとしての価値を大きく欠損しかねるので、断念しました。

なので、TIME HACKERではインタースティシャルは仕様検討の総初期から、なんとかして入れられないかを深く検討しています。
(書けば書くほど、金儲けが全面に出てしまうので、引かれるリスクはありますが、それでもこの記事を記載しています)

バナー広告の項でも記載しましたが、TIME HACKERは何よりUXが命なので、インタースティシャル広告を入れる箇所には最新の注意を払っています。

TIME HACKERでインタースティシャスは、一定の時間、一定の回数、行動ログを取った際に、行動ログを記録したタイミングで広告表示を行っています。
これは、ユーザーとして考えた際に、行動ログを計測する直前に広告が表示されたら、ユーザーの行動を阻害するリスクが非常に高まる為、記録を開始したタイミングで広告を表示することにしました。

行動ログを記録した後に広告を表示しているので、マネタイズとしての期待値は大きく下がると思いますが、私が考えている一番のリスクは、

眼の前の収益に目がくらんで、ユーザの行動を阻害する事で、アプリをアンインストールされる事を一番のリスクとして捉えています

また、上記で一定の回数と記載しましたが、初回リリースまでにどの回数が妥当なのか?の結論を出すことが出来ませんでした。

ですので、FirebaseのA/Bテストを採用して、インタースティシャル広告を出すタイミングの検証を行っています。

www.project-unknown.jp

ここまでは、アプリ設計のお話でした。
次に、実装面でのお話をしていこうと思います。

実装開始

さぁ、ここからお祭りの始まりです。
いやぁ…とにかく紆余曲折しまくった…。

プロトタイプ開発

今回のアプリ開発では、特に真新しい事を中心に、やりたいことは何でもやろうと思い色々tryしました。

大きい所で言うと、

Carthageにも挑戦しましたし

www.project-unknown.jp

Quickにも挑戦しましたし

www.project-unknown.jp

RealmSwiftにも挑戦しました

www.project-unknown.jp

Firebaseにメインの解析機能を移管しましたし、
Firebaseを使ってA/Bテストにも挑戦しています。

www.project-unknown.jp

以下の「Code Complete」等の技術書も読み漁り、開発設計についての色々な挑戦も行いました。

そして気づいたのが、

tryばっかしていて、いつまで経っても完成しない

特に、開発設計のところは、色々な本を読みすぎて、読む本全てに影響を受けて設計が日々変わりすぎていっているのが色々と足を引っ張りました。
こんなところにもエターナル現象は顔をだすんですね。

なので、プロトタイプとしてある程度のものが出来上がったタイミングで、tryは封印し、アプリ完成にフォーカスする事にしました。

以後は、アプリ制作時に大きな節目について記載します。

DBシステムの変更

これまでDBをRealmSwiftで開発していたのですが、
諸々の事情で、結局CoreDataに移しました。

RealmSwiftはCoreDataと違い、Objectを生成して値を突っ込むだけで物理領域にも保存され、直感的にデータを扱うことができる優れもので、開発当初はRealmSwiftを崇拝してやまなかったです。

しかし、

  • DeleteRuleが使えない
  • マイグレーション時に、意図しないデータが潜り込む
  • トランザクション範囲が操作し辛い

等、不満が次々と湧いてきて、最終的にCoreDataにデータを移管しました。

ココらへんは、ここに詳細を記載しています

ただ、RealmSwiftとCoreDataとでは、やはりアプリ設計が異なってくる為、ここを直すために、結構作り直しを余儀なくされました。

(それでもDAOを全て直す気力は無く、一部のDAOはRealmSwift前提とした設計のままになってしまっています)

アイコンをやっぱり全部自分らで造ることにする

開発完了が大きく遅れたのは、ここが原因です。ですがこれは英断だと思っています。

これまで、アイコンをフリーで提供してくださっているサイト様から使わせてもらうつもりでおり、完成直前までこれで行くつもりまんまんでした。

ただ、せっかく造るんだからアイコンも自作する事に決め、ぽぽたが泣きながら作業開始しました。

元々100を超えるアイコンを他サイト様のフリー素材から用意していたのですが、
流石に、この量を開発過渡期に用意するのは無理があったので、数を70ほどにしぼりました。

https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180716/20180716161615.png
(作成したアイコン一覧)

この辺の詳細については、ぽぽたが詳細の記事に起こしています。

www.project-unknown.jp

始まるエターナル化

今回一番厄介だったエターナル化は、機能のエターナル化より、コードのエターナル化でした。
一つの機能を造る度に、4,5回リファクタリングを行ったり。
一度仕上げたコードを、設計しなおしたり。
自分が満足するまで何度も何度も作り直してました。

ただ、これって、

システムの保守性だけ上がって、いつまで経ってもリリースできなくなる自体を引き起こしていました

確かに保守性は非常に大事なのですが、とにかく時間がかかる。
プライベート開発なので納期を気にしないというのが更に拍車をかけてしまっていました。

塩梅を調整するのが本当に難しい…。
このリファクタリングも最終的には自分の中で最低限に抑えるようにしました。

例えば、以下の時だけリファクタリングを行うようにしています。

  • DRYを守れていない
  • SOLIDの原則に準拠していない
  • ViewからModelクラスの細かいメソッドにまでアクセスしようとした時

観点は以下です。


DRYを守れていない

似た機能が乱立した場合、開発過渡期になればなるほど、修正漏れによるさらなる不具合、進捗の遅れに響きます。
ここは徹底的に潰しました。
ちなみにDRYが有名になったのは、以下の「達人プログラマー」からですね。この概念は道を踏み外しそうになった際の羅針盤になり得る考えでいつも大いに助けられています。


SOLIDの原則に準拠していない

iOSライクなMVVMとかではなく、またはプロトコル指向でもなく、オブジェクト指向の1つの考えであるSOLIDをとにかく大事にしました。

プロトコル指向で統一したかった所はありますが、Swiftのプロトコル指向は熟成されていません。これを守るがあまりに無駄で汚いコードになりがちです(会社の現場でこれに囚われて、意味不明なコードを書くエンジニアを散々見ています)。

また、何でもかんでもMVVMを採用するのは愚の骨頂です。ただクラスを増やすだけで、自己満足だけ満たされるコードになるでしょう。
こういうのは適材適所であるべきです。

なので、SOLIDの原則だけを準拠することにしています。
これはDRYを包括している思想ですので、DRYルールと非常に相性が良い。

以下は、SOLIDの原則について記載しています。

  • S - 単一責任の原則 (Single Responsibility Principle)
  • O - 開放・閉鎖原則 (Open/closed principle)
  • L - リスコフ置換原則 (Liskov substitution principle)
  • I - インタフェース分離の原則 (Interface segregation principle)
  • D - 依存性逆転の原則 (Dependency inversion principle)

当たり前だけど、気がついたら守れていない原則ですが、ちゃんと守れている時の効果は絶大です。
このルールには非常に気を使いました。


ViewからModelクラスの細かいメソッドにまでアクセスしようとした時

これはSOLIDの一種ですね。
ViewからModelのデータをごにょごにょするのはイケていません。
その場はそれで良いかもしれませんが、後ほど似たようなデータアクセスが必要になる度にインタフェースを作る羽目になります。


終わらないローカライズ

TIME HACKERは日本以外の世界で発信しています。
細かく言うと、GDPRを受け、EU諸国を除いた全世界に発信しています。

アプリ内のローカライズは、元々ローカライズを意識した実装を行う癖があったので、さほど問題無かったのですが、
問題は、ブログに紹介やらヘルプやらをやたらと親切に記載してしまっていた為、これら全てをローカライズする必要ができました

いやぁー、これはアプリをリリースするのを辞めるか悩むくらい霹靂しました。
最後の1ヶ月間は、このローカライズを延々とやっていた気がします。
実際にアプリ以外のローカライズは以下を対応しました。

繰り返しますが、、

アプリのローカライズ以上に、このローカライズの方が遥かにしんどかったです

しかも、後述しますが、最終的にこの1ヶ月の作業は殆ど無駄になり、申請日にプロジェクトメンバー総掛かりで作り直してます。

最後の追い込み

具体的な日付だと、2018/7/15(土)にプロジェクトメンバーで集まり、
最終テスト、ヘルプページやプロモーションページの仕上げを行って、
夜までに申請を上げて打ち上げを想定していたのですが…、

相次ぐ不具合
まさかの、申請日にUI変更
UI変更に引っ張られて、全ての画像変更

が発生し、気づいたら申請したのは、2018/7/16(日)の夜でした…。
特に、UI変更が本当にしんどかった…。
これにより、具体的には、

以下の、ストアキャプチャ作り直し

英語版iPhoneX

f:id:project-unknown:20180716013728p:plain

英語版5.5 inch

f:id:project-unknown:20180716013744p:plain

日本語版iPhoneX

f:id:project-unknown:20180716013811p:plain

日本語版5.5inch

f:id:project-unknown:20180716013830p:plain

というか、最後の2日間、殆ど画像の差し替えしかやってない気がする…。


くらうリジェクト

今書いているブログ記事もそうですが、
申請を待っている間に、私含め、ぽぽたもTIME HACKER関連の記事を色々書き溜め、申請が通ったらアプリのリリースも含め、ブログ記事のリリースをするために待ち構えていましたが、ものの見事にリジェクトをくらいました

内容は

2. 4 Performance: Hardware Compatibility
Guideline 2.4.1 – Performance – Hardware Compatibility

iPadでの表示が崩れているから、ちゃんとUI整えろよというご指摘です。

実は、リリース前にiPadで表示が崩れる事を知っていましたし、Appleの審査する人はiPadで審査するということも知っていました。

(iPhoneアプリであっても、iPadに内蔵されているiPhoneシミュレータで動くべきであるという考えなので、私が知る限りでは、iPadで審査を行っていそうです。)

ですが、まぁ、操作できない事ないじゃんという謎の過信で、対応を見送っていたところを、案の定指摘されました。

もう、目も当てられない…。

こちら、OSSを使ってUI表現していた所だったので、ライセンスの問題もあり、手を加えるわけにもいきませんから、その場しのぎとして、表示崩れが発生する端末では、一部機能を削除して再申請しました。

初回リリースで諦めた事

たくさんあります。
本当にたくさんあります。

何故か申請日にiTunes Connectの調子が悪くて、iTunes Connectにバイナリアップロードするのに数時間かかった為、
心が盛大にポッキリ折れて、気づいていた簡単なバグはそのままにしてしまっていたり、

UXの検討の時に記録忘れ防止のための通知機能も、
通知タイミングを間違えたらユーザーの不満がものすごく上がりそうという、チキンハートがひょっこり顔を出して見送ったり、

本当に色々積み残しています。

ざっくりと列挙すると、以下の機能を初回リリースでは諦めています。

通知機能

上述した通りです

Apple Watch

Twitterに思いっきりApple Watch化すると記載しているのに、工数の兼ね合いという大人の都合で諦めてます。

Widget対応

大人の都合

分析機能の何点か

最終的にTIME HACKERの押しにしていきたいのですが、
まずは、ユーザーの行動ログを貯めることが何より優先すべき事だったので、ここの機能も結構諦めています。

アプリ内課金

やりたかった…。
本当にやりたかった…。
できなかった…。

KPT

締めくくりとして、今回のTIME HACKERのKPTを考えたいと思います。

Keep

自分の信念を最後まで貫いた

ライフハック大全を読んで、是非このアプリを使いたい、そして造りたいと言う思いを最後まで貫けたのは良かった。

このアプリは、万人受けするアプリではないという事はわかっています。
しかし、ターゲット層項目に記載した人に長く使ってもらう事だけを想定して造っています。

エターナル化を防いだ

本当はAppleWatch、Widget、分析機能の強化、Push等もっとやりたかったけど、これを全て捨てました。
初回リリースでは、ユーザーのログを貯めることを何より重要視し、それ以外の機能は二の次にしました
結果として、この判断を行わなければ、リリースは来年になっていたでしょう。

Problem

何でもかんでもやりすぎた

最低限の事だけしかやっていないつもりだったのですが、今考えると色々手を広げすぎたと思います。

特にローカライズはリリース後でも良かったかもしれません…が、iTunes Connectの仕組み上、プライマリー言語を最初から英語に指定していないと、後々更に面倒な事になっていたので、これでよかったのかもしれませんが…。

またヘルプやプロモーションはやりすぎました、これこそリリース後でも良かった。

至る所でチキンハートが顔を出した

RealmSwiftをやめたのも結局の所、チキンハートが問題でしたし、
チキンハートが顔を出したせいで、ハレーションを生みそうな機能を見送ったり、もっと強気に造っても良かった。

プロジェクトとして、集まって作業する頻度が少なすぎた

最近はリモートワークが流行っていますが、やはり対面でやり取りする作業だと、それぞれのクオリティ・速度が全然違います。

特に申請日に集まって、最後のテストを行った際の不具合の出方がやばかったです。
もうちょっと早めに集まってテストするなどすれば、問題の早期発見に気づけたでしょう。

明らかにリスクがあるとわかっていた不具合を放置した

最後のリジェクトの所ですね。
これは本当にもう愚かだったとしか言いようが無い。
面倒くさがって、対応を先送りにした結果としか言いようが無い。

Try

プロジェクトとして集まる頻度を増やす

これはProblemの裏返しですね、クオリティ・速度を上げる意味で、無理にでも集まって集中して造る時間を確保したほうが良いです。

チキンハートをどうにかする

なにかする度に、いや契約が…とかいや請求一気にきたら怖いしー…とか、
出す前にでもでもだってを発揮して、チャンスをことごとく逃しているのがとにかくもったいない。
やる前に後悔するなら、やって後悔するの精神を持とうと思います。

多少面倒でも正しいことをする

これは最後のProblemの不具合と気づいていて目を背けたものに掛かります。
結局バグの放置や不具合の放置は、問題の先送りでしか無いですし、別な言い方をすれば、ローンと同じで先送りにすれば金利(より面倒な工数)が増えるものとし、リスクがあるものに関しては、速攻潰していくマインドは持ち続けなければと痛感しました。

最後に

自分たちで言うのも変な話ですが、TIME HACKERは、自分の時間を生み出す為に有用なツールに仕上がっています。
また、ユーザーの声を聞いて、機能追加やアイコン追加も行っていこうと思いますので、気になる事などありましたら、いつでも、以下のフォームよりお問い合わせください!

TIME HACKER ご要望・お問い合わせ

是非、TIME HACKERを使って、毎日の生活を少しでも改善していきましょう!

RealmSwift vs CoreData

はじめに

iOSのローカルデータベースとして、これまでは、CoreDataが主流でした。
CoreDataの他だと直接SQLiteを弄ったり、ラッパーとなるFMDBなどのOSSがありましたが、Appleが提供している機能として、やはりCoreDataのシェア率は高かったと思います。

実際に、以下はAppleが公開している資料ですが、
CoreDataを採用する事で、純粋にSQLを叩くよりスピードが上がるや、メモリ効率も良い等、夢のようなツールとして紹介されています。

f:id:project-unknown:20180717010316p:plain:w430

しかし、CoreDataは学習コストが非常に高く、扱えるようになるまで時間が掛かったり、 扱えるようになったとしても、他のDBMSライクに扱おうとしたらどハマりしたりと、非常にクセが強いです。

iOSアプリの現場で、CoreDataを採用しているPJ等に配属された際に、困った人も多いのではないでしょうか。

そんな中登場したのが、RealmSwiftです。

f:id:project-unknown:20180717010114p:plain:w430

Realmは、公式サイトから紹介を引用すると

Realm Swiftはアプリケーションのモデル層を効率的に安全で迅速な方法で記述することができます。

実際に使ってみた感想だと、とにかく実装が楽です。
あまり考えなくてもデータの永続化もでき、UserDefaultsみたいなお手軽感があります。

しかし、

結論から先に言うと、TIME HACKERアプリを開発中に、私はCoreDataを選択し、RealmSwiftベースで作ってきたアプリの構造をCoreDataに移しました。

他のサイトでも、RealmSwiftとCoreDataの比較がなされているところが多いですが、私の主観でも書いてみようと思います。

リレーションシップをやろうとすると面倒

RealmSwiftでリレーションシップライクな事をやろうとすると、以下のように記述します。

FirstEntity

import UIKit
import RealmSwift

class FirstEntity: Object {
    @objc dynamic var id = NSUUID().uuidString
    @objc dynamic var second: SecondEntity? = nil                

    override static func primaryKey() -> String? {
        return "id"
    }
}

SecondEntity

import UIKit
import RealmSwift

class ActionEntity: Object {
    @objc dynamic var id = NSUUID().uuidString
    
    let firstEntity = LinkingObjects(fromType: FirstEntity.self, property: "second")  // FirstEntityがぶら下がる
}

上記のように書くと、SecondEntityにFirstEntityがぶら下がる形となり、
SecondEntityにFirstEntityを登録する事で、データを一気に登録・参照する事が可能になります。

これだけなら、非常に便利なのですが、 削除する時(Delete Rule)が問題になります。
RealmSwiftでは、DeleteRuleを設定することができないため、他のDBMSにあるように、親となるデータを削除した際に、子のデータも一緒に削除することはできません。
(私が開発していた時に、気づいていなかっただけなら申し訳ありません。)
ですので、RealmSwiftでデータ削除する際に、親も子も手動で削除していく必要があります。

反面、CoreDataの場合だと、

f:id:project-unknown:20180716180641p:plain:w430

こうする事で、親のデータを消したら、子のデータまで一気に消えます。


トランザクションが分かりにくい

これは完全に慣れの問題ですが、RealmSwiftの場合、コードブロック内でトランザクションが貼られます。

逆にいうとトランザクションから外れてコードを書くと思わぬところでデータ更新が走って詰まった事がありました。

CoreDataのコンテキスト内に更新ロジックを書いて、最後にsaveする書き方に慣れてしまったので、ここは凄く違和感を感じました。


マイグレーションに違和感がある

例えば以下の構成があったとします。

import UIKit
import RealmSwift

class FirstEntity: Object {
    @objc dynamic var id = NSUUID().uuidString
    @objc dynamic var updateDate? = Date()
}

これにデータを突っ込んで、
アップデートのタイミングで、以下のようにカラム名を修正します。

import UIKit
import RealmSwift

class FirstEntity: Object {
    @objc dynamic var id = NSUUID().uuidString
    @objc dynamic var createdDate? = Date()
}

この時、これまで入っていたDateが初期化されて、1970年が入ってました。

単純にマイグレーションする際の考慮漏れはありますが、CoreDataの場合だとここでクラッシュして気付けます。

RealmSwiftの場合だとデータを登録して結果を見て初めて気付くと言うところが個人的にはしんどいです。

実際に個人で開発しているときは、コアな箇所だとユニットテストを書いたり、ある程度のテストをおこないますが、それでも余暇の時間を使うのもあってテスト漏れは発生しやすいです。

この時に気付けるのか?というのがCoreDataを採用する1番の動機となりました。


締めくくり

結局のところで言うと、RealmSwiftに慣れていなくて、CoreDataは散々使ってきたので慣れていた。
が、CoreDataを採用した1番の理由です。

RealmSwiftは触ったから分かる素晴らしいものですが、私の学習が追いつかなかった。
RealmSwiftはサーバとのデータのやり取りも非常に楽に実装できるよう、サポートされていますし、いずれは使いこなせるにしていきたい限りです。

最後に

TIME HACKER

記事の途中で記載しましたが、今回の記事は、TIME HACKERを開発中の記事となります。

是非お使いください!

Firebase A/Bテストを試す - Firebase A/B Testing

Firebase A/Bテスト

Firebaseが提供しているA/Bテストを利用した記事となります。

この記事では、A/Bテストとは?から、 実際にTIME HACKERアプリで、インタースティシャルの表示頻度を確認する際に、A/Bテストを導入しているので、実際に実装した際の手順・コードをご紹介します。

最初に、A/Bテストの説明や、TIME HACKERでのA/Bテスト設計について論じています。
実装方法だけを確認したいのであれば、こちらから御覧ください。

A/Bテストとは?

A/Bテストとは、アプリやWebページ等で数パターンの機能・表示を用意して、ユーザー毎に出し分けを行う事で、
用意した数パターンの機能・表示の内、どのパターンが優れているのかを見極めるのに利用します。

A/Bテストは単純に機能だけの評価だけではなく、マーケティング上だとより高いCTR, CVRを得られるのか検証したり、比較的メジャーなテストです。

特にアプリではこのテストは非常に有用で、iOSアプリ等は、機能検証しようとしたら、毎回申請を挟まないと行けないなど、多大な時間を要するのですが、
A/Bテストの概念を採用する事で、一度の申請で複数の検証を行うことが出来ます。

A/Bテストはどうやるのか?

いろんなサービスがありますが、どこもやることは簡単です、
Webサーバにパターン毎のjsonファイルを設置し、アプリ側でそのjsonファイルを取得し、イベントログ等を送信する事で検証を行います。

f:id:project-unknown:20180716184438p:plain:w430
(イメージ図)

今回のお題としているのは、FirebaseでもA/Bテストをサポートしている為(まだβですが 2018/7/16段階)、それを採用した実例の紹介となります。

Firebase A/Bテストは、jsonの設置もそうですが、イベントトラッキングも全てサポートしてくれているので、一つのコンソール上でA/Bテストで必要な一連の操作ができるので非常に便利です。

何のA/Bテストを実施するか?

当たり前ですが、何のA/Bテストを行うのか?はちゃんと設計しないといけません。
TIME HACKERでは、冒頭でも記載しましたが、インタースティシャルの表示頻度の最適解を求める上で、A/Bテストを実施しています。

ここでは、TIME HACKERでのインタースティシャルA/B設計について記載します。

インタースティシャルの頻度を何故測るのか?

インタースティシャルは全画面に表示される広告なので、ユーザの行動を大きく阻害します。
頻度を間違えた場合は、最悪ユーザがアプリから離脱していってしまうでしょう。
TIME HACKERではアクションの計測を開始したタイミングでインタースティシャル広告を表示するように設計しています。
ですが、アクションを計測するのは、1番ユーザーが利用する機能です。
なのに、毎回アクションを計測する際に表示するのでは、あまりにもフラストレーションが溜まるでしょう。

この解決策として、インタースティシャルの表示頻度を調整します。
まず1度表示したら10分間は表示させません。(これはいたずらに広告をタップする事による、アドセンス狩り対策の意味でも行っています。)

しかし、10分だけで本当に良いのか?
答えはNOです。TIME HACKERでは、ユーザがアクションを起こす時に起動するアプリです。
言うなれば、アクションとアクションのつなぎ目に起動してもらうアプリです。
そのアクションが、例えば

  • スマホをいじるのであれば、次起動するのは1時間後
  • 映画を見るのであれば、次起動するのは2時間後

等、10分だけの制限では、ほぼ毎回記録測定のタイミングで広告が表示されてしまします。

ですので、10分制限は、あくまでアドセンス狩りとして捉え、ユーザーの行動を阻害しない策として、何回計測実行したのか?で表示するようにします。

TIME HACKERでのA/Bテストで見る数値

以下の数値を追います。

アプリの定着率

定着は4-7日で計測します。
TIME HACKERは毎日複数回利用していただく事を想定したアプリです。
生活に密着している指標として、1日で見るのではなくて、4-7日の定着数を見ます。
ここが、インタースティシャルの頻度によって変わるのであれば、見直す必要が出てきます。

広告のクリック数

ユーザーの事を考えないのであれば、そもそもインタースティシャル広告を出さない事が1番であることは明白です。
ですが、アプリの収益で生活するなどを考えるのであれば、収益を得るのはマストでしょう。

なので、広告のクリック数を見る事は、絶対必要です。

AdMobの推定収益

こちらは、広告のクリック数でも似たような指標を取ることができますが、

TIME HACKERでのインタースティシャル表示制限のA/Bテスト設計

TIME HACKERでは、以下の条件でテストを行います。

Firebase A/Bテストを用いたA/Bテストを実施する

A/Bテストの準備をコンソール上で行う。

Firebaseコンソールから、A/B Testingを選択します。

f:id:project-unknown:20180716185256p:plain

テストの名前や、概要、どの割合のユーザをA/Bテストの範囲に含めるのか?を設定します。

f:id:project-unknown:20180701142853p:plain:w430

今回は100%のユーザとしていますが、規模の大きなアプリ等では、全てをテスト対象にするのではなく、ごく1部のユーザをA/Bテストのターゲットとしても良いかと思います。

次に、バリアントを設定します。
要はA/Bテストターゲットユーザのうちに、どの割合でどんな値を設定するのか?を指定します。

TIME HACKERでは、インタースティシャル広告の割合を決めたいので、
25%区切りで、値を設定しています。

f:id:project-unknown:20180701145338p:plain:w430

最後に目標を入力します。
ウォッチしたい数値をここで指定しておくことで、Firebaseが該当する数値を集計してツール上で確認できるようになります。

f:id:project-unknown:20180701145347p:plain:w430

ここまで設定したら、次へ進みます。

次に進むと、今設定したA/Bテストが下書きの状態で表示されます。

f:id:project-unknown:20180701150301p:plain:w430

アプリの設定を行う。

次にアプリの設定を行います。

plistを用意する。

f:id:project-unknown:20180701151216p:plain:w430

アプリを実行して、tokenを確認しておく。
↓の箇所。

debugPrint("a/b test token :\(InstanceID.instanceID().token()!)")

Firebase コンソールの準備を続け、テスト開始する

テスト確認してみましょう。

f:id:project-unknown:20180701153510p:plain:w430

今確認したTokenを入力して、テスト確認したいバリアントを設定します。

f:id:project-unknown:20180701153615p:plain:w430

では、テストを開始しましょう。

f:id:project-unknown:20180701153703p:plain:w430

開始して間もないのでデータは無いですが、以下の画面のようにテスト実施の画面になっているはずです。

f:id:project-unknown:20180716192307p:plain:w430

アプリ側でA/Bテスト用のコードを埋め込む

以下、実際にTIME HACKERアプリで実際に作ったコードの一部です。

import Firebase

#if DEBUG
fileprivate let Interval = 0
#else
fileprivate let Interval = 60 * 60 * 24
#endif

fileprivate let RemoteConfigKeys  = "interstitialTimesOnTapNum"
fileprivate let RemoteConfigPlist = "RemoteConfig-default"

class InterstitialTestManager: NSObject {
    
    private let remoteConfig = RemoteConfig.remoteConfig()
    
    private static let instance = InterstitialTestManager()
    
    public class var shared: InterstitialTestManager {
        
        return instance
    }
    
    
    /// ABテストのセットアップ
    /// 初回AppDelegateなどで1度呼ぶだけで十分
    func setup() {
        
        remoteConfig.setDefaults(fromPlist: RemoteConfigPlist)

        if let settings = RemoteConfigSettings.init(developerModeEnabled: false) {
            remoteConfig.configSettings = settings
        }
        
        remoteConfig.fetch(withExpirationDuration: TimeInterval(Interval), completionHandler: { (status, error) -> Void in
            if status == RemoteConfigFetchStatus.success {
                self.remoteConfig.activateFetched()
            } else {
                print("fetch error.")
            }
        })
    }

    
    /// インタースティシャルの頻度を取得します
    ///
    /// - Returns:
    func interstitialFrequency() -> Int {
        var frequency = 10
        
        if let number = remoteConfig[RemoteConfigKeys].numberValue {
            frequency = number.intValue
        }
        
        return frequency
    }
    
    
    /// インタースティシャルを表示するかを判定します。
    ///
    /// - Returns:
    func isDisplayInterstitial() -> Bool {
        
        if actionRunningNumForCount() > interstitialFrequency() {
            return true
        }
        
        return false
    }
    
}

使い方は、まずAppDelegateでセットアップを呼び出します。

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    InterstitialTestManager.shared.setup()
}

後はインタースティシャルの発動条件の条件分に以下のコードで判定を埋め込むだけです。

func displayInterstitial() {
    guard InterstitialTestManager.shared.isDisplayInterstitial() else {
        return
    }
    // インタースティシャル表示
}

これで、A/Bテストを埋め込む事が出来ました。

注意点

載せているSampleコードの以下の部分

if let settings = RemoteConfigSettings.init(developerModeEnabled: false) {

ここの、developerModeEnabledを開発時はtrueにすることで、RemoteConfigのキャッシュタイムを好きにいじることができるようになります。
が、リリース時は、falseにして、Debugモードを解除しておいたほうが無難でしょう。


締めくくり

全て自分でつくろうと思うと、かなり難しいというより、面倒な作業が必要となるため、
Firebase A/Bテストを採用する事で、簡単にA/Bテストを実行する事ができ、
またイベントトラッキングはGoogle Analyticsを利用しているので、より詳細な分析も行うことができそうです。

まだリリースして間もないので、Firebase コンソール上でテスト結果を集計出来ていないので、結果については、このタイミングではお話することができません。

ある程度集計できて、新たな気付きがあれば加筆修正を行おうと思います。

最後に

img

今回の記事で紹介していますが、TIME HACKERで、Firebase A/Bテストを採用しています。
是非お使いください!

written by ゆう@あんのうん

デザイン視点でTIME HACKER

img

はじめに

KeyHolderに次いでProject.Unknown2作目のアプリ、時間管理系ツールTIME HACKERがリリースされました!
デザイン担当のぽぽたです。

この記事ではデザイン視点で、TIME HACKER完成までのいろんなことを紹介させていただこうと思います。

TIME HACKERの紹介ページはこちらになります。
TIME HACKER - 時間管理 - Project Unknown

ロゴデザイン決定までの流れ

f:id:project-unknown:20180716170654p:plain

ロゴのデザインは、ゆう@あんのうんさんとの度重なる話し合いで決めていきました。

自分の時間を管理し、生活改善のサポートをするアプリということで、ゆう@あんのうんさんから時間や計測のイメージと聞き、いろんなサイズや色違いのアイコンを作ってみました。
テーマカラーになる青は割と早い段階にノリで決まった気がします。ゆるい。

f:id:project-unknown:20180716170706p:plain

そこから更に絞って...を何度か繰り返し。
ロゴはアプリの顔になるので、納得いく形になるまで話し合いました。

f:id:project-unknown:20180716170011p:plain

最終的に決まったTIME HACKERの公式ロゴ。
時計と円グラフを合わせたイメージになっています。

作成したアイコン

TIME HACKERで作成したアクションアイコンの一覧です。

f:id:project-unknown:20180716161615p:plain

ここに来て初めて数えてみたのですが、計68個になりました。
こう並べてみると数より少なく感じてしまいますね、自分の感覚的にはこの倍くらい作ったように思うのですが...。

本当は船、飛行機、バイク、ホワイトボードなども作る予定だったのですが、話し合いの末現段階では見送ることになりました。
こちらより機能・アイコン追加要望のページを設けていますので「ボツになったアイコンを使いたい!」や「こんなアイコンが欲しい!」などありましたらお気軽にご提案ください^^

TIME HACKER ご要望・お問い合わせ

アクションアイコンのデザインについて

ゆう@あんのうんさんから「こういうアイコンが欲しい」というリストをもらい、それを元に考えていきました。
ロゴもそうなんですが、アイコンを作るということ自体あまり経験がなかったので、結構苦戦してしまいました...。

シンプルで わかりやすく 使いやすいを意識しているアプリなので、アイコンも極力シンプルにしたかったんですよね。

f:id:project-unknown:20180716193234p:plain

初期に作ったアイコン3種。
〜不純物0%、クリアーな味わい〜という感じのシンプルさですね。

f:id:project-unknown:20180716193839p:plain

後期に作ったアイコン3種。
描けば描くほど前面に乗り出してくる『伝われ...!』という気持ちとこだわりが反映されていくアイコン達......。

電車は最初、シンプルに横向きの四角いハコにしたかったんです。
しかしサイズ的にとても短く路面電車のようになってしまうな...と思ったので、奥行きを意識したのは苦肉の策ではありました。
単純にシンプルといっても、伝わらなければ仕方ないですもんね...。

f:id:project-unknown:20180717002236p:plain

いろいろなアイコンの要望をもらい着手し、初めて気づいたのは動詞を表現するのは難しい!ということですね。
上のアイコンは何を表現しているかわかりますでしょうか?
アイコンを作る際の要望リストには、左からくつろぐ 遊ぶとありました。

くつろぐスタイルは人それぞれですよね。
スマホをいじりながらベッドでゴロゴロや...ソファーに座りコーヒーを飲みながら本を読む...。
遊ぶに関しても、なにでどう遊ぶのか...シンプルなアイコンでどう表現すれば伝わるだろうかと結構悩みました。
いろいろ考えてみて、最終的に決めたのがくつろぐ=ソファーと、遊ぶ=楽しそうな感じでした。

今後アイコンを追加する際は、もっと分かりやすく、もっとシンプルに表現できるよう表現力も養いたいところです。
アクションアイコンは全て好きに名前をつけることができるので、自分の感覚で好きに使っていただければ嬉しいです!

レポートアイコンのデザインについて

f:id:project-unknown:20180717010732p:plain

こちらは手描きで紙にいくつか案を出してみたもの。

グラフ的なものは、あまり考えずにロゴに寄せて円グラフ表現にしていたのですが、一番下の画像の、時間管理の画面は棒グラフ表現なのに円グラフアイコンだと混乱しない?という、どうまずさんからのごもっとも過ぎるツッコミにより、全員意見一致で急遽円グラフから棒グラフに変更になりました。
アプリ申請当日の出来事です(笑)

f:id:project-unknown:20180717012427p:plain

最終的に決まったレポートアイコン各種。

最後に

KeyHolderでもアイコンは少し作成したのですが、比でないほどたくさんのアイコンを作成することになったTIME HACKERでした。
そして初めての経験や思考錯誤の末の発見やと、とても実りのある開発でした。

まだまだデザイナーと胸を張れるほどの力量はないのですが...これからもProject.Unknownのデザイン担当として少しづつ精進していきたいです!

f:id:project-unknown:20180717051430p:plain

TIME HACKERは自分の時間を管理し、生活改善のサポートをする時間管理アプリです。
気になりましたら是非使ってみてください^^
timehacker.page.link

う〜のん語録

うなな:バナナ
うまいらる : おいしいもの、おやつ、食べること