1
/
5

Swift時代に悩ましいUIViewControllerをどう扱うか

こんにちは、WantedlyでiOSのエンジニアの杉上です。先日Twitter上で勉強会を行うという斬新な試みにお声がけいただき、Swift Tweets Tweetupに登壇させていただきました。このときに投稿した内容をご紹介します。

リンク

Tweetupとは


登壇者の皆様

メインスピーカー

LTスピーカー

登壇内容(登壇順)

登壇内容

Twitterによる投稿をブログ形式にリライトしてお届けします。

Swift Tweetsオーディエンスの皆様こんばんは!Tweetupという新しい試みに参加させていただきとてもワクワクしています。本日は「Swift時代に悩ましいUIViewControllerをどう扱うか」についてご紹介させていただきます。よろしくお願いします。

まずは自己紹介から。杉上洋平と申します。iOSの開発は日本でiPhoneが販売されたときに、嫁が早速手に入れてアプリがないので作ってほしいと言われたのをきっかけに、2008年からアプリを作り続けています。

AppStoreでのアプリの自主販売での生計を皮切りにフリーランスを経て現在はWantedlyでアプリの開発を行っています。WantedlyではSwiftが1.0Betaのころから3本の新規アプリを開発しSwift言語の成長とエコシステムの発展と共に歩んできました。

アプリの開発にあたりOSSやコミュニティの皆さんに助けられて進めることができました。そのため開発で得た知見は Pay it Forwardの精神のもとQiita( http://qiita.com/susieyy )に記事を記載させていただいております。

本発表では去年11月にリリースしたWantedlyPeopleというアプリの開発において、Swiftの特性を活用しつつUIViewController(以下VC)をどのように向き合うか試行錯誤した内容をご紹介します。

この扱い方が一般的により優れているということではなく、アプリの要件、開発規模、チームメンバー構成、設計方針により適応できるかどうかはケース・バイ・ケースになると思うので、開発の選択肢の1つとして捉えていただけると幸いです。コードはSwift2.3になります。

アプリ開発においてVCは画面のライフルサイクルと遷移を担い、AppleMVCパターンでアプリを構築する上で中心的な存在となっています。その上でVCは開発者にとって重量であると同時に悩ましい問題をたくさん抱える課題の宝庫でもあります。

代表的な以下の課題を含め多様な視点でVCと試行錯誤しました。

  • SwiftとStoryBoardの相性が悪い
  • VCの肥大化問題と抽象化・共通化をどのように行うか
  • 画面間(VC)を疎結合に扱いたい
  • Depplinkで任意画面へのダイレクトな遷移を行いたい


iOSの開発ではStoryBoardを活用して画面の構成を構築する方が多いのではないでしょうか。StoryBoardは直感的な操作でエンジニア、デザイナー共に扱いやすくビューの配置やレイアウトを視覚的に配置・把握が行える素晴らしいツールです。

AutoLayoutの設定ではリアルタイムで整合性を検証してくれるためあるべき正しい設定へと導いてくれます。また画面間の遷移も設定が行え画面遷移図としての仕様としても重宝します。そんな素敵なStoryBoardですが万能ではありません。使いづらい点を見てみましょう。

StoryBoardとSegueによる画面遷移を行うコードです。画面遷移決定時に遷移先の画面へ渡したい引数が確定することが都度だと思いますが引数の設定はprepareメソッド内で行うため、渡したい引数はsenderというAny?型で引き回す必要があります。

final class FirstViewController: UIViewController {
    func segueToSecondViewController() {
        let sender = "This is a second view!"
        performSegue(withIdentifier: "SecondViewIdentifier", sender: sender)
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "SecondViewIdentifier" {
            let secondViewController = segue.destination as! SecondViewController
            secondViewController.titleName = sender as! String
        }
    }
}

このときいくつかの悩ましい問題に直面します。

  • sender引数を利用すると遷移先の画面へ渡したい引数の型が失われてしまう、引数が複数の場合Dictionary型として扱う
  • prepareメソッドで責務が集約されているように見えるけどなんとなく冗長
  • Identifierは文字列型を取るためコンパイラチェックできない
  • SecondViewControllerのinitをコードから呼び出すことはできない、そのため初期化後のインスタンスのプロパティへ代入することで代替する必要がある

また画面遷移先であるSecondViewControllerのプロパティtitleNameは、初期値が不変でイミュータブルな場合でもシンプルにlet定数で定義できずForceUnwrapか再代入が不要でもvar変数として定義する必要があります。

final class SecondViewController: UIViewContoller {
  // ForceUnwrapで定義する必要がある
  let titleName: String!

  // OR

  // 再代入が可能になってしまうのでプログラマーが保証する必要がある
  var titleName: String = ""

  // OR

  // 都度OptionalBindingなどでnilチェックが必要になる
  var titleName: String?

  override func viewDidLoad() {
    super.viewDidLoad()
    // プログラマーはviewDidLoadよりも前にどこかでtitleNameが初期化されることを期待する
    title = titleName
  }
}

イミュータブルであることは保証したいのでForceUnwrapを記述してお茶を濁すケースが多いのではないでしょうか。ただしForceUnwrapで定義したlet定数はviewDidloadなどで呼び出される前に事前に初期化しておく必要があります。

そのため以下のようなコードではクラッシュをしてしまいます。この点はコンパイル時に抑止できないので、プログラマーがしっかり安全性を担保する必要があります。

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  if segue.identifier == "SecondViewIdentifier" {
    let secondViewController = segue.destination as! SecondViewController
    // titleNameが初期化されていないため以下行によるviewDidLoadの時点でクラッシュ!
    secondViewController.view.addSubView(UILbael())
    secondViewController.titleName = "This is a second view!"
  }
}

