LoginSignup
43
44

More than 5 years have passed since last update.

指定イニシャライザを確実に呼ばせる方法

Last updated at Posted at 2015-01-12

概要

よくUIKitにあるクラスのサブクラスとしてカスタムビューなどを作る際に、既存のイニシャライザではなく新たな指定イニシャライザを追加することがあるかと思います。
しかし、これを利用してイニシャライズされることを前提としていると利用者が既存のイニシャライザを使ってしまうことでバグの原因になる可能性があります :sweat:
こんな時は、指定イニシャライザ以外のイニシャライザを呼ばないようにすればいいんです!

※ Storyboardを経由してinitWithCoderを呼んでしまう場合は回避できませんでした。
@dokubeko さんコメントでの指摘ありがとうございます :tada:

アプローチ

SwiftにもObjective-Cにもattributeというコンパイラディレクティブがあります。
これにunavailableという属性があるのでこれを指定します。
例えばUIViewControllerのサブクラスであるViewControllerクラスの指定イニシャライザを必ず呼ばせたい場合

for Swift

//ViewController.swift
import UIKit

class ViewController: UIViewController {

    //properties
    var identifier:NSString?

    //呼ばせたい指定イニシャライザ
    init(identifier:NSString) {
        self.identifier = identifier
        super.init()
    }

    //呼ばせたくない既存のイニシャライザ
    @availability (*, unavailable, message="代わりにinit(identifier)を使いましょう")
    override init() {super.init()}
    @availability (*, unavailable, message="代わりにinit(identifier)を使いましょう")
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
        super.init()
    }
    @availability (*, unavailable, message="代わりにinit(identifier)を使いましょう")
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

for Objective-C

//ViewController.h
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

//必ず呼ばせたい指定イニシャライザ
- (instancetype)initWithIdentify:(NSString *)identify;

//呼ばせたくない既存のイニシャライザ
- (instancetype)init
    __attribute__((unavailable("代わりにinitWithIdentifyを使いましょう")));
- (instancetype)initWithCoder:(NSCoder *)aDecoder
    __attribute__((unavailable("代わりにinitWithIdentifyを使いましょう")));
- (instancetype)initWithNibName:(NSString *)nibNameOrNil
                         bundle:(NSBundle *)nibBundleOrNil
    __attribute__((unavailable("代わりにinitWithIdentifyを使いましょう")));
+ (instancetype)new
    __attribute__((unavailable("代わりにallocした後initWithIdentifyを使いましょう")));

@end
//ViewController.m
#import "ViewController.h"

@interface ViewController ()
@property (copy, nonatomic) NSString *identify;
@property (copy, nonatomic) NSString *name;
@end

@implementation ViewController


- (instancetype)initWithIdentify:(NSString *)identify
{
    self = [super init];
    if(!self){
        return self;
    }
    self.identify = identify;
    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
}
@end

結果

for Objective-C

補完時に使えないことを明示してくれるし、それを強行突破してもビルドエラーになる。

Objective-Cで既存のinitファミリが呼べない図

Objective-Cで既存のnewメソッドが呼べない図

強行突破してもビルドエラーにObjCのnew

for Swift

そもそも補完時に候補として出てこなくなるし、当然ビルドも失敗する。

補完の候補として出てこないSwift

強行突破してもビルドエラーに swiftのinit

まとめ

Swiftでは特にコードがちょっと汚くなるような気がするけど、絶対に呼んでほしい指定イニシャライザだけ呼ばれる というメリットは大きいと思います。
バグの防止にもなるので是非使ってみてはいかがでしょうか。

One more thing

今後指定イニシャライザをさらに追加した場合に、そのイニシャライザに必ず経由させたい指定イニシャライザがある場合は、NS_DESIGNATED_INITIALIZER というマクロをつけるといいです。
この場合は警告しか出ずビルドは通ってしまいますが補助的効果は十分にあると思います。
※ ここで紹介できるのはObjective-Cの場合のみで、Swiftでのやり方がわかる方はコメントいただけると助かります。

//ViewController.h
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

//必ず呼ばせたい指定イニシャライザ
//下記の指定イニシャライザは必ず経由させたい
- (instancetype)initWithIdentify:(NSString *)identify NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithName:(NSString *)name;

//呼ばせたくない既存のイニシャライザ
- (instancetype)init
    __attribute__((unavailable("代わりにinitWithIdentifyを使いましょう")));
- (instancetype)initWithCoder:(NSCoder *)aDecoder
    __attribute__((unavailable("代わりにinitWithIdentifyを使いましょう")));
- (instancetype)initWithNibName:(NSString *)nibNameOrNil
                         bundle:(NSBundle *)nibBundleOrNil
    __attribute__((unavailable("代わりにinitWithIdentifyを使いましょう")));
+ (instancetype)new
    __attribute__((unavailable("代わりにallocした後initWithIdentifyを使いましょう")));
//ViewController.m
#import "ViewController.h"

@interface ViewController ()
@property (copy, nonatomic) NSString *identify;
@property (copy, nonatomic) NSString *name;
@end

@implementation ViewController


- (instancetype)initWithIdentify:(NSString *)identify
{
    self = [super init];
    if(!self){
        return self;
    }
    self.identify = identify;
    return self;
}

- (instancetype)initWithName:(NSString *)name
{
    self = [super init];
    if(!self){
        return self;
    }
    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
}
@end

下図のように、[super init]で初期化すると警告が表示されます。
ここを [self initWithIdentify:@"nameId"]などとしてinitWithIdentifyメソッドで初期化すると警告は消えます。

指定イニシャライザ経由でないとWarning

警告消去

43
44
1

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
43
44