iOSアプリ開発でよく利用されるデリゲート
iOSアプリ開発では、Objective-C/Swiftに共通する概念としてデリゲートがあります。デリゲートとは、他のオブジェクトから利用されるためのメソッドのことです。特定のオブジェクトから利用されるデリゲートのメソッドをまとめたものがプロトコルで、プロトコルはクラスとして存在します。iOSアプリ開発で最もつまづきやすい点が、このデリゲートを利用する部分です。デリゲートの概念の整理も兼ねて、デリゲートとプロトコルの具体的な利用例をサンプルを用いて説明します。
デリゲートを利用する手順
Swiftでは入力欄やピッカーなどのUI部品において、あらかじめプロトコルとしてのクラスが用意されており、UI部品の動作に応じたデリゲートのメソッドがそろっています。そのため、UI部品の決まった動きを利用するだけであれば、既存のデリゲートの処理内容のみを記述することで、アプリの動きを実装できます。ビューコントローラーに配置した、UI部品のデリゲートを利用する手順を簡単にまとめると次の図のようになります。
- ビューコントローラーでUI部品のプロトコルを実装し、デリゲートを利用できるようにします。
- UI部品側からはdelegateプロパティで利用する、デリゲートを実装したビューコントローラーを指定します。
- デリゲートのメソッドをオーバーライドして処理内容を記述します。
- アプリがUI部品の動作に応じた処理内容を行うことを確認します。
iOSにおける、UI部品の画面をタッチした/スクロールした/選択したなどの動きは、すべてこのデリゲートを利用して実装します。サンプルで実際に確認してみます。
UITextFieldDelegateプロトコルを利用して入力欄の入力開始/終了の処理を行う
入力欄を作成するUITextFieldクラスとUITextFieldDelegateプロトコルを使い、既存のデリゲートの動きを確認します。先述した手順の1から3を実装してみます。
class TextFieldViewController: UIViewController { @IBOutlet weak var textField: UITextField! override func viewDidLoad() { super.viewDidLoad() // 利用するデリゲートを指定 self.textField.delegate = self // ------(2) } } // UITextFieldDelegateを実装 extension TextFieldViewController: UITextFieldDelegate { // ------(1) // 編集開始時の処理 func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { // ------(3) // UItextFieldの枠を赤に textField.layer.borderColor = UIColor.red.cgColor textField.layer.borderWidth = 1.0 return true } // リターンキーを押した際の処理 func textFieldShouldReturn(_ textField: UITextField) -> Bool { // ------(3) // UItextFieldをアンフォーカス textField.resignFirstResponder() // 枠の太さを0に textField.layer.borderWidth = 0.0 return true } }
サンプルでは前節の手順通りに実装しています。
アプリを起動すると次の動きが確認できます。
- (1)エクステンションを使ってUITextFieldDelegateプロトコルをビューコントローラーに実装します。
- (2)UITextFieldオブジェクトのdelegateプロパティで、UITextFieldDelegateプロトコルにビューコントローラーであるselfを指定します。
- (3)デリゲートのメソッドをオーバーライドして処理内容を記述します。編集開始時にレイヤーの枠を赤にして太さを1.0にします。リターンキーを押した際には入力欄からフォーカスを外し、レイヤーの枠の太さを0にします。
既存のデリゲートの処理では、このようにUI部品のクラスとペアになったプロトコルを利用してUI部品の動作時の処理を行います。
デリゲートを自作する【Swift 3】
前節のサンプルの通り、デリゲートは呼び出しと処理内容を任意で指定できる非常に汎用性の高い機能です。デリゲートは、クラスと同様に開発者が任意に作成することができます。Swift 3以降は、次の2点によってデリゲートをより容易に扱えるようになりました。
- プロトコルの宣言時に、必ずしも親クラスのNSObjectProtocolクラスを継承する必要がない。
- デリゲートのメソッドを実行する際に、実装関係やメソッドの存在確認の必要がなくなった。
サンプルを作成しながら具体的に説明します。
作成するデリゲートと処理の流れ
下図の通り、モーダルで表示したテーブルメニューから選択された項目を元の画面に表示するサンプルを作成します。
「テーブルで選択された項目を元画面に表示する処理」をデリゲートを通して行います。
プロトコルを宣言
デリゲート/プロトコルを作成する場合は、クラスやメソッドを作成する際と同様に任意の名前を利用することができます。今回は選択された項目を渡すだけなので、MenuTableViewControllerクラスを同じファイル内で次の通りに宣言します。
protocol MenuDelegate { func didSelect(_ word:String) }
Swiftの仕様には、プロトコルの親クラスに相当するNSObjectProtocolクラスと呼ばれるクラスが存在します。以前は、プロトコルの宣言時にNSObjectProtocolクラスを継承しないと、プログラムが正常に動作しませんでした。Swift 3以降は「protocol」の宣言があれば、それだけでコンパイラはプロトコルとして解釈して実行します。NSObjectProtocolクラスの機能を利用しない単純なプロトコルであれば、サンプルのように簡易的に宣言できます。
サンプルではMenuDelegateという名前でプロトコルを宣言しています。MenuDelegateプロトコルの中で、項目が選択された際に実行されるメソッドをdidSelect(_:)の名前で定義します。引数は文字列型の変数です。デリゲートなのでメソッドの内容は記述しません。
delegateプロパティで作成したプロトコルを指定
作成したプロトコルをdelegateプロパティでアクセスできるようにします。今回はMenuTableViewControllerオブジェクトがプロトコルを持つので、MenuTableViewControllerクラス内にdelegateプロパティを設け、型をMenuDelegateプロトコルに指定します。
class MenuTableViewController: UITableViewController { var delegate: MenuDelegate? ...後略...
デリゲートを使わない場合も考えられるため、後ろに「?」をつけてnullを許可するオプショナル型として宣言します。オプショナル型として宣言することで、実行時にプロトコルを実装済み、もしくはメソッドが存在するか判定する処理は不要となります。もし仮にプロトコルが未実装/メソッドが存在しないということがあっても、オプショナル型で宣言しておけばエラーでプログラムが止まる事態を回避できます。
デリゲートを実行する
MenuTableViewControllerクラス内では、delegateプロパティを設けるだけでなく、デリゲートを実行できるようにします。サンプルではテーブルのセルが選択された後に、プロトコル内のdidSelect(_:)メソッドを実行してセルの項目を渡します。
// テーブルのセル選択時の処理 override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { // IndexPathからセルを取得 let cell = tableView.cellForRow(at: indexPath) // ------(1) if let text = cell?.textLabel?.text { self.delegate?.didSelect(text) // ------(2) } // モーダルを閉じる self.dismiss(animated: true, completion: nil) }
テーブルのセルが選択された際には、UITableViewDelegateプロトコルのtableView(_:didSelectRowAt:)メソッドが呼ばれます。プロトコル内のdidSelect(_:)メソッドを記述するのはtableView(_:didSelectRowAt:)メソッドの中です。
選択されたセルは、tableView(_:didSelectRowAt:)メソッドで渡されるIndexPathオブジェクトから得られます(1)。得られたセルはtitleLabelの値が項目名なので、この値をdidSelect(_:)メソッドに渡して実行します(2)。これでモーダルで表示されるメニュー側の処理は終わりです。
プロトコルを実装する
作成したMenuDelegateプロトコルをビューコントローラーに実装します。前節のサンプルと同様に、エクステンションを利用します。
extension BaseViewController: MenuDelegate { func didSelect(_ word: String) { self.label.text = "\(word)が選択されました!" } }
実装したプロトコルの中で、メソッドをオーバーライドして処理内容を記述します。ここではメソッドに渡された項目をラベルに表示する処理を行っています。実装したプロトコルを利用するためには、delegateプロパティでプロトコルを実装した自分自身を指定することを忘れないでください。
@IBAction func onPressMenu(_ sender: Any) { // Main.storyboardを取得 let storyBoard = UIStoryboard(name: "Main", bundle: nil) if let vc = storyBoard.instantiateViewController(withIdentifier:"MenuTableViewController") as? MenuTableViewController { vc.delegate = self // delegateを指定 self.present(vc, animated: true, completion: nil) } }
MenuTableViewControllerクラスのインスタンスを生成する際に、delegateプロパティでselfを指定します。サンプルを実行すると次の動作が確認できます。
まとめ
今回はSwiftでよく利用される画面上の処理と、それに関わる新しい文法を紹介しました。画面間でのデータのやり取りはアプリを設計する際にも非常に重要です。次回は、今回解説した方法以外の動的なプロパティの値の変更手段や、クロージャを使った画面上の処理について説明する予定です。