なんとStoryBoardはSwiftと相性が悪いことでしょう。この嘆かわしい状況に1点の光を指し示す発表が昨年のTry!Swiftでありました。「実践的 Boundaries」の中で紹介されたImmutable Coreの考え方です。

初期値をStoryBoardを利用せずVCのinitを活用してイミュータブルなlet定数として定義します。インスタンスの初期化時に初期値をイミュータブルに定義するSwiftの考え方をVCにも適用するのです。

https://realm.io/jp/news/tryswift-ayaka-nonaka-boundaries-in-practice

実践的 Boundaries
Gary BernhardtのBoundariesはお気に入りの講演のひとつです。Swiftにおけるファンクショナルプログラミングの講演を見ていれば聞いたことがあるでしょう。はじめは理論を理解できてもコンセプトが理解できませんでした。Swiftを書いていくうちに、ファンクシ...
https://realm.io/jp/news/tryswift-ayaka-nonaka-boundaries-in-practice

StoryBoardを利用せずVCのinitを活用したコードは以下のようになります。required initの記述など冗長な点はありますが我慢できる範囲内です。

final class FirstViewController: UIViewController {
  func presentSecondViewController() {
    let vc = SecondViewController(titleName: "This is a second view!")
    presentViewController(vc, animated: true, completion: nil)
  }
}

final class SecondViewController: UIViewContoller {
  let titleName: String

  init(titleName: String) {
    self.titleName = titleName
    super.init(nibName: nil, bundle: nil)
  }

  required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }

  override func viewDidLoad() {
    super.viewDidLoad()
    title = titleName
  }
}

僕は思いました。そうだStoryBoardを捨てよう。。。よりSwiftらしくVCと向き合うために。型安全でイミュータブルで、プログラマーによる担保ではなく、コンパイラによる不整合の発生しないことが保証される世界のために。

ただし、この選択は上述のメリットと、裏腹にStoryBoardを利用できないというトレードオフによるデメリットの両方を内包します。以降ではデメリット部分とその対策について見ていきます。

StoryBoardを活用しないとアプリの画面遷移がわかりにくくなるという課題があります。StoryBoardがないために途中参加のメンバーがある画面がどのVCに紐づくのかわからないため、ログを出してひとつづつ確認していったという話も聞いたことがあります。

