iPhone用のアラームアプリを作った【iOS】

初の個人アプリをリリースしました!
そのアプリがこちらです。

簡単に言うとアラームアプリです。 アラームアプリは App Store にたくさんあるのですが、自分が欲しかった

  • 秒まで指定できる
  • 通知はバイブレーションのみ(音が鳴らない)

の2つの機能が備わっているものが見つからなかったので作ってみました(よく探せばあるかもですが) 。

作るに至った経緯

毎日、通勤で約1時間電車に乗っていたのですが、座れる時にはアラームをセットして寝ていました。
その際には、寝過ごさないように、他の方が作ったアラームを使って目的駅に到着する1分前に時間をセットしていました。
そのため、1分前になったらアラームが鳴って起きてはいたのですが、めちゃくちゃ眠い状態だと判断が鈍ってしまうようで、1分あればもう一度寝られると考えて二度寝してしまい、危うく乗り過ごしそうになることが何度もありました。
幸い乗り過ごしたことは一度もなかったのですが、これはいつかやらかすと思い、そもそも二度寝したくてもできないぎりぎりの時間(30秒前とか)をセットできれば良いのではと思いました。
ですが、この時使っていたアプリは1分単位でしか時間をセットできなかったので、秒まで指定できるアラームを探したところ、自分の欲しいようなアプリがなかなかなかったので、今回のアプリを作るに至りました。

技術的な話

このアプリを作った目的の一つとしては、新しいことを学ぶということもありました。
業務では、結構大きなサービスの iOS 開発をしているのですが、普段触るものとは違う技術を触ってみたい、また、一からアプリを作ることで、いつもであれば学ぶことのできない新しい学びがあるのではないかというように思っていました。

RxSwift + MVVM

iOS 開発のアーキテクチャについて学びたいと思っていたので、流行りの RxSwift を使い、結果的に MVVM について学ぼうということで RxSwift を使いました。

とりあえず Rx の雰囲気は掴めたような気がしているのですが、 RxSwift でコードちゃんと書いていますと言えるほどには身につかなかったなあという気がしています。
ですが、 RxSwift の学習コストの高さや、データバインドの良さや、関数型言語のように記述できることの便利さ等は実感できたので、ひとまずよかったかなあと思います。

ライブラリ公開

ライブラリを作ったことがなかったので、アプリを作っていく中で、ある程度抽象化できそうな部分があったらライブラリを作って公開してみたいなと思いました。

結果的に、 LabelPicker という UIPickerView 関連のライブラリを公開することができました。

github.com

アプリ内では、時刻、時間を指定する UI に使用しています。
標準の UIDatePicker でも時刻、時間はもちろん指定できるのですが、秒指定と単位の表示ができなかったので、これらを解決するためのものとして作りました。
これでライブラリの作り方を知れたので、また何か抽象化できそうなことがあればやってみようと思います。


他にも学べることはたくさんあったので、やって満足しています!
残念だった点としては、今はもう引っ越してしまい、長い電車通勤ではなくなってしまったため、ドッグフーディングができなくなってしまったということがありました。
でもまあ、日常生活の中で利用する機会はあるかと思うのでその際には使っていきます。

アプリに関して何かあればコメントください〜

Auto Layout の Content Hugging Priority と Content Compression Resistance Priority

すぐ忘れるので。

Content Hugging Priority

コンテンツの表示サイズに沿って表示されることの優先度。

そのため、この値が大きい方が優先的にコンテンツと同じサイズになる。

Content Compression Resistance Priority

コンテンツが圧縮されないことの優先度。

そのため、この値が大きい方が優先的にコンテンツが見切れないようになる。

iOS の時間表示設定を取得する

iOS の日付と時刻の設定の24時間表示が ON になっていれば24時間表示用の UILabel を、そうでなければ12時間表示用の UILabel を表示するという用に、時間表示設定によって View を切り替えたいと思ってググってみたのですが、解決策が見つからなかったので軽く調査してみました。

Locale の調査

DateFormatter を扱う時等に、強制的に24時間表記で時刻を扱えるようにと NSLocale.system を使用したことを思い出して、Locale が時間表示設定の情報を持っているのではと思って確認してみました。

