yashiganiの英傑になるまで死ねない日記

週末はマスターバイクでハイラルを走り回ります

UITableViewでレイアウトの違うセルをどう扱うか #関モバ

毎月恒例の関モバ #8に参加しました.

kanmoba.connpass.com

今回は,UITableView複数のレイアウトを持つセルをどう扱うかというトークをしました.

speakerdeck.com

骨子としては,UITableView複数のレイアウトを持つセルを実装するとき,いくつかの方法がありますがコードでレイアウトをいじるとAuto Layoutの指定が複雑になるので,レイアウトのパターンごとにxibを分けるといいというものです.

複数のxibを使うことについて少し補足

UITableViewにおいてxibを増やす場合,xibの登録やセルのリユースなどセルを使うための決め事が複雑になることが問題になります. セルを複数のView Controllerで使いまわしたい場合,それぞれで同じ実装をすることが強いられます. そこで,このトークでは実装パターンとしてEntryCellRepresentableのようなprotocolを使うことを紹介しました. 紹介したものは単純で以下のような実装です. ※ この例ではEntryCellに対して複数のxib(EntryCell.xibとEntryCellWithImage.xibの2つ)が存在し,それぞれでレイアウトを定義することを前提としています(画像があるパターンと無いパターンです).

protocol EntryCellRepresentable {
  var tableView: UITableView! { get }
  func registerEntryCell()
  func dequeueEntryCell(entry: Entry, forIndexPath indexPath: NSIndexPath) -> EntryCell
}

extension EntryCellRepresentable {
  func registerEntryCell() {
    ["EntryCell", "EntryCellWithImage"].forEach {
      tableView.registerNib(UINib(nibName: $0, bundle: nil), forCellReuseIdentifier: $0)
    }
  }

  func dequeueEntryCell(entry: Entry, forIndexPath indexPath: NSIndexPath) -> EntryCell {
    let reuseIdentifier = entry.hasImage ? "EntryCellWithImage" : "EntryCell"
    let cell = tableView.dequeueCellReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! EntryCell
    cell.entry = entry
    return cell
  }
}

EntryCellRepresentableEntryCellを使ううえでの規約(全てのパターンのxibを漏れなくregisterNibする,dequeueCellReusableCellWithIdentifierし,モデルであるEntryをセットする)を隠蔽します. UITableViewを使うとき,多くはUITableViewControllerのサブクラスを使うことでしょう. UITableViewControllerはプロパティにvar tableView: UITableView!を持っていますので,EntryCellRepresentableを指定するだけでprotocolに適合させることができます. したがって,EntryCellを表示するclass EntriesViewController: UITableViewControllerはこのように実装できます.

class EntriesViewController: UITableViewController, EntryCellRepresentable {
  override func viewDidLoad() {
    super.viewDidLoad()
    registerEntryCell()
  }

  override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let entry = entries[indexPath.row]
    return dequeueEntryCell(entry, forIndexPath: indexPath)
  }
}

UITableViewを自分で追加する場合はvar tableView: UITableView!を実装するだけです. このような工夫によって,使いまわしやすさを維持しつつxibを分けることが可能です.

unwrapして値を棄てるときを少し小洒落たかんじに

たまにunwrapするけど,必要なのはその有無でそのままそれを棄てることがあります. こういうときです.

if let a = some {
    print("some is .Some")
}

このままだとXcodeに,aを使ってないから_に変えろって怒られたりしてゲンナリ………オシャレ感ゼロです. そんなとき,パターンマッチを使うとちょっと小洒落たかんじになります.

if case .Some = some {
    print("some is .Some")
}

同様に,nilかどうかを確かめたいときもsome == nilよりパターンマッチを使うほうが小洒落てます.

if case .None = some {
    print("some is .None")
}

最近Swiftを始めた同僚がif caseなんてあるんですねって言ってたときに,「あんま使わねーな」とおもってたんですけど,それから意識してみると小洒落たコードが書けました.

追記

次の日また別の同僚と話していたら,「SwiftってOptionalの存在を極力隠しているからnilとの比較のほうがよくない?」って言われました. たしかに,Optionalの正体を知っていると別になんともないコードなんですが,そうでないと読み解くのが難しいかもしれません. 別段パターンマッチが必要な場面ではありませんしnilとの比較のほうが凡庸なので,普段はそっちを使ったほうがいいなとおもいました. オシャクソ野郎は使え. こちらからは以上です.

ぼくのApple Special Event

まとめ

  • iPad Proはでかすぎる
  • iPhone 6sを買う
  • 今回は2時間くらいで終わったけどねむいものはねむい

顧客の欲しかったもの

関西モバイルアプリ研究会で「Swiftで自然数を作るっ」という発表をしました #関モバ

Fondue enchaînéewww.flickr.com

毎月おなじみの関西モバイルアプリ研究会で「Swift自然数を作るっ」という発表でライブコーディングをしました.

