Storyboardから生成したインスタンスで外部指定された定数を扱う

Xcode

StoryboardからViewControllerをインスタンス化 > 遷移する場合に、letプロパティを用いて状態を引き継ぐ方法を検討します。

次の理想の状態を作りたいです。

理想の状態

  • 遷移元で状態(property)を指定してViewControllerのインスタンスを生成する
  • ViewControllerを遷移させる
  • 遷移先のViewControllerは指定された状態をletで保持している

具体例を使って、理想の状態をもう少し具体的に表現します。

理想の状態の具体例

入力した文字(名前)から画像を作るアプリで考えてみます。

遷移元のTextFieldから名前を入力(TextFieldの確定で画面遷移)し、遷移先でテキストから画像を生成させます。

遷移元でできること / やること(NamaInputViewController)

  • 文字の入力 / 確定
  • 入力文字の受け渡し

遷移先でできること / やること(NameImageMakeViewController)

  • 入力文字の受け取り
  • 文字の画像変換

動作例

入力した文字の受け渡しを考える

文字を画像へ変換するアプリの例を使って、画面間での文字の受け渡し方法について、ここから考えます。

NamaInputViewControllerからNameImageMakeViewControllerへの文字の受け渡しについて、次の順で考えて定数で扱う方法へ終着させます。

  • 変数を使った受け渡し
  • 定数を使った受け渡し(固定値)
  • 定数を使った受け渡し(変動値)

定数を使った受け渡しの固定値と変動値は、TextFieldに入力した値の使用の有無の差になります。

変数で扱う例

「名前の入力フォームで文字を入力」と「お名前画像が表示される」は、それぞれ別の画面で行われます。

はじめに1番単純な変数を使った、入力文字の受け渡しを実装してみます。

(受け渡しに扱う変数はreceivedText)

NamaInputViewControllerのnameTextFieldに名前を入力する

class NamaInputViewController: UIViewController, UITextFieldDelegate {
   @IBOutlet private var nameTextField: UITextField!
   
   override func viewDidLoad() {
       super.viewDidLoad()
       nameTextField.delegate = self
   }
   
   func textFieldShouldReturn(_ textField: UITextField) -> Bool {
       nameTextField.resignFirstResponder()
       // 引数に入力した名前を指定してNameCheckViewControllerへ渡す
       let nextViewController = NameCheckViewController.instantiate(receivedText: textField.text)
       navigationController?.pushViewController(nextViewController, animated: true)
       
       return true
   }
}

NameImageMakeViewControllerで文字の画像を生成表示する

class NameCheckViewController: UIViewController {
   private var receivedText: String?
   @IBOutlet private var nameImageView: UIImageView!
   @IBOutlet private var nameLabel: UILabel!
   
   static func instantiate(receivedText: String?) -> NameCheckViewController {
       let sbName = "NameCheckViewController"
       let storyboard = UIStoryboard(name: sbName, bundle: nil)
       let vc = storyboard.instantiateViewController(withIdentifier: sbName) as! NameCheckViewController
       // NamaInputViewControllerで入力した名前を反映する
       vc.receivedText = receivedText ?? ""
       return vc
   }
 
   override func viewDidAppear(_ animated: Bool) {
       super.viewDidAppear(animated)
       
       nameLabgeView.image = UIImage.imageFromText(text: receivedText ?? "名無し")
   }
}

問題点

グローバルな変数を介しての、値の受け渡しはとても単純で簡単です。

しかし、変数であるが故に、書き換えが可能になってしまいます。

問題の例

class NameCheckViewController: UIViewController {
// ~~ 省略 ~~    
   static func instantiate(receivedText: String?) -> NameCheckViewController {
       let sbName = "NameCheckViewController"
       let storyboard = UIStoryboard(name: sbName, bundle: nil)
       let vc = storyboard.instantiateViewController(withIdentifier: sbName) as! NameCheckViewController
       // NamaInputViewControllerで入力した名前を反映する
       vc.receivedText = receivedText ?? ""
       return vc
   }
 
   override func viewDidLoad() {
       super.viewDidLoad()
       
       receivedText = "名無し"
   }
 
   override func viewDidAppear(_ animated: Bool) {
       super.viewDidAppear(animated)
       
       nameLabel.text = receivedText
       nameImageView.image = UIImage.imageFromText(text: receivedText ?? "名無し")
   }
}