(lldb) po NSLocale.system
▿  (fixed)
  - identifier : ""
  - kind : "fixed"

identifier と kind しか持っていない、、、

念のために24時間表示設定がONの時の Local.current も確認してみました。

(lldb) po NSLocale.current
▿ ja_JP (current)
  - identifier : "ja_JP"
  - kind : "current"

Locale が時間表示設定の情報を持っているわけではなさそうですね、、、


となれば、次は———

と別の方法で調査できたらよかったのですが、思いつく方法がなかったし、ググっても見つからないということは時間表示設定はそもそも取得できないのではと思ったので、以下の方法で無理やり判別することにしました。

if String(describing: Date()).contains("午") {
    print("12時間表示")
} else {
    print("24時間表示")
}

もっと良い判別方法があれば是非とも教えていただきたいです、、、

Swift の Date の操作

Date の操作方法が全く覚えられないので、調べたことをここにまとめていく。

日時を指定して Date 生成

Calendar(identifier: .gregorian).date(from: DateComponents(year: 2017, month: 8, day: 13))

ある Date の次の日の Date 生成

Calendar(identifier: .gregorian).date(byAdding: .day, value: 1, to: Date())

UnitTest 用のデータを共通化する【Swift】

テストをする際には複数のオブジェクトの初期化をする必要があり、この作業が面倒でテストを書くのが面倒になってしまうことがあったので、どうにかできないかと思い調べてみたところ、良さそうな方法がありました。

clean-swift.com

これを、自分が使いやすいと思った形に少しアレンジしたものを紹介してみようと思います。

Seeds Struct

以下のように Human というクラスがあったとします。

class Human {
    
    var name: String
    var age: Int
    var height: Double
    var job: String
    
    init(name: String, age: Int, height: Double, job: String) {
        self.name = name
        self.age = age
        self.height = height
        self.job = job
    }
    
}

この Human に関するテストを書くといったときに、以下のようにテストを書く度に値を指定して初期化していくかと思いますが、本当に全部の値を指定しなければならないというケースはそれほど多くはないのではないかと思います。

let takeru = Human(name: "takeru", age: 30, height: 180.0, job: "engineer")
let rika = Human(name: "rika", age: 28, height: 160.0, job: "designer")

こういった時に上記の記事に書いてあるようにして Seeds というテスト用のデータを持つ Struct を用意することで、Xcode 等の補完機能を用いながら以下のように簡単にデータを作成することができます。

struct Seeds {
    
    struct Humans {
        
        static let takeru = Human(name: "takeru", age: 30, height: 180.0, job: "engineer")
        static let rika = Human(name: "rika", age: 28, height: 160.0, job: "designer")
        
    }
    
}

Seeds.Humans.takeru
 => Human(name: "takeru", age: 30, height: 180.0, job: "engineer")
Seeds.Humans.rika
 => Human(name: "rika", age: 28, height: 160.0, job: "designer")

Problem

ですが、上記の様に Stored Property を static で宣言してしまうとシングルトンになってしまいます。
そのため、テスト間でオブジェクトのプロパティの値を変更する時に、思わぬところでテストが落ちてしまうということがあるかもしれないので、値を変更する際には気をつけなければなりません。

let takeru = Seeds.Humans.takeru
takeru.age = 10

// 一度プロパティの値を変更するとその後も維持されてしまう
Seeds.Humans.takeru.age
 => 10

Computed Property

そこで、テストデータの定義には以下のように Computed Property を用いるようにします。
こうすることで、以下のように Seeds からオブジェクトを呼ぶ出す度に異なるオブジェクトが返ってくるため、安心してプロパティの値を変更することができます。

struct Seeds {
    
    struct Humans {
        
        static var takeru: Human {
            return Human(name: "takeru", age: 30, height: 180.0, job: "engineer")
        }
        static var rika: Human {
            return Human(name: "rika", age: 28, height: 160.0, job: "designer")
        }
        
    }
    
}

let takeru = Seeds.Humans.takeru
takeru.age = 10

// 異なるオブジェクトなので 30 のまま
Seeds.Humans.takeru.age
 => 30

Unique Value

先ほどの Human に一意の値となる myNumber というプロパティを持たせる必要が生じたとします。

