swiftのdelegateで挫折する理由


はじめに

C言語で言うところのポインタの様に,初心者が躓きやすいポイントというものがどの言語にもあると思います.

swiftではdelegateが躓きポイントとしてよく挙げられます.僕は躓くどころか床を舐めました.

本記事では以下の順に説明します.


  1. delegateがなぜ分かりにくいか

  2. delegateのサンプル

  3. よく見かけるTextFieldのdelegateについて


対象

本投稿では,以下の人を対象にしています.


  • delegateで挫折した人

  • delegateで戦意喪失した人

  • なんとなくは分かるけど説明はできない人

  • サンプルコードが書けって言ってるから書いてる人

  • 意味がわからないまま以下の様にdelegateを書いている人


よくあるdelegateの例.swift

class MyClass: UIViewController, UITextFieldDelegate{

@IBOutlet weak var myTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
myTextField.delegate = self
}

// Enter keyを押したときに実行される
func textFieldShouldReturn(_ textField:UITextField) -> Bool {
// 独自処理 キーボードを閉じる
self.view.endEditing(true)
return true
}
}



delegateとは

こういうものは定義から書くのが常なので,定義から書くと,delegateとは操作の一部他のクラスに代替させるデザインパターンです. なるほど.よく分からん!

delegateを翻訳すると「代理人」や「委任する」になります.

ここでは,「delegateなるものを使うと処理を他に任せることができる」と覚えておけば問題ありません.


protocolとは

delegateを語る上で外せないのがprotocolです.

protocolとは,クラスが定義すべきメソッドを定めたものです.

みんな大好きTCP/IPやHTTPSもprotocolの一種です.

HTTPS(Hypertext Transfer Protocol Secure)を例にすると,相手のWebサーバの実態が分からなくても,HTTPSというprotocolさえ分かっていれば,URLという入力を与えるだけで,Safariだろうが,Chromeだろうが,IEだろうが,一般的なWebブラウザから本記事を表示することができます.

このとき,ポート番号は何番で,ルーティングはこうして,暗号化はこの方式で,ルート証明書はどこで,ロードバランサではどこに振り分けられて・・・なんて考える必要はありません.

とりあえずは,「protocolを使って,処理の依頼方法と実装方法を定義する」と思ってください.


delegateがなぜ分かりにくいのか

delegateがなぜ分かりにくいのかというと,僕は以下の2つの理由があると考えています.


1. 初心者が初めに出会うdelegateはほとんど実装されていて全体像が見えない

delegateを構成する要素としては以下の3つがあります.


  • 処理を依頼するクラス

  • protocol(=依頼方法)

  • 処理を実行するクラス(=処理を依頼されるクラス)

ここで,冒頭のTextFieldのdelegateを見てみましょう.

Enter keyがタップされたときにキーボードが閉じるように,TextFieldのdelegateを実装しています.


EnterKeyが押されると閉じる例.swift

class MyClass: UIViewController, UITextFieldDelegate{

@IBOutlet weak var myTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
myTextField.delegate = self
}

// Enter keyを押したときに実行される
func textFieldShouldReturn(_ textField:UITextField) -> Bool {
// 独自処理 キーボードを閉じる
self.view.endEditing(true)
return true
}
}


実は,この中にdelegateを構成する3つの要素が全て含まれています.

構成要素
クラス

処理を依頼するクラス
UITextField(myTextField)

protocol
UITextFieldDelegate

処理を実行するクラス
MyClass

サンプルに書いた中で自分で書く要素は,処理を実行するクラスのMyClassのみです.

その他は定義済みです.誰か偉い人が書いてます.

構成要素のうちの1つ(のさらにそのうちの1メソッド)を書いて全体を理解するなんて,1を言ったら10のことが分かるような人間でない限り,できるはずがありません.少なくとも僕は理解できませんでした.


2. 自分でクラスを作る分にはdelegateを利用しなくても大体なんとかなる

個人(or 小規模)での開発であれば,作成するクラスの中身は自分が書いたものです.ちゃんとコメントを書いていれば,時間が経っていたとしても中身が分かります.

コメントを書きましょう(戒め)

例えば,以下の(ややコメントが過剰に思える)財布クラスを作るとします.


財布.swift


class 財布{
/// 財布の残金 初期値は0
var 残金:Int = 0

/// 財布のコンストラクタ
///
/// - Parameter 初期値: 初めの金額
init(初期値:Int = 0) {
self.残金 = 初期値
}

/// お金を財布に入れる
///
/// - Parameter 入金額: 入金する金額
/// - Returns: 入金後の残額
func 入金(入金額:Int) -> Int{
残金 = 残金 + 入金額
return 残金
}

/// お金を財布から出す
///
/// - Parameter 出金額: 出金する金額
/// - Returns: 出金後の残額
func 出金(出金額:Int) -> Int{
残金 = 残金 - 出金額
return 残金
}

/// 現在の残金を返す
///
/// - Returns: 現在の残金
func 残金確認() -> Int{
return 残金
}
}


※swiftでは2byte文字の利用が可能です.このサンプルでもコンパイル通ります.この記事では全力で使っていきます.

※簡略化のためエラー処理は省略しています.

このクラスを利用した人クラスを作成します.


人.swift

class {

var 個人のお財布 = 財布(初期値: 3000)

func 買い物(商品の値段:Int){
if 商品の値段 <= 個人のお財布.残金確認(){
個人のお財布.出金(出金額: 商品の値段)
}else{
fatalError("お金が足りないぜ!!!どうしよう!!!")
}
}
}


自分の財布ですから,使い方はもちろん知っています.

忘れていればソースコードを見ればいいんです.コメントさえ書いていれば,1週間前の自分に殴られることもありません.(コメントを書きましょう(2回目))

