hiragram no blog

iOSとか

iOSアプリのバックエンドにFirebase使ってみた感想

習作としてレシピのマスターデータがあってそれを作ったよというレポートをアプリから投稿できるようなやつを作ってみて感じたこと。

とりあえず動かすのが超簡単

コンソールからプロジェクトつくってキー発行してSDK入れて初期化すればDBにアクセスできて簡単だった。

考えなしに使うと多分いろいろ破綻する

  • DBがスキーマレスなのでなんでもぽいぽいpostできちゃう
    • DB操作する所は1箇所にまとめて抽象化しないと欠けたオブジェクトがDBに乗っちゃったりして詰みそう。そこはSwiftのカチッとした型と柔らかいスキーマレスDBとの境界で大変というかんじ。考えなしに進めてオブジェクトのプロパティがどんどんOptionalになっていくのは見ていられないので抽象化がんばりましょう
    • 既にデータが刺さってるモデルにプロパティ追加したりしても古いデータは当然マイグレートされないので古いデータが落ちてきてパースに失敗するみたいなこともよく起きそう。
  • DBへの書き込みはアプリ内で直接やらないほうがいいかもしれない
    • アプリのアップデートで書き込みの操作が変わっても古いアプリでDBいじられて壊れるみたいなことが将来ありそう。
    • 使ってないけどFirebase Functions経由でしかwrite操作できないようにすればいいのかしら。そうするとオフラインでも使えますみたいな旨味が無くなっちゃうけど。

オフラインDB、開発中は便利かもしれないけど本番運用で役に立つイメージあんまない

オフラインでデータの読み書きができたところで、画像などは結局ダウンロードできないので「オフラインでもアプリが完全に動作する」を実現するのはそんなに簡単じゃなさそうだなーという印象。 僕は通勤の電車のなかでコード書くのでその時はオフラインDBすごく助かった。

RxSwiftとの相性はいい感じに見えた

DB抽象層としてRxSwiftのObservableをふんだんにつかったRepository層みたいなのを作ったけどデータ更新されたら都度ストリームにながすみたいなことができてリアクティブプログラミングとの相性は良さそうな感じだった。

[2017/09/08追記] ユーザー管理が超楽

iOSアプリのバックエンドにFirebase使ってみた感想 - hiragram no blog

Firebase使ってみた× Firebase Realtime Database使ってみた◯

2017/09/08 09:54
b.hatena.ne.jp

仰る通りなので追記してみます

ユーザー管理、メールアドレスとパスワードとか、Twitter/facebook連携とか、そういうの全部面倒見てくれるのは体験としてすごく良かった。習作アプリは特にSNS連携とかいらなかったので、匿名ログインみたいなやつを使った。ユーザーから情報を得ることなく、端末を識別してくれるので「自分の投稿」をIDで抽出することができたし、トークンの更新みたいなことも全部SDKに丸投げでよかったのでよかった。

総評

お手軽に使えるが、お手軽に使いすぎると容易に死ぬことがわかったので長く保守する大きなアプリをFirebaseだけでやっていくのはなかなか難しいんじゃないかという感じがした。将来Firebaseを剥がすってなったときに死なないようなアプリ側/サービスの設計が必要で、それをちゃんとやろうとするとFirebaseのお手軽さみたいな所をどんどん鈍らせていく感じがしてものを作るというのは難しいなあと思いました。

行動指針

SwiftでiOSアプリを作るにあたっての自分の行動指針をまとめて公開してみた。

というのも、最近転職したり個人でアプリ作ったり友人とのプロジェクトが動き出しそうになったりしており、複数のコーディング規約にまたがってコードを書くことになりそうで(仕事-個人間では既に起きてる)、頭のスイッチコストやそもそもどういうルールだっけみたいなのが問題になりそうなので個人側の規約みたいなものもテキスト化してついでに公開しようと思った次第。

個人プロジェクトは僕一人でやってるのでチームで円滑にやるためのルールみたいなのは考えてないし、そもそも細かい具体的なコーディング規約はないので、迷ったときにはこう判断するのだというのを示す意味で行動指針としてみた。

以下のリポジトリにおいた。

hiragram/my-guidelines

一応思いついただけ普段書いてるコードの規約みたいなのを書き出してみたが言いたいことは冒頭のBasis of thoughtsの部分なのでそこだけ和訳しておいておく。

Humans must not do anything which a compiler can do. A compiler is far more reliable than humans are.

コンパイラができることを人間はやらない。コンパイラは人間よりはるかにずっと信頼できる。

型推論が効くところは型を書かない、省略記法が使える所は省略記法を使う

Do not make extra efforts for tiny readability. The most important thing is that each parson has same understanding, not same appearance of code.

小さな可読性のために余計な手間をかけない。重要なのは誰が読んでも同じ理解をできることで、コードの見た目が同じであることではない。