StoryBoardはそれ自体が実装なので画面遷移図として見たときも図(仕様書)と挙動が乖離しないメリットがありますが、ここではStoryBoardを画面遷移図とする代替手段として画面遷移図を把握するための2つのツールをご紹介します。

1つ目はGoodpatchのProttというプロトタイプツールです。iPhoneから利用し画面の一部をタップすると指定した画面へ遷移する動きを実際のアプリさながらに確認できます。この遷移の設定を行うと自動的に各画面が重ならないような画面遷移図を自動生成してくれます。

初期開発から導入しておけば、デザイン ➜ プロトタイプ/画面遷移図 ➜ iOS実装の流れで開発が進められるので大変重宝します。また遷移の変更などメンテナンスもしやすくワイヤーの画面遷移図よりも視認性も高くいので素晴らしい仕様書となります。

http://qiita.com/hirokidaichi/items/ff54a968bdd7bcc50d42

2つ目は37signalsで紹介されているUI Flowです。UI Flowの記述はテキストで行えるguiflowを利用しています。新規登録ログインなど条件により複雑に遷移先が変わる遷移部分について活用しています。

画面の遷移はStoryBoardのSegueや画面遷移図の遷移順だけではなく、アプリ外でのユニバーサルリンクやプッシュタップによって任意画面ダイレクトに遷移したい場合もあるでしょう。さらにその画面への遷移に伴う一連の遷移スタックも保持したい場合もあるかもしれません。

このような場合にSegueベースの遷移ではスタックを保持しつつの遷移は扱いにくいのですが、VCのinitで初期化するコードではシンプルなので比較的扱いやすいです。

extension UIApplication {
  // windowへの参照はkeyWindow経由だと状況により返るwindowのインスタンスが異なることがあるので
  // 一意に決定できるAppDelegate#windowを参照するほうが無難です
  var appDelegate: AppDelegate { return (UIApplication.sharedApplication().delegate as? AppDelegate)! }
}

// UnivasalLinkやPushのハンドリングメソッドに以下を記述します
// rootViewControllerがUINavigationControllerであることを前提にしています
let navi = UIApplication.sharedApplication().appDelegate.window?.rootViewController as! UINavigationController

let profileVC = ProfileViewController(id: userId)
navi.pushViewController(profileVC, animated: fasle)

let photosVC = PhotosViewController(id: userId)
profileVC.presentViewController(photosVC, animated: fasle)

先程のコードは一見うまく動作しそうに見えますが実はバグを孕んでいます。rootViewControllerのナビゲーションが起点のVCの表示ではなく、すでにある画面へ遷移していたりあるモーダルの表示がある場合は遷移スタックに不整合が発生します。

しかもモーダルの表示はVCに限らずUIAlertControllerによるアラートやアクションシートも含まれます。そのため再帰的にすべてのモーダルをdismissしたり、すべてのナビゲーションをポップしたりする必要があるのですが漏れの危険もあり骨が折れます。

そのため起点のVCを作り直してしまうことで現状の遷移状態を破棄し1から画面と遷移を構築します。このような処理が頻繁に発生する場合はVCのメモリリークがないかチェックもあわせて行います。

guard let window = UIApplication.sharedApplication().appDelegate.window, rootViewController = window.rootViewController else { return }
let newRootViewController = UINavigationController(rootViewController: vc)

// rootViewControllerを差し替える場合は、メモリリーク防止のため事前にdismissを行います
// https://gist.github.com/mono0926/abd7d079361f36efdd9068b27b41ea50
UIView.transitionWithView(window, duration: 0.2, options: .CurveEaseInOut, animations: {
rootViewController.dismissdismissViewControllerAnimated(false, completion: nil)
window.rootViewController = newRootViewController
}) { _ in }

// rootViewControllerを差し替えず、ChildViewControllerの差し替えで行う方法もあります
// refs. http://dealforest.hatenablog.com/entry/2016/12/14/124555
UIView.transitionWithView(window, duration: 0.2, options: .CurveEaseInOut, animations: {
rootViewController.childViewControllers.forEach {
$0.view.removeFromSuperview()
$0.removeFromParentViewController()
}
rootViewController.view.addSubView(newRootViewController.view)
rootViewController.addChildViewController(newRootViewController)
newRootViewController.didMoveToParentViewController(rootViewController)
}) { _ in }

