ObjecTips

Swift & Objective-C で iOS とか macOS とか

IBSegueAction の使い方

Xcode 11から IBSegueAction が追加された。利用可能なOSは iOS 13, macOS 10.15以降

この新機能は Segue で接続し呼び出す ViewController への必須パラメータ渡しの点でメリットがある。

既存実装

prepare(for:sender:) だと

func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if let viewController = segue.destination as? MyViewController {
        viewController.text = "example"
    }
}

という様に、渡ってくる ViewController は init?(coder: NSCoder) で既に初期化済のインスタンスで、プロパティ経由でパラメータ渡しを行うしかなかった。
そのためパラメータは private にする事ができず internalpublic にする必要があり、また外部から変更可能な var にする必要があった。

Optional の場合は以下

class DestinationViewController: UIViewController {
    var text: String?
}

Non Optional の必須パラメータの場合は以下の様に ImplicitlyUnwrappedOptional で実装する事になる。

class DestinationViewController: UIViewController {
   var text: String!
}

これに加えて ViewController 側での値変更がどのタイミングで呼ばれても良い様に考慮すると(一例としては)以下の様な実装になる。

class DestinationViewController: UIViewController {
    
    @IBOutlet weak var label: UILabel!
    var text: String? {
        didSet {
            updateLabel()
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        updateLabel()
    }
    
    private func updateLabel() {
        if isViewLoaded {
            label.text = text
        }
    }
IBSegueAction

@IBSegueAction attributes を使うと、IBで接続と呼び出しが可能な ViewController を返すメソッドを定義でき、任意のイニシャライザを使って ViewController を作成する事ができる。

class SourceViewController: UIViewController {

    @IBSegueAction
    func makeDestinationViewController(coder: NSCoder, sender: Any?, segueIdentifier: String?) -> DestinationViewController? {
        return DestinationViewController(coder: coder, text: "example")
    }
}

ViewController 側のパラメータは private にする事が可能で let にする事もできる。
初期化に必要なパラメータが変更不要で外部公開不要なプライベートな変数な場合、その役割通りに正しく宣言できる。

class DestinationViewController: UIViewController {

    @IBOutlet weak var label: UILabel!
    private let text: String
    
    init?(coder: NSCoder, text: String) {
        self.text = text
        super.init(coder: coder)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        label.text = text
    }
}
利用方法

IBでの設定はちょっと分かり辛い。
まずこれまでのIBと同じ様に Button などから別の ViewController へ Segue での画面遷移を設定する。
作成した Segue を選択しインスペクタの一番右のタブに切り替えると instantiation という項目がXcode 11では増えており、ここから Control+Drag で ViewController に線を伸ばすとコードで定義した makeDestinationViewControllerWithCoder を選択し接続する事ができる様になっている。

まとめ

外部に出さない変数や変更しない変数をその通りに正しく宣言する事ができるのでGood。
大した事ではないけど独自イニシャライザの宣言によって、既存のイニシャライザ required init?(coder: NSCoder) の空実装が必要な点だけちょっと面倒。


追記

ViewController をカスタムイニシャライザで作成可能にするためのメソッドが UIStoryboard にも追加されていた。

https://developer.apple.com/documentation/uikit/uistoryboard/3213988-instantiateinitialviewcontroller

https://developer.apple.com/documentation/uikit/uistoryboard/3213989-instantiateviewcontroller

使い方は以下の様になる

let viewController = storyboard.instantiateInitialViewController { (coder) -> DestinationViewController? in
    return DestinationViewController(coder: coder, text: "example")
}
let viewController = storyboard.instantiateViewController(identifier: "identifier") { (coder) -> DestinationViewController? in
    return DestinationViewController(coder: coder, text: "example")
}

UIStoryboard からインスタンス化する場合も同じく外部に出さない変数や変更しない変数をその通りに正しく宣言する事ができるのが肝だと思う。