例ではreceivedTextを参照する前に、間違って初期化してしまったケースです。

この場合NamaInputViewControllerで、入力さえれた名前が名無しで書き換えられてしまいます。

このような問題を防ぐために、受け渡しに扱う値を定数で扱いたいです。

定数で扱う例

定数で扱う場合は、指定する値が固定の場合と変動の場合で変わります。

固定値と変動値について、今回の例に当てはめますと、次のようなイメージです。

  • 固定値 : TextFieldの入力値は無視して、常に「名無し」を渡す
  • 変動値 : TextFieldの入力値を渡す

ここからは、固定値のケースを実装した後に、変動値の機能を追加する方法で見ていきます。

①指定する値が固定値の場合

コードで示すと次の実装になります。

class NameCheckViewController: UIViewController {
   private let receivedText: String?
   @IBOutlet private var nameImageView: UIImageView!
   @IBOutlet private var nameLabel: UILabel!

   required init?(coder aDecoder: NSCoder) {
     self.receivedText = "名無し"
     super.init(coder: aDecoder)
   }
   
   static func instantiate() -> NameCheckViewController {
     let storyboard = UIStoryboard(name: "NameCheckViewController", bundle: nil)
     let viewController = storyboard.instantiateInitialViewController()
     
     return viewController as! NameCheckViewController
   }
   // 以下省略
}

ポイントはinstantiateInitialViewControllerになります。次の流れを抑えておきます。

参考 apple documentationhttps://developer.apple.com/documentation/appkit/nsstoryboard/3203779-instantiateinitialcontroller

  1. Storyboardの読み込み
    • 初期ビューコントローラの情報(Storyboardファイルがバイナリ形式)で保存。
  2. Storyboardファイルのデコード
    • instantiateInitialController()にて初期ViewControllerの情報の情報をデコード。
  3. イニシャライザの呼び出し
    • デコードされた情報を使って、ViewControllerのインスタンスを生成。
    • init?(coder aDecoder: NSCoder) イニシャライザを呼び出し。
  4. 固定値の情報(名無し)で定数を更新
    • self.receivedText = "名無し"で定数の値を指定。

②指定する値が変動の場合

固定値で定数の更新を把握したため、今度は’変動’外部から指定する)値で、更新する方法を確認します。

class NameCheckViewController: UIViewController {
   private let receivedText: String?
   @IBOutlet private var nameImageView: UIImageView!
   @IBOutlet private var nameLabel: UILabel!
   
   private init(receivedText: String?, coder: NSCoder) {
       self.receivedText = receivedText
       super.init(coder: coder)!
   }
 
   required init?(coder aDecoder: NSCoder) {
       fatalError("init(coder:) has not been implemented")
   }
   
   static func instantiate(receivedText: String?) -> NameCheckViewController {
       let storyboard = UIStoryboard(name: "NameCheckViewController", bundle: nil)
       let viewController = storyboard.instantiateInitialViewController { coder in
           return NameCheckViewController(receivedText: receivedText, coder: coder)
       }
       return viewController!
   }
   // 以下省略
}

今回はキーボードで入力した名前をinstantiateの引数receivedTextを介して、指定させたいです。

引数で受け取った値を実際に、定数に指定するためにinstantiateInitialController(creator:)を使用してViewControllerのインスタンスを生成します。

固定の場合では、Storyboardファイルのデコードを実施後、そのままイニシャライザの呼び出しでした。

しかし、変動の場合では以下のように少し処理の流れが変わります。

Storyboardファイルのデコード

  • instantiateInitialController(creator:)にて初期ViewControllerの情報の情報をデコードプロセスを開始。
  • デコードプロセス中に creator クロージャを呼び出し。
  • creator クロージャへ NSCoder オブジェクト(UINibDecoder のインスタンス)が渡す。

creator クロージャの実行

  • 渡された NSCoder オブジェクトを使用して、初期ViewControllerの情報の情報をデコード。
  • creator クロージャ内で、init?(coder aDecoder: NSCoder) イニシャライザを呼び出し。
  • creator クロージャ内で、インスタンスを生成。

クロージャーを介すことで、ViewControllerのインスタンス生成時にinstantiate(receivedText: String)の引数の値を、receivedTextへ反映することが可能になります。

以上の対応で、変数を使わない且つプロパティをローカル変数に保った状態で、外側から値の指定ができる状態になります。

コメント

タイトルとURLをコピーしました