上記のような起点からVCを作りなおす実装を行うに限らず、すべてのVCがしっかりdeinitが呼ばれているかを確認することはとても大事なことです。普段から画面を閉じたり、戻った場合はVCのdeinitが呼ばれているかログなどでこまめに確認するのがよいでしょう。

final class FirstViewController: UIViewController {
  deinit{
    logger.inifo("deinit \(self)")
  }
}

ダイレクトな遷移先が1つだとシンプルで良いのですが、多義に渡る遷移パターンがある場合は、ルーティングを担うロジックを導入することを検討すると良いでしょう。ライブラリがいくつかありますが、Swiftはenumが強力なので自前実装でも十分対応可能です。

enum RouteType {
    case notMatch
    case profile(userId: Int)
    case settings
    case http(url: NSURL)

    init(URL url: NSURL) {
        guard let scheme = url.scheme else { self = .notMatch; return }
        switch scheme {
        case "http", "https":
            self = .http(url: url)
        case Global.appScheme:
            self = self.dynamicType.appScheme(url)
        default:
            self = .notMatch
        }
    }
    private static func appScheme(url: NSURL) -> RouteType {
        guard let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false)
            , let host = components.host
            , let paths = url.pathComponents else {
                return .notMatch
        }
        switch host {
        case "profile":
            guard paths.count >= 1 else { return .notMatch }
            return .profile(userId: paths[1])
        case "settings":
            return .settings
        default:
            return .notMatch
        }
    }
}

struct Router {
  // 遷移処理はRxSwiftのObservableを返すことで非同期でも行えるようにしています
  func execute(routeType: RouteType) -> Observable<Void> {
      switch routeType {
      case .notMatch: break
      case .profile(let userId):
          return presentProfile(userId)
      case .settings:
          return presentSettings()
      case .http(let url):
          return presentWeb(url)
      }
      return Observable.empty()
  }
}

URIベースのルーティングテーブルはパスとIDで構成されます。パスは対応するVCのクラスを一意に定めます。VCが必要なデータそのものを引数とせず、データのIDのみを引数にとる設計だとURI情報のみである画面へ遷移が可能になりよりルーティングと相性がよくなります。

// URI /profiles/:id
final class ProfileViewController: UIViewController {
  let userRealm: UserRealm

  init(userId: Int) {
    self.userRealm = try! Realm().objectForPrimaryKey(UserRealm.self, key: userId)!
    super.init(nibName: nil, bundle: nil)
  }

  required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}

VCはデータのIDのみを自身の初期化時に受け付けて紐づく必要なデータをローカルのDBや通などから取得します。ルーティングや遷移元のVCから、遷移先のVCへ依存はIDのみになるので疎結合であり独立生が高くVCの責務がVCに閉じる設計となります。

また、VCの初期化時の引数により内部の分岐するような場合は、引数をenumで定義して分岐処理部分を極力enumに実装することでVCの可読性が高くなります。以下は1つのVCで自分と他人のプロフィールを表示する例です。次のtweetに続きます。

enum ProfileDisplayType {
  case mine
  case other(userId :Int)
  var title: String {
    switch self {
    case .mine: return "My profile"
    case .other(_): return "\(userRealm.name)'s pofile"
    }
  }
  var userRealm: UserRealm {
    switch self {
    case .mine: return try! Realm().objectForPrimaryKey(UserRealm.self, key: Shared.mineId)!
    case .other(let userId): return try! Realm().objectForPrimaryKey(UserRealm.self, key: userId)!
    }
  }
}

自分か他人かの差異はenumに閉じているので、VCではその差異を意識せず見通しの良い記述が行えます。

// URI /profiles/:id OR /mine
final class ProfileViewController: UIViewController {
  let userRealm: UserRealm
  let profileDisplayType: ProfileDisplayType