改行のあるなし、ブレースのあるなし、など。 ifの括弧かかないとかブラケット前後のスペースとかはまあ常識的に。

Take advantage of IDE(Xcode) strongly.

IDE(Xcode)をめちゃ活用する

定義へのジャンプ、型の表示、MARK、など

Do not choose a way that can crash apps on the runtime if there are another options that can prevent crash or detect errors on compilation time.

実行時にクラッシュしうるやり方より、クラッシュしなかったりコンパイル時にエラーチェックできる選択肢があるならそっちを使う。

User Defined Runtime Attributes, Implicitly Unwrapped Optional, Any, など

退職します!

3年2ヶ月在籍したSpeeeを退職します。今日が最終日です。

なにしてたの?

元々サーバーサイドのエンジニアとして新卒入社してPHPやっていて、自社サービスのiOSアプリ用のAPIを作ってました。そのうちアプリ側のちょっとしたデバッグとか修正をXcodeでやるようになり、アプリおもしれーなとなってきたタイミングで社内公用語がPHPからRubyになるとのことで、どうせ未経験から勉強するならRubyよりアプリやりたいなとなり転向させてもらいました。そこからはそのプロジェクトでObjective-Cを書いたり幾つかのプロジェクトでSwiftを書いたりしました。

取り組んだこととしては、

  • Swift1.xから2.0への移行
  • RxSwiftの導入
  • API抽象レイヤーの型々しい設計
  • TravisCI上でCarthageの依存フレームワークをキャッシュしたりCartfile.resolvedの差分をみて更新分だけ再ビルドしたりする仕組みを作って導入
  • typo警察

なんで辞めるの?

アプリチームは半分くらい(以上かな?)が業務委託メンバーで入れ替わりもそこそこあってさみしいなーみたいなことと、Ruby勢のわいわいやっていくぞという雰囲気を羨ましく思っていたことがちょっとモニョっていました。 あと、去年の夏秋くらいにプロジェクトにモニョモニョ〜という感じの状態があったことをきっかけに、漠然とあと1年くらいかな〜くらいな気持ちが湧きました。

あとは直接的な理由があるわけじゃなくて、名前を知ってる会社からオファーを貰って、経営層から現場のエンジニアまで話を聞かせてもらい、いろいろ比較するとこっちのほうがいい!となって辞めることにしたという感じです。9時半に出社しなくていいのと台風の時は自宅作業ができるのすごいという感じ。

次どうするの?

7月から五反田でiOSアプリエンジニアとして働きます。希望すればiOS以外のポジションも挑戦させてもらえそうなので、落ち着いたらKotlinでAndroidをやってみたいと思っています。六本木よりもランチが安いとのことで嬉しいです。かれーの店うどんという気になりすぎる店があるので楽しみです。ホームページがパンクで良かったです。

ほしいものリスト

http://amzn.asia/1otnYYA

AutoLayoutのエラーを紐解くときにやっていること

以前ふとこんなツイートをしたところ意外とLikeされた。

AutoLayoutの制約に矛盾があるときにコンソールにでるメッセージ

f:id:hiragram:20170611004037p:plain

これをみて直さないといけないときに自分がやっていることをまとめておく。

ビューのaccessibilityIdentifierを設定する

コードからならhogeView.accessibilityIdentifier

xib/storyboardならここ。

f:id:hiragram:20170611004958p:plain

すると

f:id:hiragram:20170611005235p:plain

UIView:0xXXXXXXXXXXXXと出てたところがaccessibilityIdentifierに置き換わる。

制約のidentifierを設定する

コードからならconstraint.identifier

xib/storyboardなら制約を選択してここ。

f:id:hiragram:20170611005813p:plain

すると

f:id:hiragram:20170611005858p:plain

アドレスを指定してビューに背景色をつけたりする

コンフリした制約が多すぎてidentifierつけるのがだるいときとかはlldb上でアドレスを指定してUIViewのインスタンスをとってきてbackgroundColorで色を変える。 エラーでる画面のviewDidAppearとかでブレークして、

(lldb) e let $v = unsafeBitCast(0x7fc85dd0a0f0, to: UIView.self)
(lldb) e $v.backgroundColor = UIColor.yellow

でブレークを解除して動かすとアプリ上で色が変わる。隠れてたら階層見るアレで見て。

NSLayoutConstraintのアドレスを渡すと対象のビューに枠線をつけるlldbプラグインを作った

Python初めて書いたので許して

#!/usr/bin/python

import lldb

