LoginSignup
36
31

More than 5 years have passed since last update.

NullabilityとGenericsでObjective-CのコードをSwiftから使いやすくする

Posted at

Objective-CからSwiftへ移行する過渡期

数年間開発され続けているアプリにもそろそろSwiftが入ってきているのではないでしょうか。当然、Objective-Cで作ってきた資産を生かしながら開発することになると思います。

この時、Optionalの扱いに苦労するのではないかと思います。Objective-Cではレシーバがnilだった場合にメッセージを送ってもクラッシュするようなことはありませんが、OptionalのあるSwiftからObjective-Cのコードを呼び出す場合はちょっと困ってしまいます。

SwiftとObjective-Cの互換性を強化するために、nullable, nonnullがObjective-Cに追加されています。

Swiftのコードを書いていく際に、これらの型修飾子を使ってObjective-C側も改善をしていくことでSwift導入がやりやすくなると思います。

nullable

nullableはOptionalでありnilを許容することを明示するための型修飾子です。

例えば、NSDatainitWithContentsOfURL:イニシャライザは以下のように定義されており、戻り値がnilになり得ることを明示しています。

NSData.h
- (nullable instancetype)initWithContentsOfURL:(NSURL *)url;

これをSwiftで見てみると、以下のようにfailabel initializer、失敗する可能性のあるイニシャライザとなっていることが分かります。

NSData
public init?(contentsOfURL url: NSURL)

このようにインスタンス生成や引数、戻り値がnilになり得る場合は、Objective-C側でnullableを指定しておくことで、SwiftでOptionalとして扱うことが可能になります。

Objective-Cでnullableを指定しない場合はImplicitly Unwrapped Optionalになります。以下のようなObjective-Cのメソッドで考えてみます。

- (UIImage*)createImage;

nullableをつけずにSwiftから使おうとすると変換時にImplicitly Unwrapped Optionalになってしまいます。

func createImage() -> UIImage!

このメソッドの戻り値を使用しようとした際にnilだった場合クラッシュしてしまうので、SwiftからこのObjective-Cのコードを使用する際に不安が付きまといます。もし、このような処理を見かけたときはnullableを指定し、Swift側でOptionalとして利用できるようにするとSwiftからも安心して利用することができます。

- (nullable UIImage*)createImage;

以上のObjective-Cのコードを修正するとSwiftでは、

func createImage() -> UIImage?

と変換されます。

nonnull

nonnullはOptionalではないことを明示するための型修飾子で、nonnullを関数やメソッドの引数、戻り値に指定した場合はOptionalな変数を指定することはできなくなります。

NS_ASSUME_NONNULL_BEGIN, NS_ASSUME_NONNULL_END

しかしUIKitのソースコードを見てみるとnonnullが見当たりません。

例えばUIVisualEffectViewは以下のように定義されていますが、nonnullはどこにも書かれていません。nullableはしっかりと書かれています。

UIVisualEffectView.h
NS_CLASS_AVAILABLE_IOS(8_0) @interface UIVisualEffectView : UIView <NSSecureCoding>
@property (nonatomic, strong, readonly) UIView *contentView; // Do not add subviews directly to UIVisualEffectView, use this view instead.
@property (nonatomic, copy, nullable) UIVisualEffect *effect;
- (instancetype)initWithEffect:(nullable UIVisualEffect *)effect NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
@end

しかし、Swiftの変換はしっかりと非Optionalになっています。

@available(iOS 8.0, *)
public class UIVisualEffectView : UIView, NSSecureCoding {
    public var contentView: UIView { get } // Do not add subviews directly to UIVisualEffectView, use this view instead.
    @NSCopying public var effect: UIVisualEffect?
    public init(effect: UIVisualEffect?)
    public init?(coder aDecoder: NSCoder)
}

つまりnonnullの指定はしっかりと行われているということです。どうしてnonnullを指定されていないにもかかわらず変換ができているのでしょうか?

