January 12, 2017

NS_REFINED_FOR_SWIFTって何者なのかと使い道

Categories: 技術 | Tags: #Swift #Objective-C


ふとソースコードを眺めていると、 NS_REFINED_FOR_SWIFT というマクロが目に止まったのでこいつはなんなのか調べてみることにしました。

DocumentRefining Objective-C Declarations の項を眺めてみると、
このマクロを使用することで、Objective-Cで宣言されたメソッド(オリジナル)をキープしたまま、Swiftでより良い(改善された)インターフェースを提供することが可能になりますよと書いてあります(ざっくりですみません)
また、このマクロを付与することで、次に書くルールに則ってSwift側で見た時のメソッド名が変化するため、Swift側では元のメソッド名で呼び出すことができなくなります。(変化したメソッドはもちろん見ることができます)
なので、Swift側で改良したインターフェースと、オリジナルそのままのインターフェースと2つが見えてしまうという問題は発生しないようです。

refineのルール

この NS_REFINED_FOR_SWIFT を適応することでSwift側でどのようにオリジナルのメソッドを見ることができるかは、一定のルールがあります

イニシャライザの場合

- (instancetype)initWithMessage:(NSString *)message のようなイニシャライザメソッドの場合は、Swiftだと
init(__message: String) のように、最初の引数ラベルに アンダースコア2つ 付いたイニシャライザに変化します。

サブスクリプトメソッドの場合

Objective-Cで、object[0]object[@"banana"]の用に添え字アクセスを可能にするサブスクリプトを実装していて、それに適応した場合は、
Swift側でsubscript定義に変わる…のではなく、それぞれ アンダースコア2つ 付けたメソッドに変化します。

その他のメソッド

単純にメソッド名の銭湯に アンダースコア2つ つきます。

どういう時に使うのか

別にObjective-Cの実装がそのまま使えるなら良いのではと思うこともありますが、

  • よりSwiftに適した関数名を付けることができる
  • Objective-Cでidinstancetypeを付けている箇所を、Swift側で吸収して型を定めてあげて使いやすくする
    (→そのままだとAnyとかになってて使いづらい…とか)
  • Objective-Cではメソッドだけど、Swiftでは計算型プロパティに変更したい

といった場合に有用になります。そしてそれらを、Objective-CのソースをSwiftに書き直すことなく実現ができるので、
がっつりSwiftのソースコードに移行させる前の手助けになるんじゃないかと思います。

使用例1 (Appleのexample)

Documentにはこのようなサンプルが載っています。

@interface Color : NSObject

- (void)getRed:(nullable CGFloat *)red
         green:(nullable CGFloat *)green
          blue:(nullable CGFloat *)blue
         alpha:(nullable CGFloat *)alpha NS_REFINED_FOR_SWIFT;

@end
extension Color {
    var RGBA: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
        var r: CGFloat = 0.0
        var g: CGFloat = 0.0
        var b: CGFloat = 0.0
        var a: CGFloat = 0.0
        __getRed(&r, green: &g, blue: &b, alpha: &a)
        return (red: r, green: g, blue: b, alpha: a)
    }
}

この例では、 Color というクラスの getRed:green:blue:alpha というメソッドを、Swiftでは RGBA として呼び出せるように改良しています。

このように定義してあげることで、以下のように呼び出し方が変化します

// NS_REFINED_FOR_SWIFTを付けない場合
var red: CGFloat = 0.0
var green: CGFloat = 0.0
var blue: CGFloat = 0.0
var alpha: CGFloat = 0.0
Color().getRed(&red, green: &green, blue: &blue, alpha: &alpha)

// NS_REFINED_FOR_SWIFTを付けた場合
let rgba = Color().RGBA (タプルで返ってくる)
_ = rgba.green

ほんのちょっとの手間を加えてあげると、よりSwiftっぽい書き方になりますね!
特にこの例だと、Swift側で UnsafeMutablePointer を渡さないといけないのを回避できますし、結果をそれぞれ4変数で受けるのではなくて、タプルとして受けることができるので、
そのまま我慢して呼び出すよりは遥かに改善されています。Swifty。

使用例2 (Objective-CのSingleton)

大規模なプロジェクトで、なかなかSwift化ができなかったり、するのがリスクとなりがちなのが何かしらのヘルパー、マネージャー的な役割を果たすシングルトンクラスだったりします。
例えば、 UploadHelper という(ちょっと怪しいにおいのする)シングルトンがあったとします。

@interface UploadHelper : NSObject
+ (instancetype)sharedHelper
- (void)upload;
@end

これをSwift側で呼び出すと、

UploadHelper.shared().upload

となり、常に shared の後ろに()がついてまわります。
幸いにも sharedHelper() とはならずすっきりとしましたが、Swiftではシングルトンを定義する時に

@import Foundation
import UIKit

class UploadHelper {
    static let shared = UploadHelper()
    private init() {
    }
}

とすることが多いため、 () がつかないことが多いです。
この差が気になる時は、 NS_REFINED_FOR_SWIFT を使うと解決させることができます

@interface UploadHelper : NSObject
+ (instancetype)sharedHelper NS_REFINED_FOR_SWIFT;
- (void)upload;
@end
extension UploadHelper {
    static var shared: UploadHelper {
        return UploadHelper.__shared()
    }
}

// 呼び出し時
UploadHelper.shared.upload()

まとめ

大規模すぎてなかなかObjective-CからSwiftに移行しづらいけど、Swiftで使う時の書き方や利便性を向上させたい!
というときは是非活用してみてください。
と、ちょっと深掘りした内容でお送りしました。


written by sgr-ksmt