LoginSignup
1

More than 3 years have passed since last update.

posted at

updated at

Organization

Swift5でちょっと安全になったAccess Control

こんにちは。 Swift Advent Calendar 11日目担当の @sato-shin です。

今日は、Objective-C で Protocol 宣言されたオプショナルなメソッドを呼び出すときの、Swift5 と Swift4系 の挙動の違いから、Access Controlがちょっと安全になったよというお話です。

SwiftでProtocolを宣言し、使ってみる

foo()をもつFooProtocolを定義します。なお、foo()にはデフォルト実装が存在しています。

protocol FooProtocol {
    func foo()
}
extension FooProtocol {
    func foo() {}
}

このFooProtocolに適合するBarクラスを定義します。

class Bar: FooProtocol {
   func foo() {}
}

この時、 Bar に実装する foo() の Access Control を private や fileprivate にするとコンパイルできなくなります。

class Bar: FooProtocol {
   // ErrorMsg: Method 'foo()' must be declared internal because it matches a requirement in internal protocol 'FooProtocol'
   private func foo() {}
}

Objective-CでProtocolを宣言し、使ってみる

では次に、 FooProtocol と同じような使い方ができるObjcFooProtocol を作ってみます。
FooProtocolではfoo()のデフォルト実装が存在したため、適合側でfoo()の実装は任意でした。
そこで、ObjcFooProtocolでも同じ使い方ができるようにするため、foo()はオプショナルであると宣言します。

ObjcFooProtocol.h
@protocol ObjcFooProtocol <NSObject>

@optional
- (void)foo;

@end

ObjcFooProtocol に適合する、Bar クラスを swift で実装すると以下のようになります。

class Bar: NSObject, ObjcFooProtocol {
   func foo() {}
}

Swift4とSwift5の挙動の違い

それでは、foo()を private にし、Swift4 と Swift5 でコンパイルしてみましょう。
すると、Swift4ではコンパイルが成功し、Swift5ではコンパイルが失敗します。

Bar.swift
class Bar: NSObject, ObjcFooProtocol {
   // Swift4系ではコンパイルが成功する
   //
   // Swift5だとコンパイルが失敗する
   // ErrorMsg: Method 'foo()' must be as accessible as its enclosing type because it matches a requirement in protocol 'ObjcFooProtocol'
   private func foo() {}
}

このようにSwift5では、Swiftから見るObjective-CのProtocolに適合させるさいにprivateやfileprivateが許されなくなりました。

この変更により何がよくなったのか?

これによりDelegate実装のミスをなくすことができるようになりました。
どうミスが無くなるのか説明していきます。

プレゼントを届けることのできないサンタさん

以下のコードでは、ObjcSantaClaus, Daddyの二人が登場し、
サンタさんの代わりにパパさんがプレゼントを届けるということを表現しています。

ObjcSantaClausdeliveryHappyItem()をすると、
ObjcSantaClausDelegateに適合するオブジェクトのputGift()に動作が委譲されています。

DaddyObjcSantaClausDelegateに適合しており、ObjcSantaClausの代わりにプレゼントを届けることができます。

ObjcSantaClaus.h
#import <Foundation/Foundation.h>

@protocol ObjcSantaClausDelegate <NSObject>

@optional
- (void)putGift;

@end


@interface ObjcSantaClaus : NSObject

@property (weak, nonatomic) id <ObjcSantaClausDelegate> delegate;

- (void)deliveryHappyItem;

@end
ObjcSantaClaus.m
#import "ObjcSantaClaus.h"

@interface ObjcSantaClaus ()
@end

@implementation ObjcSantaClaus

- (void)deliveryHappyItem {
    if ([self.delegate respondsToSelector:@selector(putGift)]) {
        [self.delegate putGift];
    }
}

@end
Daddy
class Daddy: NSObject, ObjcSantaClausDelegate {
    func putGift() {
        print("put a gift!")
    }
}
プレゼントを届ける
let santa = ObjcSantaClaus()
let daddy = Daddy()
santa.delegate = daddy
santa.deliveryHappyItem() // put a gift! と表示される

これは正常に動作し、 put a gift! と表示され、無事にプレゼントが届きました。

問題

このとき、DaddyクラスのputGift()を privateに変更したらどうなるでしょうか?

class Daddy: NSObject, ObjcSantaClausDelegate {
    private func putGift() {
        print("put a gift!")
    }
}

答え

  • Swift4系ではコンパイルが成功するが、実行してもDaddyputGift()が呼ばれない。
  • Swift5ではコンパイルが失敗する。

です。

なぜか

DaddyputGift()を実装していても、private宣言をされていると、
SantaClausクラス側の [self.delegate respondsToSelector:@selector(putGift)]putGift()へアクセスできないので、このようなことが起こります。

これで、Swift4ではObjcSantaClausがプレゼントを届けようとしてもプレゼントが届かないコードの完成です。
Swift5ではコンパイルが失敗し、上記のようなミスをできないので安全になりました。

まとめ

Swift5からは、Objective-Cによって定義されたprotocolに適合するよう実装するときにprivate, fileprivateを指定できなくなったため、
Swift4では気づきづらかった、 Delegate におけるの Access Control のミスをコンパイル時に見つけることができるようになりました。

蛇足

サンタさんはいます。25日にはサンタさんを追跡しましょう。
https://santatracker.google.com/

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
What you can do with signing up
1