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 documentation:https://developer.apple.com/documentation/appkit/nsstoryboard/3203779-instantiateinitialcontroller
- Storyboardの読み込み
- 初期ビューコントローラの情報(Storyboardファイルがバイナリ形式)で保存。
- Storyboardファイルのデコード
instantiateInitialController()
にて初期ViewControllerの情報の情報をデコード。
- イニシャライザの呼び出し
- デコードされた情報を使って、ViewControllerのインスタンスを生成。
init?(coder aDecoder: NSCoder)
イニシャライザを呼び出し。
- 固定値の情報(名無し)で定数を更新
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
へ反映することが可能になります。
以上の対応で、変数を使わない且つプロパティをローカル変数に保った状態で、外側から値の指定ができる状態になります。
コメント