読者です 読者をやめる 読者になる 読者になる

hiragram no blog

iOSとか

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

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

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

registという単語が含まれていたらビルドエラーにするやつ作った

iOS その3 Advent Calendarの9日目の記事です。

最近、NGワード一覧みたいなのを定義しておいたらコンパイル時にそれがコード中に含まれていないかチェックして含まれてたらビルド落ちるように出来ないかな〜とか思って試してみた。

XcodeのBuild phaseの中でSwiftLintを使うと、warning/errorのある行をXcodeが黄色とか赤にハイライトしてくれるやつ、どうやってるのかなーと思っていろいろ見てみると

/path/to/file:line:何文字目か(?): error: ほげほげ

このフォーマットでエラー出力にprintして、非0なexit statusを返せばXcodeがハイライトしてくれるらしいということがわかった。

"register"を"regist"と間違えて書いてある奴を許せないので、registという単語を使っている行をビルドエラーにするツールをPerlでさくっと書いた。

#!/usr/bin/perl

use strict;
use warnings;

my $current = `pwd`;
chomp($current);
my $root = ".";

my $filelist = &get_files($root);

my $status = 0;
foreach my $file (@$filelist) {
  my @lines = split("\n", `cat $file`);
  my $i = 1;
  foreach my $line (@lines) {
    if ($line =~ /[rR]egist[^e]/) {
      print STDERR "$current/$file:$i:1: error: Registという英単語はない💢😡\n";
      $status = 2;
    }
    $i++;
  }
}

exit($status);

sub get_files {
  my $dir = shift;
  my $filelist = shift;
  opendir(DIR, "$dir");
  my @list = grep /^[^\.]/, readdir DIR;
  foreach my $file (@list) {
    if (-d "$dir/$file") {
      $filelist = &get_files("$dir/$file", $filelist);
    } else {
      if ($file =~ /.swift$/) {
        push @$filelist, "$dir/$file";
      }
    }
  }
  return $filelist;
}

XcodeのRun script phaseに

perl regist.pl

と追加してやると、コード中にあるregistの行で

f:id:hiragram:20161209010504p:plain

エラーとしてハイライトしてくれるようになった。

今後はもっと汎用的なNG単語フィルターツールとしてもうちょっと拡張したい。

iOSアドベントカレンダーなのにPerlしか書いて無くてすみませんでした。

アクセス修飾子周りでSwiftのバグっぽいのみつけた

Swiftのバージョンは3.0

ViewController.swift

import UIKit

class ViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()

    print(TypeA.name)
  }

}

ProtocolA.swift

import Foundation

private protocol ProtocolA {

}

extension ProtocolA {
  static var name: String {
    return "aaa"
  }
}

struct TypeA: ProtocolA {

}

でビルドしようとするとこんなエラーが出る

Undefined symbols for architecture arm64:
  "protocol witness table for BugTestProj.TypeA : BugTestProj.(ProtocolA in _ED3794DE2981E83EA71158CCFF2975C6) in BugTestProj", referenced from:
      BugTestProj.ViewController.viewDidLoad () -> () in ViewController.o
  "static (extension in BugTestProj):BugTestProj.(ProtocolA in _ED3794DE2981E83EA71158CCFF2975C6).name.getter : Swift.String", referenced from:
      BugTestProj.ViewController.viewDidLoad () -> () in ViewController.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

ProtocolAはprivateなので、そこに定義されてるメソッド/プロパティには外からアクセスできないんじゃないかなーと思うんだけどどうなんだろう。

バグレポートは送った。