調べてみると、NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_ENDマクロが使われていました。

前述のUIVisualEffectView.hにもしっかりとこのマクロが書かれていました。

UIVisualEffectView.h
//
//  UIVisualEffectView.h
//  UIKit
//
//  Copyright (c) 2014-2015 Apple Inc. All rights reserved.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

// ...中略

NS_CLASS_AVAILABLE_IOS(8_0) @interface UIVisualEffectView : UIView <NSSecureCoding>
@property (nonatomic, strong, readonly) UIView *contentView; // Do not add subviews directly to UIVisualEffectView, use this view instead.
@property (nonatomic, copy, nullable) UIVisualEffect *effect;
- (instancetype)initWithEffect:(nullable UIVisualEffect *)effect NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
@end

NS_ASSUME_NONNULL_END

FoundationやUIKitのソースを見てみると、このマクロが使われています。

確かに、nonnull,nullableをいちいち書いていくのは大変ですよね。Objective-Cでnonnull,nullableをつける必要がある場合は積極的にNS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_ENDを使い、nullableだけを書いていくと良いのかもしれません。

注意

同じファイル内のメソッドやプロパティに一つでもnonnull,nullableをつけた場合、ファイル内のすべてのメソッドの引数、戻り値、プロパティに型修飾子をつけなければいけません。warnigが出ます。

スクリーンショット 2016-02-28 20.05.06.png

Lightweight Generics

SwiftからObjective-Cのコードを使おうとした時に、NSArrayからArrayの変換、NSDictionaryからDictionaryの変換では困りませんが、配列の要素の型がAnyObjectになってしまい困ることがあると思います。

このような場合、guardやOptional Bindingなどを使って安全にキャストしていくことになると思いますが、Objective-CのコードをGenericsを使って修正したほうが良さそうです。

UIViewはsubviewsというNSArrayのプロパティを持っていますが、Genericsを使ってUIViewの配列であることを明示しています。

UIView.h
@property(nonatomic,readonly,copy) NSArray<__kindof UIView *> *subviews;

__kindofはサブクラスも許容するアノテーションです。subviewsはUIViewのサブクラスも許容することを示しています。

Objective-CにNullabilityとGenericsを指定していく工程

Objective-Cで以下のように定義されているオブジェクトを見てみます。

@interface MNPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger age;
@property (nonatomic, copy) NSArray *items;
- (NSString*)hey;
- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age;
@end

これをSwiftからも使いやすいようにNullabilityとGenericsを指定してみます。

何も手を加えない場合、Swiftからはこのように見えます。イニシャライザやプロパティに!がついていてImplicitly Unwrapped Optionalになっていることが分かります。

public class MNPerson : NSObject {
    public var name: String!
    public var age: UInt
    public var items: [AnyObject]!
    public func hey() -> String!
    public init!(name: String!, age: UInt)
}

Objective-CのヘッダがSwiftでどのように表示されるかを確認する方法

Objective-CでNullabilityやGenericsの指定をする際に、jump barの左端にあるボタンをクリックすると出現する"Generated Interface"を使ってObjective-CのヘッダファイルがSwiftでどのようなインターフェースになるか確認することができます。

スクリーンショット 2016-02-28 13.55.45.png

nullableの設定

まずはnullableをつけてみましょう。こうなります。

