Swift 1.2 + UITableViewControllerで発生する問題と回避方法


以下の記事は[banjun](http://qiita.com/banjun)氏との共同執筆です。 banjun氏の深い知識と経験に尊敬と感謝の意を表します。

# 要約
UITableViewControllerを使わずに、自分で実装しろ。

# ちょっと長い要約

 1. UITableViewControllerのコンストラクタにはバグがある
 2. Swift 1.1には、簡単な回避方法があった
 3. Swift 1.2で使えなくなったので、別の回避方法が必要

# 前提知識
Swiftのdesignated initializer と convenience initializerに関する知識を前提する。

 * [[Swift] クラスのイニシャライザ３： Designated Initializer と Convenience Initializer - Qiita](http://qiita.com/cotrpepe/items/3052d91468f1b852582a)

# UITableViewControllerに関する問題
## 複数回呼ばれる `designated initializer`
次のように `super.init(style:)` だけを呼ぶUITableViewControllerのサブクラスを作る。

```swift
import UIKit

class ViewController: UITableViewController {

     init() {
          super.init(style: .Grouped)
     }

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

このクラスを `init()` で初期化すると、以下のような実行時エラーが発生する。

```
/Users/mzp/Documents/TableViewSample/TableViewSample/ViewController.swift: 3: 7: fatal error: use of unimplemented initializer 'init(nibName:bundle:)' for class 'TableViewSample.ViewController'
```

これは `UITableViewControllerのinit(style:)` の内部で、`UITableViewContlollerのinit(nibName:bundle:)` ではなく、`ViewControllerのinit(nibName:bundle:)` が呼ばれてしまっているためである。

そのため、`init(nibName:bundle:)` を実装することで回避できる。

```swift
import UIKit

class ViewController: UITableViewController {

     init() {
          super.init(style: .Grouped)
     }

     override init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) {
          // DO NOTHING
          super.init(nibName: nil, bundle: nil)
     }

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

ただ、これは `ViewController` のdesignated initializerが2回呼ばれるため、[SwiftBook](https://itunes.apple.com/jp/book/swift-programming-language/id881256329?mt=11)に記載された挙動と合致しない。

## 発生する問題
本来1度しか実行されないdesignated initializerが2回呼ばれるので、`let` で宣言したプロパティであっても、予期されない値が代入される。

```swift
import UIKit

class ViewController: UITableViewController {
     let x : Int

     init() {
          self.x = 1
          super.init(style: .Grouped)
          NSLog("%d", x) // あれ x が 2 になってる???
     }

     override init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) {
          self.x = 2
          super.init(nibName: nil, bundle: nil)
     }

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

また初期化が2回実行されるため、非常に重い初期化処理等を行なっている場合などにも問題となる。

# Swift 1.1以前における回避方法
Swift 1.1までは、`init` 内で `let` の再代入ができた。
そのため `let x: Int!` としておき，`super.init` 後に正しい値を(再)代入することで，designated initializerが2回呼ばれても1箇所で初期化を行なえた。

```swift
// Swift 1.1時代の回避策
import UIKit

class ViewController: UITableViewController {
     let x : Int!

     init() {
          // 暗黙的にxにはnilが代入される
         super.init(style: .Grouped)
// ここだけで初期化を行なう
         self.x = 1
     }

     override init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) {
          super.init(nibName: nil, bundle: nil)
     }

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

# Swift 1.2における回避方法
## letの挙動の変化
Swiftでは宣言時に初期化されていないプロパティは`super.init(...)` 以前に初期化しなければいけないが、Swift 1.2で `init` 内であっても再代入できなくなった。

```swift
class Base {}

class Foo : Base {
     let x : Int! = nil

     override init() {
          super.init()
          // init内でも再代入できない
          self.x = 2
     }
}
```

そのため、以下のコードは前述のSwift 1.1では回避方法を用いることはできなくなった。

## 回避方法1: すべてvar + ImplicitlyUnwrappedOptional(!) にする
すべての変数を `var` かつImplicitlyUnwrappedOptional(`!`)にすれば、`super.init(...)` の後に初期化を行なえるので、前述の問題を回避できる。

```swift
import UIKit

class ViewController: UITableViewController {
     var x : Int!

     init() {
          super.init(style: .Grouped)
          self.x = 1
          NSLog("%d", x)
     }

     override init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) {
          super.init(nibName: nil, bundle: nil)
     }

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

ただ、`nil` に対して非常に弱くなるので、Swiftを使う価値が激減してしまう。 また、designated initializerが複数回呼ばれるという根本の問題を解決できていない。

## 回避方法2: UITableViewController相当のクラスを実装する
UITableViewControllerの不具合が原因のため、UITableViewControllerを再実装すれば、この問題を回避できる。 例えば、今回は [SafeTableViewController](https://github.com/codefirst/FlickSKK/blob/02af2c0c8f3a557b37ba38292f2822ec81539955/FlickSKK/SafeTableViewController.swift) のようなクラスを実装し、この問題を回避した。

```swift
//
//  SafeTableViewController.swift
//  FlickSKK
//
//  Created by BAN Jun on 2015/04/13.
//  Copyright (c) 2015年 BAN Jun. All rights reserved.
//

// NOTE: workaround for fatal error: use of unimplemented initializer 'init(nibName:bundle:)'
// see https://github.com/banjun/SwiftUnsafeTableViewController
// remove after everything is purified

import UIKit

class SafeTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    var tableView: UITableView { return view as! UITableView }
    
    init(style: UITableViewStyle) {
        super.init(nibName: nil, bundle: nil)
        
        view = UITableView(frame: CGRectZero, style: style)
        tableView.delegate = self
        tableView.dataSource = self
        viewDidLoad()
    }

    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func loadView() {
        // do nothing
    }
    
    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        
        if let selected = tableView.indexPathForSelectedRow() {
            tableView.deselectRowAtIndexPath(selected, animated: animated)
        }
        tableView.reloadData()
    }
    
    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        
        tableView.flashScrollIndicators()
    }
    
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 0
    }
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        fatalError("tableView(tableView:cellForRowAtIndexPath:) has not been implemented")
    }
}
```


# 補足
## 補足1: Xcode6.3 Known Issues
これは、Xcode 6.3のRelease notesにもKnown Issuesとして記載されている。(が、記載されている回避方法も謎)

> To override the designated initializer initWithNibName:bundle: you will need to declare it as a designated initializer in a class extension in an Objective-C bridging header. The following steps will guide you through this process:
> 
> https://developer.apple.com/library/mac/releasenotes/DeveloperTools/RN-Xcode/Chapters/Introduction.html

## 補足2: 原因に対する考察
このようになっている原因は[banjun/SwiftUnsafeTableViewController](https://github.com/banjun/SwiftUnsafeTableViewController/blob/master/UnsafeTableViewController/UnsafeTableViewController.m )で考察されている。

`UITableViewController`の`init(style:)`では本来は`super`に対してメソッド呼び出しをしなければならないが、誤って`self`が使われいる可能性がある。

## 補足3: 複数インスタンスが生成される場合
本文中では問題点として予期しない値が代入される可能性のみを挙げたが、使い方によっては2個インスタンスが生成され，予期せぬインスタンスを使ってしまう場合も存在する。

例えば、`UINavigationController`のサブクラスと宣言初期化を併用すると、本来同じインスタンスを差すはずの変数が違う値を持ってしまう。

```swift
class NavigationController: UINavigationController {
    let rootVC = UIViewController()
    
    init() {
        super.init(rootViewController: rootVC)
    }
    
    override init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) {
        super.init(nibName: nil, bundle: nil)
    }

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

実行結果:
![Screen Shot 2015-04-14 at 12.38.53 AM.png](https://qiita-image-store.s3.amazonaws.com/0/5920/80a0fc7f-df02-f2e5-871b-f31e33b7be44.png "Screen Shot 2015-04-14 at 12.38.53 AM.png")


# 関連した問題
`UINavigationController`の`init(rootViewController:)`もSwiftからはdesignatedに見えるが同様にサブクラスから呼べない。`UIViewController` のサブクラスファミリーに注意したほうがよい。