delegateなんていう理解困難な概念を使わなくてもコードが書けます.

個人(or 小規模)の開発であれば,delegateを使う恩恵はほとんど感じられません.上記の例であれば,delegateを使うと余計にソースコードが複雑になる可能性だってあります.

デリゲート? そんなことせずに自分でやりますよ.俺は今までそうやって生きてきたんだ!!!


delegateを使ってみる

UITextFieldでdelegateが出てくるため,「delegateは使う必要はありません」なんて言ってられません.

ここからは,実際にdelegateを使う場面を見ていきましょう.


財布の拡張

オンラインストアで支払いする場合に,財布(物理)の処理を他の決済手段に任せたいとします.

そのために,財布クラスにオンラインストアでの買い物用に処理を追加します.


オンラインストアで支払いを実装した財布(コンパイルエラー)

class 財布{

func オンラインストアで支払い(支払先:String, 出金額:Int){
if オンラインストア用財布 == nil{
self.出金(出金額: 出金額)
}else{
オンラインストア用財布?.送金(支払先: 支払先, 出金額: 出金額)
}
}
}

ただし,人によって決済方法が異なるため,オンラインストア用財布にはクレジットカードが入るか,銀行振込が入るか,Amazonギフトカードが入るか,楽天ポイントが入るか,この時点では分かりません.

不確定な状態でクラスを作ることはできないので,呼び出し方法を決めておく必要があります.


財布代理として要求されるメソッド

protocol 財布代理{

func 送金(支払先:String, 出金額:Int)
}

Javaでいうインターフェース(抽象クラス)が完成しました.

財布代理が要求する送金メソッドを備えるクラスであれば(=財布代理のprotocolに適合するクラスであれば),財布代理としての役目を果たせます.

財布クラスに追加しましょう.


オンラインストア用財布を追加した財布

class 財布{

var オンラインストア用財布: 財布代理?

func オンラインストアで支払い(支払先:String, 出金額:Int){
if オンラインストア用財布 == nil{
self.出金(出金額: 出金額)
}else{
オンラインストア用財布?.送金(支払先: 支払先, 出金額: 出金額)
}
}
}


財布としてはこれでコンパイルが通るようになりました.


財布の代理を決定する

財布代理の呼び出し方が決定したので,後は使う人がその時に財布代理として使えるものを指定すればOKです.


クレジットカードで支払う人

class {

var 個人のお財布:財布 = 財布(初期値: 3000)

init(){
個人のお財布.オンラインストア用財布 = クレジットカード()
}

func オンラインストアでお買い物(店名:String, 金額:Int){
個人のお財布.オンラインストアで支払い(支払先: 店名, 出金額: 金額)
}
}


一方,クレジットカードとしては財布代理の要求する送金メソッドを実装しておけば,財布代理となることが可能です.


クレジットカード

class クレジットカード: 財布代理{

func 送金(支払先: String, 出金額: Int) {
// 送金
}
}

これでオンラインショッピングを,好きな決済方法で楽しめるようになりました.めでたしめでたし.


なんでわざわざprotocol使うの?

財布代理クラスを作って,財布代理となるクレジットカードや銀行振込が継承すれば呼び出し方も決まってるし,やりたいことを実現できるんじゃないの?

と思うかもしれませんが,クレジットカードクラスが他のクラスを継承して作成されていると,二重継承になるためコンパイルエラーとなります.protocolであれば複数適合しても問題ありませんし,何かの基底クラスのサブクラスでも問題ありません.

protocolでは,指定のメソッドor変数だけ用意しておけば良いので,継承に比べて制限が緩くなります.(TextFieldのdelegateであれば,メソッドがあれば呼びますレベル)


UITextField.delegateについて

財布の例とUITextField.delegateの構造を対応させると以下のようになります.

構成要素
財布の例
UITextField

所有者

MyClass

処理を依頼するクラス
財布
UITextField(myTextField)

protocol
財布代理
UITextFieldDelegate

処理を実行するクラス
クレジットカード
MyClass

改めてサンプルとその関連する定義を見てみましょう.


UITextFieldの定義(抜粋+編集)

class UITextField{

var text: String?
var font: UIFont?
var delegate: UITextFieldDelegate?
}


UITextFieldDelegateの定義(抜粋+編集)

protocol UITextFieldDelegate{

optional func textFieldDidBeginEditing(_ textField: UITextField)
optional func textFieldDidEndEditing(_ textField: UITextField)
optional func textFieldShouldReturn(_ textField: UITextField) -> Bool
}

※見やすいように修飾子やメソッドを大幅に削ってます.

UITextFieldの中身なんて見る機会ないですよね.全体像が見えない所以です.


EnterKeyが押されると閉じる例.swift

class MyClass: UIViewController, UITextFieldDelegate{

@IBOutlet weak var myTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
myTextField.delegate = self
}

// Enter keyを押したときに実行される
func textFieldShouldReturn(_ textField:UITextField) -> Bool {
// 独自処理 キーボードを閉じる
self.view.endEditing(true)
return true
}
}


UITextFieldの処理の一部を,プロトコル「UITextFieldDelegate」に適合したクラス「MyClass」が代わりに処理していたんですね.

protocolを使うことで,MyClassに対する制約はなく,自由にクラスを作成できます.

盲目的にmyTextField.delegate = selfと書かなくても,自分でUITextFieldDelegateに適合したクラスを作成して,そのクラスにEnter keyの処理を委譲してもOKです.


最後に

最後までご覧いただきありがとうございます.

swiftで初心者が躓くであろうdelegateについて,少しでも理解の手助けになれば幸いです.

なお,本記事はswift 1年生が書いています.間違いなどあれば,ご指摘いただけると嬉しいです.