@interface MNPerson : NSObject
@property (nullable, nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger age;
@property (nullable, nonatomic, copy) NSArray *items;
- (nullable NSString*)hey;
- (nullable instancetype)initWithName:(nullable NSString*)name age:(NSUInteger)age;
@end

Nullabilityはポインタ型にのみ指定するものなので、プリミティブな値、NSUIntegerなどにはnullableをつける必要はありません。

これをGenerated Interfaceで見てみると以下のようになります。

public class MNPerson : NSObject {
    public var name: String?
    public var age: UInt
    public var items: [AnyObject]?
    public func hey() -> String?
    public init?(name: String?, age: UInt)
}

nonnullの設定

とりあえずOptionalとして扱えるようになりました。しかし、すべてOptionalだといちいちOptional Bindingなど値を取り出さなければならないのでちょっと面倒です。

nonnullとして扱える箇所がないか実装を見てみます。

- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age {
    self = [super init];

    if (self) {
        _name = name;
        _age = age;
    }
    return self;
}


- (NSString*)hey {
    return @"hey";
}

イニシャライザでインスタンス変数の_name_ageに値がセットされています。heyメソッドも失敗の可能性はないので、これらに該当するプロパティやメソッドの引数をnonnullにしてみましょう。

@interface MNPerson : NSObject
@property (nonnull, nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger age;
@property (nullable, nonatomic, copy) NSArray *items;
- (nonnull NSString*)hey;
- (nonnull instancetype)initWithName:(nonnull NSString*)name age:(NSUInteger)age;
@end

Swiftで見てみるとこのようになります。

public class MNPerson : NSObject {
    public var name: String
    public var age: UInt
    public var items: [AnyObject]?
    public func hey() -> String?
    public init(name: String, age: UInt)

NS_ASSUME_NONNULL_BEGIN,NS_ASSUME_NONNULL_ENDマクロを使うと以下のように書くことができます。

NS_ASSUME_NONNULL_BEGIN
@interface MNPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger age;
@property (nullable, nonatomic, copy) NSArray *items;
- (NSString*)hey;
- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age;
@end
NS_ASSUME_NONNULL_END

nilの可能性がある箇所のみnullableを指定する必要がありますがnonnullのプロパティに対しては指定が不要になります。

結果は先ほどと同様、以下の通りになります。

public class MNPerson : NSObject {
    public var name: String
    public var age: UInt
    public var items: [AnyObject]?
    public func hey() -> String
    public init(name: String, age: UInt)
}

ジェネリクスの設定

これでOptionalの設定は完了しましたが、Swiftから使う際に面倒な点が一点残っています。Generated Interfaceを見てみましょう。

public class MNPerson : NSObject {
    public var name: String
    public var age: UInt
    public var items: [AnyObject]?
    public func hey() -> String
    public init(name: String, age: UInt)
}

itemsプロパティがAnyObjectの配列になっています。いちいちキャストするのも面倒です。ここではitemsに格納されているのは文字列だと仮定してジェネリクスの設定を行います。

NS_ASSUME_NONNULL_BEGIN
@interface MNPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger age;
@property (nullable, nonatomic, copy) NSArray<NSString*> *items;
- (NSString*)hey;
- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age;
@end
NS_ASSUME_NONNULL_END

itemsプロパティに対してNSStringを指定しました。Generated Interfaceを見てみます。

public class MNPerson : NSObject {
    public var name: String
    public var age: UInt
    public var items: [String]?
    public func hey() -> String
    public init(name: String, age: UInt)
}

以上のように[String]となっていることが分かります。

このように、実装を確認しながらSwiftから利用しやすいようにしていくことが可能です。

まとめ

コード量的にはそこまでガッツリと書き直さなくてもSwiftから扱いやすいようにインターフェースを修正していくことが可能です。

実装が把握できている場合は、このようなNullabilityやGenericsを指定することで既存のObjective-CのコードをSwiftから使いやすくできることは間違いないと思います。

ただし、これらの置き換えは以外と簡単にできるものではない印象です。理由としては、一つだけnullablenonnullを指定することはできなかったり、それなりに大きなクラスになってくるとnullableなのかnonnullなのか簡単に判断できないことが多くなる印象です。

Objective-Cの場合は基本的にはnullableになると思うのですが、Swiftで利用する際にOptionalとして扱わなければならないため、単純にnullableに置き換えることに大きなメリットは感じません。

それでもOptionalとして扱えるようになることはメリットだと思いますし、、AnyObjectからのキャストが減ることはSwiftのコードを書いていく上でメリットになると思います。

参考

36
31
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
36
31