def process(debugger, command, result, internal_dict):
    lldb.debugger.HandleCommand("""
    expr -l swift --
    func $process(_ address: Int) {
        let constraint = unsafeBitCast(address, to: NSLayoutConstraint.self)
        guard let view1 = constraint.firstItem as? UIView else {
            return
        }
        view1.layer.borderColor = UIColor.red.cgColor
        view1.layer.borderWidth = 2

        guard let view2 = constraint.secondItem as? UIView else {
            return
        }
        view2.layer.borderColor = UIColor.blue.cgColor
        view2.layer.borderWidth = 2
    }
    """.strip())
    lldb.debugger.HandleCommand('expr -l swift -- $process('+command+')')

def __lldb_init_module(debugger, internal_dict):
    debugger.HandleCommand("command script add -f visualizeAutoLayoutTargets.process visualizeAutoLayoutTargets")
    print "enabled visualizeAutoLayoutTargets command."

導入とか使い方はこの辺を参考にしたのでそっちを見てください

iOSエンジニアのための LLDB Plugin 入門

使った感じはこんな感じ

これは記事頭のツイートがちょっと盛り上がってからふーんと思って作ったものなので実務から生まれたものではないです。使いづらいとか知らない。

おわり

Mastodon用のiOSクライアントアプリを作っている

Mastodon、どうせ1ヶ月で飽きるっしょという思いがあったのでユーザーとしては全然使っていなかったが、流行りモノのサービスでアプリ開発競争みたいになってるのには参加してみたいという思いがあったので作っている。

f:id:hiragram:20170508044227g:plain

開発に使っているアカウントはPawooにあり、一旦はPawoo用クライアントという位置づけで出すつもり。

半年先には誰もMastodonの話してないでしょという思いは相変わらず持っているので、一旦リリースして飽きたらTwitterクライアントに転生させると思う。

自分用 けっこう体調崩したとき用

コカ・コーラ アクエリアス 1.0L×12本

コカ・コーラ アクエリアス 1.0L×12本

ウイダーinゼリー エネルギー マスカット味 180g×6個

ウイダーinゼリー エネルギー マスカット味 180g×6個

パーフェクトプラス 即攻元気ゼリー 180g×6個

パーフェクトプラス 即攻元気ゼリー 180g×6個

熱さまシート 冷却シート 大人用 12枚

熱さまシート 冷却シート 大人用 12枚

Segueを使わずにかっこよく画面遷移する方法を考えた

最近Segueをいかに安全に使って画面遷移するかということを考えていたけど、#swtwsを見ていてそもそもSegueを使わない/嫌いという人が結構いるんだなと思ったのでSegueを使わないで楽に安全に画面遷移する方法を考えてみたら意外といい感じになった。

Segueの危うい所

  • コード内ではSegueのidentifierを単なる文字列として扱う所
performSegue(withIdentifier: "toDetailVC", sender: nil)

だからSegueの名前が変わった時に直し忘れていてもコンパイル時にチェックできない。

  • 遷移先のViewControllerをいじるのはいわゆるprepareForSegueメソッドの中なので、複数の遷移に関する処理が一つのメソッドにまとめられてしまう所
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  switch segue.identifier {
  case "toDetailVC":
    // DetailVCの準備
  case "toOtherVC":
    // OtherVCの準備
  default:
    break
  }
}
  • prepareForSegueの中で遷移先の型でVCを取得するにはダウンキャストが必要な所
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  switch segue.identifier {
  case "toDetailVC":
    // segue.destinationの型はUIViewController
    let vc = segue.destination as! DetailViewController
    vc.hoge = "hogehoge"
  default:
    break
  }
}

identifierと同じく行き先の型が正しいかどうかもコンパイル時のチェックができない。

これくらいかと思う。

Segueを使わずに画面遷移する

本題。

Segueを使わずに画面遷移をするということは、どうにかして遷移先のVCのインスタンスを作って、presentなりshowなりで表示するということだろう。

VCのインスタンスをどこから作るかというと、UIStoryboardのinstantiateInitialViewController()instantiateViewController(withIdentifier:_)を使うことが多いはずだ。

コードに起こすとこんな感じだろうか。

@IBAction func buttonTapped() {
  let storyboard = UIStoryboard.init(name: "DetailViewController", bundle: nil)
  let vc = storyboard.instantiateInitialViewController() as! DetailViewController
  vc.hoge = "hogehoge"
  show(vc, sender: nil)
}

確かにSegueのidentifierからは脱却したが、Storyboardの名前はまだ文字列で指定しているし、ダウンキャストもある。こんなコードがいろんなVCのいろんなところに散らばっているのは、相変わらず治安が悪いと言えるだろう。

こんな方法ならどうですか

こんな感じのコードで気軽に画面遷移できたらよくないですか。

@IBAction func buttonTapped() {
  show(DetailViewController.self, sender: nil) { (vc) in
    // ここでのvcはUIViewControllerではなくDetailViewControllerのインスタンス
    vc.hoge = "hogehoge"
  }
}

これは以下の仕組みによって実現できる。

まず、UIViewControllerのサブクラスに対して、「このVCはStoryboardからインスタンスを作ることができる」ということを示すプロトコルを定義する。