  init(profileDisplayType: ProfileDisplayType) {
    self.profileDisplayType = ProfileDisplayType
    self.userRealm = profileDisplayType.userRealm
    super.init(nibName: nil, bundle: nil)
  }

  required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }

  override func viewDidLoad() {
    super.viewDidLoad()
    title = profileDisplayType.title
  }
}

さて、StoryBoardは使わない方針としていますが、やはり部分的にInterfaceBuilderを利用してビューのデザインを行いたいケースも多々あります。そのような場合はVCに紐づくStoryBoardで構築するのではなくビューに紐づくXIBを利用しています。

XIBの初期化を行うコードは以下のように記述が長くForceUnwrapもあり、引数のnibNameは文字列をとるなど頻繁に記述するには使い勝手がよくありません。

let nib = UINib(nibName: "HogeView", bundle: nil)
let hogeView = nib.instantiateWithOwner(self, options: nil).first as! HogeView

そこで、Protocolを活用して記述しやすくかつ、ForeUnwarapを局所化しています。ビューのクラス名とXIBのファイル名を揃えることで文字列の介入を防いでいます。

protocol InstantiatableFromNib {
  static var nibName: String { get }
  static func instantiateFromNib() -> Self
}

extension InstantiatableFromNib where Self: UIView {
  static var nibName: String {
    get { return String(Self.self) }
  }

  static func instantiateFromNib() -> Self {
    let nib = UINib(nibName: nibName, bundle: nil)
    return nib.instantiateWithOwner(nil, options: nil).first as! Self
  }
}

以下のようにInstantiatableFromNibプロトコルに準拠したビューを定義します。XIBの初期化を行うコードは文字列とForceUnwrapが排除され記述しやすく可読性も向上しました。

// HogeView.swift has HogeView.nib
final class HogeView: UIView, InstantiatableFromNib { }

// HogeViewController.swift
final class HogeViewController: UIViewContoller {
  private lazy var hogeView: HogeView = { return HogeView.instantiateFromNib() }()
}

InterfaceBuilderを利用してビューの生成、AutoLayoutの設定を行うのもよいですが、強力なSwiftの世界で行えないものでしょうか。以降では左記の2点(AutoLayout、ビューの生成)について掘り下げていきます。

Swiftと言えどもAutoLayoutをそのままコードで扱うのはとても辛いです。以下はparentViewに追加したchildViewがparentViewと同じ4辺を共有するレイアウトの例です。2行ぐらいで済みそうな記述なのにこんなに記述する必用があります。

parentView.addSubView(childView)
childView.setTranslatesAutoresizingMaskIntoConstraints(false)
parentView.addConstraint(NSLayoutConstraint(item: childView, attribute: .Top, relatedBy: .Equal, toItem: parentView, attribute: .Top, multiplier: 1.0, constant: 0))
parentView.addConstraint(NSLayoutConstraint(item: childView, attribute: .Right, relatedBy: .Equal, toItem: parentView, attribute: .Right, multiplier: 1.0, constant: 0))
parentView.addConstraint(NSLayoutConstraint(item: childView, attribute: .Bottom, relatedBy: .Equal, toItem: parentView, attribute: .Bottom, multiplier: 1.0, constant: 0))
parentView.addConstraint(NSLayoutConstraint(item: childView, attribute: .Left, relatedBy: .Equal, toItem: parentView, attribute: .Left, multiplier: 1.0, constant: 0))

そこでSnpaKitというライブラリを活用します。先程と同等のコードが端的で直感的な記述で行うことができます。非常に簡単にAutoLayoutを記述できるので自分はXIBによるレイアウトはごく一部でほとんどのAuotLayoutをSnapKitで記述しています。

parentView.addSubView(childView)
childView.snp_makeConstraints { $0.edges.equalToSuperview() }

ビューのレイアウトの次は、コードで行うビューの生成について見ていきましょう。以下は生成とAutoLayoutを行うコードです。ViewDidLoad内ですべて記述しているので画面の規模が大きくなると煩雑になりそうです。

