以下の記事はbanjun氏との共同執筆です。 banjun氏の深い知識と経験に尊敬と感謝の意を表します。
要約
UITableViewControllerを使わずに、自分で実装しろ。
ちょっと長い要約
- UITableViewControllerのコンストラクタにはバグがある
- Swift 1.1には、簡単な回避方法があった
- Swift 1.2で使えなくなったので、別の回避方法が必要
前提知識
Swiftのdesignated initializer と convenience initializerに関する知識を前提する。
UITableViewControllerに関する問題
複数回呼ばれる designated initializer
次のように super.init(style:)
だけを呼ぶUITableViewControllerのサブクラスを作る。
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:)
を実装することで回避できる。
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に記載された挙動と合致しない。
発生する問題
本来1度しか実行されないdesignated initializerが2回呼ばれるので、let
で宣言したプロパティであっても、予期されない値が代入される。
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 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
内であっても再代入できなくなった。
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(...)
の後に初期化を行なえるので、前述の問題を回避できる。
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 のようなクラスを実装し、この問題を回避した。
//
// 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:
補足2: 原因に対する考察
このようになっている原因はbanjun/SwiftUnsafeTableViewControllerで考察されている。
UITableViewController
のinit(style:)
では本来はsuper
に対してメソッド呼び出しをしなければならないが、誤ってself
が使われいる可能性がある。
補足3: 複数インスタンスが生成される場合
本文中では問題点として予期しない値が代入される可能性のみを挙げたが、使い方によっては2個インスタンスが生成され,予期せぬインスタンスを使ってしまう場合も存在する。
例えば、UINavigationController
のサブクラスと宣言初期化を併用すると、本来同じインスタンスを差すはずの変数が違う値を持ってしまう。
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")
}
}
関連した問題
UINavigationController
のinit(rootViewController:)
もSwiftからはdesignatedに見えるが同様にサブクラスから呼べない。UIViewController
のサブクラスファミリーに注意したほうがよい。