public protocol StoryboardInstantitable {}

// StoryboardInstantiatableなUIViewControllerのサブクラスで利用可能になるもの
public extension StoryboardInstantitable where Self: UIViewController {
  static var storyboardName: String {
    // UIStoryboard.init(name:_, bundle:_)に渡すname (*1)
    return String.init(describing: Self.self)
  }

  static var storyboard: UIStoryboard {
    return UIStoryboard.init(name: storyboardName, bundle: nil)
  }

  static func instantiateFromStoryboard() -> Self {
    // storyboardからインスタンスを生成して自分自身の型にキャストして返す (*2)
    return storyboard.instantiateInitialViewController() as! Self
  }
}

これによって、以下のコードでキャスト済のVCのインスタンスを得られるようになった。

let vc = DetailViewController.instantiateFromStoryboard()

次に、UIViewControllerにこのようなextensionを追加する。

public extension UIViewController {
  public func show<T: StoryboardInstantitable>(_ vcType: T.Type, sender: Any?, configuration: ((T) -> Void)?) where T: UIViewController {
    // 中身はのちほど...
  }
}

シグネチャが長くなってしまったが、重要なのはvcType: T.Typeconfiguration: ((T) -> Void)である。 このメソッドは型パラメータTを持ち、このTはStoryboardInstantiatableプロトコルを実装し(<T: StoryboardInstantiatable>)、且つUIViewControllerあるいはそのサブクラスである(where T: UIViewController)という制約がかかっている。

vcType: T.Typeは遷移先のVCの型である。 configuration: ((T) -> Void)は遷移先のVCのインスタンスを引数にとるクロージャで、この中で遷移先VCに値を渡したり、プロパティを変更したりすることを想定している。

このメソッドの具体的な実装は以下のようになる。数字の順番が逆転しているが、数字の順に説明する。

public func show<T: StoryboardInstantitable>(_ vcType: T.Type, sender: Any?, configuration: ((T) -> Void)?) where T: UIViewController {
  let vc = T.instantiateFromStoryboard() // ①
  _ = vc.view // ③
  configuration?(vc) // ②
  show(vc, sender: sender)
}

まず、①の行では先程定義したStoryboardInstantiatableのinstantiateFromStoryboardメソッドを使い、Storyboardから遷移先VCのインスタンスを得る。

次に、②の行で呼び出し元から受け取ったconfigurationブロックを使ってVCのインスタンスを設定する。

但し、この時点ではまだIBOutletなプロパティが初期化されておらず(=nil)、例えばconfigurationブロックの中でIBOutletでStoryboardに接続されたUILabelのテキストを設定しようとすると暗黙アンラップによってアプリがクラッシュする。
これはUIViewControllerのviewが遅延ロードされるためで、一度読み出してやればサブビューも含めて初期化される(loadViewviewDidLoadも呼ばれる)。その為の③の行である。 configurationブロックを用いてVCの設定をしたら、あとは従来あるUIViewControllerのshowメソッドで画面遷移を実行するだけだ。

これで、先程示した

show(DetailViewController.self, sender: nil) { (vc) in
  // ここでのvcはUIViewControllerではなくDetailViewControllerのインスタンス
  vc.hoge = "hogehoge"
}

// (trailing closureを使わなければ)
show(DetailViewController.self, sender: nil, configure: { (vc) in
  // ここでのvcはUIViewControllerではなくDetailViewControllerのインスタンス
  vc.hoge = "hogehoge"
})

という呼び出し方ができるようになった。

導入のしやすさ

一番簡単にこの仕組みに乗っかるなら、VCのクラス名 = storyboardの名前 且つ そのVCがInitial View Controllerにすれば遷移先のVCのクラスにStoryboardInstantiatableプロトコルへの適合を記述するだけでよい(追加でメソッドを生やしたりしなくてよい)。

もう既に開発が進行しているアプリで、例えばクラス名=storyboard名になっていなくて、わざわざリネームするのが面倒なときは

extension DetailViewController: StoryboardInstantiatable {
  static let storyboardName = "SomeOtherName"
}

とデフォルト実装を上書いてやるだけでいいし、Initial View Controllerでないときも同様に

extension DetailViewController: StoryboardInstantiatable {
  static func instantiateFromStoryboard() -> DetailViewController {
    return storyboard.instantiateViewController(withIdentifier: "IdentifierForThisClass") as! DetailViewController
  }
}

としてやるだけでよい。

ごちゃごちゃコードがあってわからん

そうおっしゃると思って、GitHubにサンプルつきで上げておきました。cloneして触ってみてください。

github.com

注意

記事中のSwiftのコードは本文を書きながらそらで打ったものもあるのでコピペしてもコンパイル出来ないかもしれません。だからやっぱりサンプルを触ってみてください。