iOSプログラミングに慣れてくると、複数の画面(ビュー)を遷移(画面遷移)するようなアプリを作り始めることになります。
そうすると、あるビューで作成したデータを違うビューで使いたいような場面も当然出てきます。
このように、複数のビューの間で値を受け渡ししたいという時に困るのが「値渡し」。
正確には遷移先のクラスのプロパティに何らかのデータを渡したい、あるいは遷移元のビューにデータを戻したいという時、その方法について記述してある初心者向けの参考書は少ないものです。
いくつかの参考書では UserDefaults を使ってデータを渡す方法が紹介されているものの、本来 UserDefaults は「永続させるデータ」、つまり一度アプリケーションを終了したあと、再度その時のデータを使って作業を継続するような目的(ゲームで言えばデータの保存)といった目的で使うべきであり、ビュー間の移動においてデータを保存するような目的でつかうようなものではありません。
また、UserDefaults は保存できるデータ型に制限があり、カスタムクラスの保存となると一手間必要です。
それよりも、ビューを遷移する際、遷移先のビュー(ビューのクラス)にあるプロパティに直接値を保存し、遷移先のビューが表示されたタイミングでデータを処理するほうが適切だと言えます。
ここでは、「あるビューから違うビューに Segue を使って移動する時の遷移先のクラスのプロパティ」と、「移動先のビューを破棄(dismiss)する前に遷移元のクラスのプロパティ」にデータを渡す方法を紹介します。
このページではSegueを使った画面遷移における値渡しを紹介します。Navigation Controller でも考え方は同じなのですが、受け渡しの仕方がちょっと違いので、Navigation Controller の例はまた違う機会に紹介します。
遷移先のビューに値を渡す
遷移先のクラスに値を受け取るプロパティを準備する
まず、遷移先のクラス(ここではDetailViewController)に値を渡すためのプロパティ(変数)を宣言しておきます。
1 2 3 4 5 6 |
class DetailViewController: UIViewController { // 遷移元から受け取りたいデータ var attributedText: NSAttributedString! // 以下、必要な処理 } |
この例では、テキストラベルのように、「文字の色や大きさなどの属性が付いたテキスト」を受け取りたいのでNSAttributedStringクラスの変数を作っていますが、Int型などのように基本的なデータ型でも構いません。
注意しなければならないのは、クラスで宣言されたプロパティ(変数)はインスタンス化される前に初期化されている必要があるということです。なので、宣言と同時に何らかの値を代入するか、イニシャライザで未初期化のプロパティに値を代入する必要があります。ここでは、プロパティを有値オプショナル型(Implicitly unwapped optional)、つまり!マーク付きで宣言することで、初期値を nil にしておきます。
もちろん、通常のオプショナル型(?で宣言するオプショナル型)で宣言しても構わないのですが、そうすると使うたびにアンラップする必要があります。
しかし、この値は画面遷移の時に必ず値を設定し、nil になることはあり得ないプロパティなので、アクセスするときにアンラップする必要がない有値オプショナル型として宣言します。
遷移元で受け渡しの準備をする
次に遷移元(ここではViewController)で受け渡しの準備をします。Segue を使った画面遷移の例です。
1 2 3 4 5 6 7 8 9 10 |
@IBAction func copyLabelButton(_ sender: Any) { performSegue(withIdentifier: "detail", sender: nil) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let vc = segue.destination as? DetailViewController { // 遷移先のクラスのプロパティに値を代入する vc.attributedText = eventText.attributedText } } |
この例では、ボタンが押された際に detail と名付けられた Segue を実行する例を示しています。
Segue を実行する際は performSegue(withIdentifier:sender:) を使って遷移しますが、この段階では遷移先のビュー(のインスタンス)はまだ生成されていません。
インスタンスが生成されていないということは、当然プロパティも存在しません。
では、いつ値を書き込むのかというと、prepare(for:sender:) が呼び出された時に書き込みます。
ここでは、単一の値を書き込んでいる(代入している)だけの例ですが、複数のプロパティがある場合には、必要な分だけ書き込みます。
注意しなければいけないのは、UI部品(ボタンやラベルなど)のインスタンスはこの段階ではまだ生成されていないという点です。もし、ラベルにテキストを表示したいという場合には、いったん仮のプロパティに値を代入し、遷移先で改めて処理を行う必要があります。
遷移先でUI部品などの値をセットする
遷移先のUI部品、あるいは新たにインスタンスを作る必要があるクラスの場合には、適切なタイミングで値を書き込みます。
UI部品であれば、viewDidLoad() の段階では部品のインスタンスができていますので、ここで改めて書き込みます。
遷移先のクラス(ここではDetailedViewController)でその処理を行います。
1 2 3 4 5 6 |
override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. detailTextView.attributedText = attributedText } |
もちろん、viewDidLoad()である必要はありませんが、いずれにしても適切なタイミングでプロパティをセットしなければいけません。
遷移元のクラスにデータを渡す
遷移先のクラスでの処理
遷移元のクラスにデータを戻す場合でも考え方は同じです。
遷移元のクラスに適切なプロパティを準備しておき(詳細は省きます)、「戻る」処理を行う際に遷移元のプロパティに値をセットします。
1 2 3 4 5 6 7 8 |
@IBAction func backButton(_ sender: Any) { if let vc = presentingViewController as? ViewController { vc.detailedText = detailTextView.attributedText // 移動先のUI部品のインスタンスは残っているので、直接セットすることもできる vc.eventTextView.detailedText = detailTextView.attributedText } dismiss(animated: true, completion: nil) } |
ここでは、あるボタンを押した時に、現在表示しているビューを破棄する例を示します。ここで、遷移元のビューのプロパティに値をセットしています。
前述の 呼び出し元のクラスは presentingViewControllerというプロパティにセットされています。ただし、クラスは UIViewController なので、遷移元のクラスとしてダウンキャスト(as?演算子)を行い、その後プロパティを書き込んでいます。
呼び出し時と違い、呼び出し元のビューのインスタンスはのこっていますので、直接UI部品のプロパティをセットすることも可能です。
遷移元のクラスでの処理
基本的にこれ以上行う処理はありません。
しかし、遷移先の画面で行われた処理をもとに呼び出し元のUI部品の状態を変えるような場合はよくあります。
そのような場合の処理は、呼び出し元(ここではViewController)の viewWillAppear() などで行います。
1 2 3 4 5 6 7 |
override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // この程度の処理であれば呼び出し元で処理してもいい if detailedText != nil { eventText.attributedText = detailedText! } } |
ラベルのプロパティのように、単一の値のセットであれば、呼び出し元でセットすることも可能です。しかし、多くの処理が必要な場合は、呼び出し元で値をセットすることも、見通しの上では良いのかもしれません。
気をつけなければいけないのは、呼び出し元に戻った際の処理は viewWillAppear() に記述するという点です。呼び出し元のビューのインスタンスは残ったままなので、呼び出し先から戻っても viewDidLoad() は呼び出されないという点に注意が必要です。