class Human {
    
    var name: String
    var age: Int
    var height: Double
    var job: String
    let myNumber: Int // 追加
    
    init(name: String, age: Int, height: Double, job: String, myNumber: Int) {
        self.name = name
        self.age = age
        self.height = height
        self.job = job
        self.myNumber = myNumber // 追加
    }
    
}

こういった、 オブジェクト毎に異なる一意の値を持たせたいという場合においても、Seeds に以下の uniqueInt の様なコンピューテッドプロパティを持たせることで、オブジェクトを呼び出す度に自動的に持たせることが可能となります。

struct Seeds {
    
    static var increment = 0
    static var uniqueInt: Int {
        increment += 1
        return increment
    }
    
    struct Humans {
        
        static var takeru: Human {
            return Human(name: "takeru", age: 30, height: 180.0, job: "engineer", myNumber: Seeds.uniqueInt)
        }
        static var rika: Human {
            return Human(name: "rika", age: 28, height: 160.0, job: "designer", myNumber: Seeds.uniqueInt)
        }
        
    }
    
}

Seeds.Humans.takeru.myNumber
 => 1
Seeds.Humans.takeru.myNumber
 => 2

テストは人間が見過ごしてしまうような部分や人間がすべきでないような部分をカバーすることができるので書くに越したことはないのですが、やはり辛い面も多々あるかと思うので、テストを書く度に毎回同じ様なオブジェクトを用意していて辛いなあと感じたら、こういったテストデータ作るくんを用意してみてはいかがでしょうか!

Swift化開発合宿 in 熱海♨️

先日、会社のモバイルエンジニア5名でSwift化開発合宿に行ってきました!
普段はなかなか Swift 化を進めることができないので、こういった機会を設けてガガガっと進めてしまおうということから、今回の合宿が決まりました。
この開発合宿記録を書いていきます。

1日目

移動

みんなでお昼ご飯を食べてから宿泊所の最寄駅である網代駅に向かいました。 特急等は一切使用しなかったので、電車だけで約2時間半かかりました。
着くと綺麗な梅の花がお出迎え。
普段は都会のビルばかりを見ているので、男5人で「梅が綺麗だね」とかなんとか言いながら、熱海に到着したことに浸っていました。 合宿場となる別荘は、駅から2kmほどの山奥にあるということを聞いていたので、早速タクシーを捕まえました。
タクシーの運転手さんと「この辺はイノシシがそこらへんにいるから気をつけなよ」「昔は良くイノシシを捕まえて食ったもんだよ」「俺らからしたら犬もイノシシも変わらんよ」と、いのししトークで盛り上がっているとあっという間に到着しました。

宿泊所

旅館ではなく、元々は別荘であっただろう場所だったので、部屋はとても広くて3LDK。山の中にあるということもあり、窓からは見えるオーシャンビューはとても綺麗でした!

f:id:komaji504:20170322222818j:plain

肝心なのは、ネット環境です。
事前情報としては WiFi 環境がないということだったので、 WiMax を持参していたのですが、山奥ということもあり、そもそもこの WiMax も使えるかということが不安でした。スイッチを入れてみるたのですが、やはり繋がらない… 部屋の中であちこち移動させてみるとなんとか繋がる場所があったのですが、これも不安定だったので、結局スマホテザリングも併用して乗り切りました。

ちなみに作業場はこんな感じです。

f:id:komaji504:20170322223133j:plain

Swift化

ひたすら Objective-C で書かれているコードを Swift に書き換えていきます。
前日に作戦会議をして担当範囲や進め方を決めていたので、各々、担当範囲を黙々と書き換えて行きました。

f:id:komaji504:20170322223516j:plain

夕食 & 温泉

山の中なので近くにお店もなく、下山するのも大変なので、事前に最寄駅のコンビニで大量に食材を買っておいて、それにありつきました。近くにお店がないような環境だと、自分以外が食べているもの全部が美味しそうに見えてきてしまうのが不思議ですね…

温泉ですが、ここの宿には本来露天風呂があるらしいのですが、修理中とのことなので、部屋についているもので我慢。
ですが、蛇口からはなんと温泉が出る!23時以降は温泉が止められてしまうとのことだったので、早めに温泉を溜めておいて浸かりました。やっぱり温泉は最高!

