Help us understand the problem. What is going on with this article?

Swift4 [SE-160 Limiting @objc inference] 概要

More than 3 years have passed since last update.

SE-160 Limiting @objc inferenceの概要

TL;DR

  • Objective-Cランタイムから呼ぶ必要があるものは明示的に@objcの付与が必要になった(今まではコンパイラが勝手につけていた)
  • @objcをクラス全体に適用する@objcMembersが追加になった
  • いらない内部コードが減ってバイナリサイズが6-8%減る
  • マイグレーションとかはXcodeのマイグレータでほぼOK

前提

Swift3では@objc推論が多く行われていた
(@objcとはSwiftのコードをObjective-Cランタイムから呼べるようにするためのもの)
以下の場合に暗黙的に@objc推論が行われている

  • Objective-Cのクラスを継承している
  • dynamicキーワードを付与している変数やメソッド
  • etc.

提案された理由

  • @objc推論される規則がわかりにくい
  • Objective-Cのセレクターの衝突が起こりがち
class MyNumber : NSObject {
  init(_ int: Int) { }
  init(_ double: Double) { } // error: initializer 'init' with Objective-C selector 'init:'
      // conflicts with previous declaration with the same Objective-C selector
}
class MyNumber : NSObject {
  @objc(initWithInteger:) init(_ int: Int) { }
  @objc(initWithDouble:) init(_ double: Double) { }
}
  • SwiftコンパイラはObjective-C Calling ConventionからSwift Calling Conventionを関連づけるthunkメソッドをObjective-Cメタデータ内に記録するためバイナリサイズが大きくなってしまう
    • Objective-C entry pointごとにコストがかかる
    • thunkメソッドにより6-8%のバイナリサイズが大きくなる(未使用のものも含まれている) Apple's Music Appでは5.7%サイズが減った

提案

プログラムの整合性上、必要な部分を除き@objc推論をしないようにする

その上で広範囲で@objcをon/offする時に定型記述を減らすためにclassレベル、extensionレベルのアノテーションを追加する

引き続き推論されるパターン

  • @objc属性がついているものをオーバーライドする場合
    • Super.foo()に対するObjective-C callersがオーバーライドしたSub.foo()を適切に呼び出すため
class Super {
    @objc func foo() { }
}

class Sub : Super {
    /* inferred @objc */
    override func foo() { }
}
  • @objc属性がついているprotocol要件を満たす時
    • MyDelegate.bar()がObjective-C message sendを介して呼ばれるため
@objc protocol MyDelegate {
    func bar()
}

class MyClass : MyDelegate {
    /* inferred @objc */
    func bar() { }
}
  • @IBAction,@IBOutlet属性がついている時
    • Interface BuilderとのインタラクションがObjective-Cランタイムで行われるため
  • @NSManaged属性がついている時
    • Core DataとのインタラクションがObjective-Cランタイムで行われるため

追加で推論されるパターン

  • @GKInspectable属性がついている時
    • GameplayKitとのインタラクションがObjective-Cランタイムで行われるため
  • @IBInspectable属性がついている時
    • Interface BuilderとのインタラクションがObjective-Cランタイムで行われるため

推論されなくなるパターン

  • dynamicキーワードが付与されているもの
    • dynamicは動的ディスパッチをするためのもの(Objective-Cランタイムからよびだされる)なのでSwift3の時は@objcが推論されていた
class MyClass {
  dynamic func foo() { }       // error: 'dynamic' method must be '@objc'
  @objc dynamic func bar() { } // okay
}
  • NSObject由来のもの
    • これがこの提案で唯一の大きな変更
    • これにより多くのメソッドをObjective-Cから呼べなくなる(これによりバイナリサイズが小さくなる)
class MyClass : NSObject {
  func foo() { } // not exposed to Objective-C in Swift 4
}

Swift3時代はObjective-Cで表現できるもののみ@objc推論されていた

extension MyClass {
  func bar(param: ObjCClass) { } // Swift3では推論された。この提案では推論されない
  func baz(param: SwiftStruct) { } // StructはObjective-Cでは表現できないので推論されない
}

クラス全体で@objc推論を有効にさせる(@objcMembers)

XCTest,Realmのようないくつかのライブラリは依然としてObjective-Cランタイムに依存する
そのためクラス、そのExtension、サブクラス、およびそれらすべてのExtensionに対して今まで通りの@objc推論を一括で行うため
@objcMembers属性を新しく追加する。

@objcMembers
class MyClass : NSObject {
  func foo() { }             // implicitly @objc

  func bar() -> (Int, Int)   // not @objc, because tuple returns
      // aren't representable in Objective-C
}

extension MyClass {
  func baz() { }   // implicitly @objc
}

class MySubClass : MyClass {
  func wibble() { }   // implicitly @objc
}

extension MySubClass {
  func wobble() { }   // implicitly @objc
}

また以下のようにswift_objc_membersを付与することで
インポートされたObjective-Cクラスを@objcMembersとしてSwiftにインポートすることができる

__attribute__((swift_objc_members))
@interface XCTestCase : XCTest
/* ... */
@end

@objcMembers
class XCTestCase : XCTest { /* ... */ }

Extensionでの@objcのon/off

Extensionに@objc or @nonobjc属性をつけることで
そのExtension内の@objcor@nonobjc属性を個別で持ってないメンバーに対して一律適用できる

class SwiftClass { }

@objc extension SwiftClass {
  func foo() { }            // implicitly @objc
  func bar() -> (Int, Int)  // error: tuple type (Int, Int) not
      // expressible in @objc. add @nonobjc or move this method to fix the issue
}

@objcMembers
class MyClass : NSObject {
  func wibble() { }    // implicitly @objc
}

@nonobjc extension MyClass {
  func wobble() { }    // not @objc, despite @objcMembers
}

Swift4へのマイグレーション

Step1:
Swift4モードで、@objc推論されている警告の対処をする

Step2:
環境変数SWIFT_DEBUG_IMPLICIT_OBJC_ENTRYPOINTの値を1〜3にしてアプリをテストする

  1. ObjectiveCのエントリポイントへの呼び出しを記録
    ex.***Swift runtime: entrypoint -[MyApp.MyClass foo] generated by implicit @objc inference is deprecated and will be removed in Swift 4
  2. 1 + バックトレースを発行
  3. 2 + クラッシュ

Step3:
この時点でSwift4へマイグレーション可能になる
Swift4でビルドすると、廃止予定のルールに基づき@objc推論されたObjective-Cエントリポイントが削除される

※実際はXcodeのマイグレーターがよしなにやってくれるので、自分のプロジェクトではここら辺は何も考える必要がなかった

furuyan
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away