hiragram no blog

iOSとか

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

クソアプリ開発は目的ではなく手段

クソアプリ Advent Calendarの16日目です。

先日個人で作ったSushiCameraというアプリをリリースした。

hiragram.hatenablog.jp

個人で作ったアプリがストアリリースまで行ったのは初めてだったので、やる前の気持ちや今の気持ちを雑に書く。


クソアプリ作るぞ〜という気持ちがあって作ったわけではなくて、「カメラ」「画像加工」あたりの技術に触りたかったのでそのために雑に作ったという感じで、大衆にウケようとか収益化しようみたいなことは一切考えてなかった。

もともと今まで関わってた(or関わってる)アプリがステルスリリースだったりファーストリリース前だったりして「私の作品はこれです」といえるものが無かったのもあって、せっかく作るなら他者から見える形で、1つのプロダクトとして最低限の体だけは整えようと思っていた。テーマにした「カメラ」「画像加工」以外の例えばUIとかシェア機能とかそういうのは本当に適当に作ったり全く作らなかったりした。

ある程度作っていろいろ好きに作り込んでみて、満足したら後は適当に整えてストアに申請。何度かリジェクトされたけどなんだかんだ通った。

自分のプロフィール(Wantedlyとか)にストアのURLを載せられるというのはなかなか気分がいい。ストアに出してからはもうコードをいじるモチベーションもそんなになくなってたのでほったらかしていたら、「SushiCameraをみてご連絡しました」という企業からメッセージが来たりした。

のちのち転職とか考えるときに効いてくればいいやーくらいに思ってはいたので、いきなりそういう連絡が来てマジかとなった。たまたまどこかで目に触れて連絡してみようとなったんだとしたら、かなり運が良かったと思う。正直これが自分のなかで成功体験としてかなり強く印象に残っていて、クソでもアプリとして出すことに意味はあるなあとより思うようになった。

自分のスキル/経験を端的に示す意味でも、その技術を扱ったシンプルなアプリを作って置いておくのはポートフォリオ的に使えてよい。それはアプリとしてはクソアプリの域を出なかったとしても、自分のアピールには十分つかえるだろう。

SushiCameraリリース以前は休日にずっとコードを書くみたいなことはあんまりなくて、せいぜい小さいスクリプトを書いて遊んだり、新しいライブラリをちょっと触ってみたりぐらいだったが、最近は結構プロダクトを作るためのコードを書く時間が増えたと思う。一昨日から新しい動画カメラアプリを作り始めた。これは習作というよりはちゃんと作り込んでみたい系のアプリだけど。

僕はクソアプリを作ったぞ〜わいわい〜みたいな楽しみ方は当人としても傍から見てもあんまりおもしろいと思っていないが、客観的にクソに見えるアプリでも、そこに自分にとって有意義なチャレンジとかアウトプットとしての意義がちゃんとあれば、どんどん作って出せばいいのではという視点を得られたよい経験だった。

アプリをつくる、サービスをつくる以外にコードのアウトプットとしてOSSなんかがあげられるけど、これは僕は過去に何度か挑戦しつつもコードを書く、技術力を身につける、ということ以外に考えなきゃいけないことが多くてなかなか大変だなという感じで何度も投げ出している。この辺は向き不向きがあると思うので、僕は今後も習作クソアプリとたまにちゃんとしたアプリをがんばってつくっていく感じの努力を続けていきたいと思う。

結局言いたいことは、クソアプリを出すことは自分の技術と経験をきちんと見える形で残しておくための手段であるということである。