final class HogeViewController: UIViewContoller {
  override func viewDidLoad() {
    super.viewDidLoad()
    let titleLabel = UILabel()
    titleLabel.font = UIFont.boldSystemFontOfSize(32)
    titleLabel.textAlignment = .center
    titleLabel.textColor = .grayColor()

    let nextButton = UIButton()
    nextButton.setTitle("Next", forState: .Normal)
    nextButton.titleLabel?.font = UIFont.systemFontOfSize(13)

    view.addSubview(titleLabel)
    view.addSubview(nextButton)
    titleLabel.snp_makeConstraints {
      $0.top.equalToSuperview().offset(64)
      $0.center.equalToSuperview()
      $0.width.equalTo(100)
    }
    nextButton.snp_makeConstraints {
      $0.bottom.right.equalToSuperview().offset(-16)
      $0.width.height.equalTo(44)
    }
  }
}

そこでビューの生成部分をクロージャーによるデフォルトプロパティを活用して生成と初期設定値をまとめて記述します。ViewDidLoadにいくつものビューを手続き的に記述するよりもビュー単位の初期化コードの範囲が明確に局所化され可読性も向上します。

final class HogeViewController: UIViewContoller {
  private let titleLabel: UILabel = {
    let label = UILabel()
    label.font = UIFont.boldSystemFontOfSize(32)
    label.textAlignment = .center
    label.textColor = .grayColor()
    return label
  }()
  private let nextButton: UIButton = {
    let button = UIButton()
    button.setTitle("Next", forState: .Normal)
    button.titleLabel?.font = UIFont.systemFontOfSize(13)
    return button
  }()

  override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(titleLabel)
    view.addSubview(nextButton)
    titleLabel.snp_makeConstraints {
      $0.top.equalToSuperview().offset(64)
      $0.center.equalToSuperview()
      $0.width.equalTo(100)
    }
    nextButton.snp_makeConstraints {
      $0.bottom.right.equalToSuperview().offset(-16)
      $0.width.height.equalTo(44)
    }
  }
}

注意点としてクロージャが実行される時点ではVCインスタンスが保有するその他のプロパティは初期化されていません。つまりクロージャ内部ではその他のプロパティへアクセスすることが出来ません。ということはselfプロパティも使えませんし、インスタンスメソッドも使えません。

画面間の遷移がサクサクと滑らかに感じられるとユーザの体験は向上します。もしある画面への遷移時にもたつきを感じ、ある画面が遷移の初期状態で表示しないビューがいくつかある場合は、遷移時に表示しないビューを初期化しないことで遷移の速度を向上できることがあります。

上記は遅延評価プロパティを活用することで容易に記述できます。

  • 初期値の設定処理を参照が発生するまで遅延できる
  • デフォルトプロパティと異なりselfを参照できる
  • クロージャー内のself参照は循環参照を発生しない

遅延評価については、Kumagaiさんが大変すばらしい資料にまとめてくださっているのでご参照ください。
http://www.slideshare.net/tomohirokumagai54/lazy-var-cocoakansai-cswift

以下のコードはemptyViewの初期化を通信の結果件数が0件であると評価するまで遅延する例です。

final class HogeViewController: UIViewContoller {
  private lazy var emptyView: UIView = {
    let view = UIView()
    self.view.addSubview(view)
    view.snp_makeConstraints { $0.edges.equalToSuperview() }

    let label = UILabel()
    label.textAlignment = .center
    label.text = "No data"
    view.addSubview(label)
    label.snp_makeConstraints { $0.center.width.equalToSuperview() }
    return view
  }()

  override func viewDidLoad() {
    super.viewDidLoad()
    // リクエスト部分は擬似コードです
    Session.shard.request() { [weak self] response in
      // 以下の最初の参照時に初めてemptyViewが初期化される
      emptyView.hidden = (response.count > 0)
    }
  }
}

ビューの遅延評価による初期化だけでなく、子のVCの初期化遅延にも活用できます。

