72
68

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-04-13

以下の記事はbanjun氏との共同執筆です。 banjun氏の深い知識と経験に尊敬と感謝の意を表します。

要約

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

ちょっと長い要約

  1. UITableViewControllerのコンストラクタにはバグがある
  2. Swift 1.1には、簡単な回避方法があった
  3. 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:

https://developer.apple.com/library/mac/releasenotes/DeveloperTools/RN-Xcode/Chapters/Introduction.html

補足2: 原因に対する考察

このようになっている原因はbanjun/SwiftUnsafeTableViewControllerで考察されている。

UITableViewControllerinit(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")
    }
}

実行結果:
Screen Shot 2015-04-14 at 12.38.53 AM.png

関連した問題

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

72
68
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
72
68

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?