kanmoba.connpass.com

書いたコードはgistで公開しています(少し長いので最後に埋め込んであります).

自然数についてはWikipediaの項目を参照してください.

自然数 - Wikipedia

コードだけだとわかりにくいとおもうので少し解説を書きます.

解説コーナー

自然数のデータ構造を作る

例えば,自然数3は以下のように表現できます.

suc(suc(suc(0)))

要は0からはじめて3回後に進んだ状態です. この構造をSwiftで表現すると,

enum N {
    case Zero
    indirect case Succ(N)
    
    static func succ(n: N) -> N {
        return .Succ(n)
    }
}

となります. Swift2で新たに追加されたindirectによって再帰的なenumが作れるようになったので,このような表現が可能です. ちなみに,.Sucでなく.Succにしているのは.Zeroと揃えるためです.

static func succ(n: N) -> N はある自然数の次の自然数を得るための関数です. mutatingは絶対書きたくないので普通の関数にしています.

.Succ(.Succ(.Succ(...))) のようにネストしまくればどんな数でも作れるようになりましたが,さすがに10みたいな値を作るのは難しいのでヘルパ用意します.

extension N {
    init(from: UInt) {
        self = Repeat(count: Int(from), repeatedValue: N.succ).reduce(.Zero) { $1($0) }
    }
}

負の値が入力されるとinit?にする必要があって面倒なので,ズルしてfrom: UIntにします. fromの回数ぶん.Succで囲えばいいので,長さfromで中身が全部N.succの配列を用意して,.Zeroに当てはめます. 要はこれと同じです.

var n: N = .Zero
for (var i = 0; i < from; i++) {
  n = N.succ(n)
}
self = n

同値,順序

これだけだと正しく実装できているのか証明できないので同値と順序を実装します. SwiftではEquatableComparableプロトコルがそれに当たりますね. 最低Equatableではfunc ==(_:_:) -> Boolを,Comparableではfunc <(_:_:) -> Boolを実装する必要があります. (これだけで==!=<<=>>=が使えるようになります)

extension N: Equatable, Comparable {}

func ==(lhs: N, rhs: N) -> Bool {
    switch (lhs, rhs) {
    case (.Zero, .Zero): return true
    case let (.Succ(a), .Succ(b)): return a == b
    default: return false
    }
}

func <(lhs: N, rhs: N) -> Bool {
    switch (lhs, rhs) {
    case (.Zero, .Succ(_)): return true
    case let (.Succ(a), .Succ(b)): return a < b
    default: return false
    }
}

==は両辺共に.Zeroなら真,共に.Succなら中の値で再帰,それ以外は偽です. <も同様で,左辺が.Zeroで右辺が.Succなら真,両辺共に.Succなら中の値で再帰,それ以外は偽です. 単純なパターンマッチで実装できますね.

加法・乗法

定義に従って,

func +(lhs: N, rhs: N) -> N {
    switch (lhs, rhs) {
    case (let a, .Zero): return a
    case let (a, .Succ(b)): return .Succ(a + b)
    }
}

func *(lhs: N, rhs: N) -> N {
    switch (lhs, rhs) {
    case (_, .Zero): return .Zero
    case let (a, .Succ(b)): return a * b + a
    }
}

ここも単純なパターンマッチで実装できます. 定義をすごく綺麗に実装に落とし込めるので非常に気持ちがいいです.

おまけ

playgroundなどの出力がこのようになって少し見にくいです.

let two: N = .Succ(.Succ(.Zero)))  // => Succ(N.Succ(N.Zero)

CustomLiteralConvertibleを実装すると整形できます.

extension N: CustomStringConvertible {
    var description: String {
        func toInt(n: N) -> Int {
            switch n {
            case .Zero: return 0
            case let .Succ(a): return 1 + toInt(a)
            }
        }
        return "\(toInt(self))"
    }
}

反省

LTなのに15分かかった.

まとめ

enumで簡単に再帰的データ構造が書けるようになって最高ですね!

gist.github.com

Lazy stored propertyについて発表しました #関モバ

この間の関西モバイルアプリ研究会#4でLazy stored propertyについて発表しました.

speakerdeck.com

Lazy stored propertyについて大まかな使いドコロと,テスタブルな設計をする際にこいつを使うと依存の注入に使いやすいのではないか,というアイディアについて話しました. 簡単にまとめると,Lazy stored propertyを使えば,初めてアクセスするときまでインスタンスの生成が遅延されるので,それまでにテストの準備ができるよねということです. lazy は最もすきなキーワードなのでめっちゃオススメです!

追記

Twitterで@さんと@さんにimplicitly unwrapped optionalなpropertyをlazyにしたときにnilを代入することで再度初期化処理が走ることを教えていただきました. こういうかんじです.

gist.github.com

optionalの場合は再度初期化されることはありません. ドキュメントにもこういう動作するという記載は特に見つからないので,実装の都合で動作を騙せるのかもしれません.