final class HogeViewController: UIViewContoller {
  private lazy var searchViewController: SearchViewController = {
    let vc = SearchViewController()
    self.addChildViewController(vc)
    self.view.addSubview(vc.view)
    vc.view.snp_makeConstraints { $0.edges.equalToSuperview() }
    vc.didMoveToParentViewController(self)
    return vc
  }()
}

extension HogeViewController: UISearchBarDelegate {
   func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
      // 以下の最初の参照時に初めてsearchViewControllerが初期化される
      searchViewController.search(searchText)
   }
}

UITableViewはアプリにおいて頻出のコンポーネントです。画面のVCをUITbaleViewController(以下TVC)を継承して開発する方も多いのではないでしょうか。 そうするといくつか問題が発生することがあるのでTVCを継承しなくなりました。

TVCを継承するとtablewViewがVCの起点ビューとなるため後々のUI/UXの変更によりtablewViewと並列やtablewViewよりも背後のビューを生成できずに作りなおすはめになることがあるからです。なのでTVCを子のVCとして扱うようにしています。

final class HogeViewController: UIViewContoller {
  private lazy var tableViewController: UITableViewController = {
      let vc = UITableViewController(style: .Plain)
      let tableView = vc.tableView
      let tableFooterView = UIView()
      tableView.tableFooterView = tableFooterView
      tableView.rowHeight = 44
      tableView.layoutMargins = UIEdgeInsetsZero
      tableView.delegate = self
      tableView.dataSource = self
      self.addChildViewController(vc)
      self.view.addSubview(vc.view)
      vc.view.snp_makeConstraints { $0.edges.equalToSuperview() }
      vc.didMoveToParentViewController(self)
      return vc
  }()
  private var tableView: UITableView { return tableViewController.tableView }
}

ちなみにtableViewのみを作成せずにUITableViewControllerを活用しているのはセルをタップしてUINavigationController#pushで遷移してpopで戻ってきた場合にセルのハイライトをアニメショーン付きでオフにしたいためです。

TVCを継承しない話をしましたが、僕は基本的にアブストラクトなVCを設けて処理を共通化する方法は行わないようにしています。ある画面のVCは常にUIViewControllerを継承し、finalを明記することで自身も継承させてません。

VCである処理を共通化したい場合は、独自のビューを持つ場合は任意のVCクラスと切り出して子のVCとして扱うか、プロトコルによる抽象化とプロトコルエクステンションによる実装を行います。これらにより肥大化しやすいVCをコンパクトに扱うよう心がけています。

protocol FBAppPresentable {}
extension FBAppPresentable where Self: UIViewController {
  func openFacebookUrl(url: NSURL) {
    guard let userId = url.lastPathComponent where Global.fbInstalled else {
        presentWebViewController(of: url); return
    }
    guard let encodedUrl = NSURL.encoded(of: "fb://profile/\(userId)") else {
        presentWebViewController(of: url); return
    }
    UIApplication.sharedApplication().openURL(encodedUrl)
  }
}

継承ではなくプロトコルによる共通化を行うのは共通化したい対象を責務の範囲として局所化しやすいためです。プロトコルはVCに多数適用できますが、継承だとどうしても最大公約数的な共通処理のアブストラクトなVCを継承しがちで多様な共通処理がクラスに記述されてしまいます。

UIViewControllerをextensionすることで共通化を行うこともできますが、実装した影響範囲がすべてのVCになるので本当にすべてのVCで利用するような処理だけを記述するようにしています。

extension UIViewController {
  func screenName() -> String {
    let subjectType = Mirror(reflecting: self).subjectType
    let className = "\(subjectType)"
    let screenName = className.stringByReplacingOccurrencesOfString("ViewController", withString: "")
    return screenName
  }
}

以上で「Swift時代に悩ましいUIViewControllerをどう扱うか」終了です。ObC時代から苦しめられたVCですがSwiftのお陰で悩みも軽減された感じがします。それでは、ごTweet聴ありがとうございました。m(_ _)m

Wantedly, Inc.では一緒に働く仲間を募集しています
48 いいね!
48 いいね!
今週のランキング