読者です 読者をやめる 読者になる 読者になる

shobylogy

叩けシンプルの杖

Swift + iOS 8のUITableViewControllerのバグを回避するためのSwiftLintカスタムルール

iOS 8のUITableViewControllerの実装にはバグがあり、Swiftでサブクラスを作ると、iOSのクラッシュが発生する場合があります。

今回はそれをSwiftLintで検知して回避するためのカスタムルールについて説明します。

なお、以下の説明はXcode 7.3.1 + Swift 2.2環境を想定しています。

Swift + iOS 8のUITableViewControllerバグとは

SwiftでUITableViewControllerのサブクラスを実装している場合、iOS 8での実行時にクラッシュが発生する場合があります。

具体的には、以下の条件を満たす際に発生します。

  • SwiftでUITableViewControllerのサブクラスを実装
  • letでpropertyを宣言
  • 独自のinitializerを実装し、内部でsuper.init(style:)を呼んでいる
  • init(nibName:bundle:) が未定義

具体的な以下のようなコードです。

class ViewController: UITableViewController {
    let id: Int
    
    init(id: Int) {
        self.id = id
        super.init(style: .Plain)
    }
}

上記の条件が満たされる場合、実行時に以下のようなエラーが出てクラッシュします。

fatal error: use of unimplemented initializer 'init(nibName:bundle:)' for class 'AppName.SubClassTableViewController'

これはiOS 8のバグが原因で、ビルド時にはエラーが出ず、実行時にしか検知することができません。

どうやら、UITableViewControllerの init(style:) 内で super.init(nibName:bundle:) ではなく、 init(nibName:bundle:) が呼ばれてしまっているのが原因なようです。

詳しい原因についてはこちらの記事が詳しいです。ご参照ください。 Swift 1.2 + UITableViewControllerで発生する問題と回避方法

このバグが厄介な点は、以下の3点にあります。

  • immutableなSwiftらしいコードを書いて初めて発生する
  • 実行時にしか問題に気づけない
  • 主流でない過去のバージョンのiOSでしか発生しない

そのため、チームのSwift開発力が向上したタイミングで問題が起き始め、過去のiOSのバグの対応に追われる羽目になります。

このバグへの対応策ですが、根本的にはiOS 8をサポート対象外にする以外はありませんが、以下のように回避策を取ることはできます。*1

  • propertyのletをやめてvarにする
  • nonoptionalをoptionalにする
  • init後に代入する
class ViewController: UITableViewController {
    var id: Int?
    
    init(id: Int) {
        super.init(style: .Plain)

        self.id = id
    }

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

発生条件を満たすすべてのUITableViewControllerに上記のような対応をする必要があり、対応が漏れていても実行時まで一切気づけないのがつらいところです。

そのため、SwiftLintのカスタムルールを使い、発生条件に相当するUITableViewControllerに対してエラーを出すことで、実行時にしか気づけない問題をビルド時に気づけるようにしました。

SwiftLintのカスタムルール

SwiftLintのカスタムルールを使うと、気合いや根性で解決していた問題をルールに落とし込むことができます。*2

今回は、以下の条件をすべて満たす場合にerrorを出すルールを作りました。

  • ファイル内にUITableViewControllerのサブクラスが実装されている
  • ファイル内にinit もしくは init?(という文字列が存在する
  • ファイル内に override init(nibName という文字列が存在しない
    ios8_table_view_controller_bug:
        name: "iOS 8 TableViewController Bug"
        regex: "class.+UITableViewController(?=[\s\S]*init(\?)?\()(?![\s\S]*override.+init\(nibName)"
        message: "Please implement init(nibName:bundle)."
        severity: error

以下のようなエラーが出て、対応漏れに気づけます。

f:id:shoby:20161002004703p:plain

ViewController.swift:11:1: error: iOS 8 TableViewController Bug Violation: Please implement init(nibName:bundle). (ios8_table_view_controller_bug)

まとめ

iOS 8のUITableViewControllerにはバグがあり、Swiftでサブクラスを実装している場合、実行時にクラッシュする場合があります。

この問題は実行にしか気づけないですが、SwiftLintのカスタムルールを使うことによりビルド時にエラー表示することができます。

このように、SwiftLintを使うと、気合いや根性でなんとかするしかなかった問題をルール化できるので、皆さんもご活用ください。

*1:公式のRelease Noteによれば、Objective-CのBridging Headerに特殊なコードを記入すれば、回避できるようですが、私の環境(Xcode 7.3.1 + Swift 2.2)ではうまく動きませんでした

*2:スクリーン計測用コードの実装漏れを防ぐこともできます