2日目

散歩

朝に軽く散歩をしました。
自動販売機があったのでコーヒーを買ったところ、サンプルと違う…!
コーヒーには違いがないので良しとしましょう。こんなことも熱海なら全然OKです。

f:id:komaji504:20170322223711j:plain

Swift化

散歩から帰ってきて、コンビニで買った朝食をとったらまたまたひたすら書き換えをしていきました。
2日目ということもあり、黙々とSwift化することにも慣れてガガガっと進めていきました。

夕食

最終日なのでうまいものを食べようと下山することにしました。
タクシーを呼ぼうと思ったのですが、全然捕まえることができなかったので自らの足で山を下りました。男5人が集まっても、やっぱり山はなんだか怖いので、各々が好きな音楽を流しながら気を紛らわせながら歩きました。2km といえども山道はとてもキツく、道が平坦になってきた頃には足は棒切れ状態でした…

食事はやっぱり寿司!!!にしようと思ったのですが、寿司屋はこの辺にはないとのことを伺っていたのでとりあえず魚が食べられるお店へ GO!
刺身定食を食べたのですが、必死の思いで下山したこともあり最高に美味しかったです。刺身は見た目も豪華だしとにかく最高!

f:id:komaji504:20170322222827j:plain

帰りは運良くタクシーを捕まえられたので歩かずに済みました。歩く気力も体力もなかったので本当に良かった…

合宿を終えて

こんな感じで、海と山に囲まれて美味しい魚を食べることもできたので Swift 化という単調で楽しいわけではない作業もガガガっと進められて、開発合宿ってやっぱり最高!という気持ちになりました。普段の機能開発の時間とは別に、こういった機会があると普段なかなか行えないようなことも行えて良いですね。

合宿の環境としては、やっぱり WiFi が用意されているところを探したほうが安心でした。
山の中ということに関しては、景色は綺麗で良かったのですが、やっぱりお店が近くにないのも少し不便だったので、コンビニくらいは近くにある場所が良さそうと思いました。
次回、開発合宿をやるなら上記を満たすような場所を探そうと思います。

何より、こういった開発合宿に関して、積極的に支援してくれる会社に感謝です!

成果

合宿前の Swift の割合は約 30% でした。

f:id:komaji504:20170323001552p:plain


そして、



合宿後の Swift の割合が…



こちらです!!!

f:id:komaji504:20170323003519p:plain

約 5% の Objevtive-C のコードが Swift に置き換わったということですね。
ちなみに、置き換えたコード量でいうと約 5000 行でした。

まだまだ Objective-C のコードがわんさかいるので、引き続き撲滅作業を進めていきたいと思います!

iOSアプリ開発で実機のログに filter をかける

アプリ起動中のログであれば Xcode のコンソールから確認できますが、 アプリを起動したり終了させたときのログを確認したい場合には、Xcode のコンソールでは確認できないので実機のログを確認する必要があるかと思います。

このとき Xcodeツールバーの Window -> Devices から実機を選択すれば Xcode 上でログを確認することができますが、この方法だとログに対して find はできるのですが、 filter はかけられないので結構不便だったりします。

そこで、なんとか filter をかける方法がないかなと思って探してみたら方法がありました〜

libmobiledevice

www.libimobiledevice.org

こちらを使うことでターミナルでログを確認できるので、それを grep すればよいということです。

Homebrew から入れることができるのですが、 iOS10 の実機のログを確認する場合には、以下のように HEAD を指定して install しないといけないようです。

$ brew install -v --HEAD --fresh --build-from-source libimobiledevice

refs:

[SOLVED] Unable to start syslog with iOS 10 on Windows · Issue #325 · libimobiledevice/libimobiledevice · GitHub

使い方

$ idevicesyslog -u 実機のUUID

でターミナルにログを出力することができます。
もちろん UUID を確認するコマンドもあります。

$ idevice_id -l

以下のように出力されるログに対して grep すれば filter をかけることができます!

$ idevice_id -l | xargs idevicesyslog -u | grep hogehoge

これで快適なデバッグライフを送ることができそうです!