【Swift】delegate(デリゲート)とは?使い方とメリット
この記事からわかること
- Swiftのdelegate(デリゲート)とは?
- 使い方やメリット
- 処理を任せるクラスとは?
- 処理を任されるクラスとは?
- デリゲートメソッド
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
環境
- Xcode:15.0.1
- iOS:17.1
- Swift:5.9
- macOS:Sonoma 14.1
Swiftでアプリ開発をしているApp Delegate
やUITableViewDelegate
などDelegateという単語が度々登場します。「Delegate(デリゲート)」とはどのような仕組みなのかまとめていきたいと思います。
Delegate(デリゲート)とは?
Swiftで登場するDelegate(デリゲートパターン)とはSwift独自のものではなく、オブジェクト指向型のプログラミング言語に活用されるGoFのデザインパターン(設計パターン)の一種です。
おすすめ記事:【GoF】23種類のデザインパターンとは?Swiftでよく使う活用例
delegateとは日本語で「委任/委譲」を意味する英単語でその名前の通り、処理を委任する役割をもっています。
では誰が誰に処理を委任するのかというとクラスからクラス(オブジェクトからオブジェクト)です。オブジェクト指向型のプログラミング言語ではクラスを定義してインスタンスを生成し、オブジェクト単位でのデータ操作が大きなメリットになっています。 このオブジェクト間(クラス間)の処理の受け渡しをdelegateでは簡単かつ再利用しやすくすることができます。
メリット
デリゲートパターンを使用することで処理の実装を委任先のオブジェクトに隠蔽することができます。これにより処理を行うオブジェクトと処理を受け取るオブジェクトの間の関係が疎結合になります。 オブジェクト同士はできるだけ依存を減らし、疎結合にしていくことでオブジェクトの関心領域を明確にし、汎用性を高めることができます。
実装例
例えばボタンが押された時に実行される処理を実装するためにデリゲートパターンは活用されます。
「ボタンクラス」と「デバッグエリアにプリントするクラス」があるとします。ボタンには押されたことを検知しその処理を委任するためのオブジェクトを持たせます。このオブジェクトがデリゲートであり、処理を受け取るオブジェクトがデリゲート先となります。デリゲート先には「デバッグエリアにプリントするクラス」を指定すればボタンが押されたことを検知した時にデバッグエリアにプリントされます。
「ボタンクラス」はボタンを押した時に何が実行されるかを知る必要はありません。同様に「デバッグエリアにプリントするクラス」はどのタイミングで呼ばれるかを知る必要はありません。
また委譲先のオブジェクトを切り替えるだけで同じ処理を異なる方法で実行することができます。 この状態こそがお互いの依存度が低く、疎結合に近い状態なのだと思います。
Delegate(デリゲートパターン)とは?
- delegateは処理を委任する役割
- 日本語で「委任」の意
- 委任し合うのはクラス(オブジェクト)
- デザインパターン(設計パターン)の1つ
- 処理の実装を委任先のオブジェクトに隠蔽
- オブジェクト同士の依存度を減らし疎結合にする
Delegate(デリゲートパターン)の仕組みと実装方法
では実際にSwiftでDelegateを使用して処理を委任するための仕組みを構築していきます。今回は基礎的な実装方法に焦点を当てたいので、意味のないクラスを生成することをご了承ください。
Swiftのデリゲートで重要となるのは以下の3つの存在です。
- プロトコル
- 処理を任せるクラス
- 処理を任されるクラス
プロトコルとは
プロトコルとは規約のことであり、Swiftではクラスや構造体を定義する際のルール(構造規格)のことを指します。プロトコルを準拠させたクラスはプロトコルの持つプロパティとメソッドの定義が必須になります。
デリゲートではプロトコルを宣言してその中に任せる処理の枠組みをメソッドとして定義しておきます。このメソッドをデリゲートメソッドと呼びます。
デリゲートメソッドには実際の処理部分は記述せず定義のみを記述しておきます。
処理を任せるクラスとは
処理を任せる側のクラスはデリゲートプロトコルには準拠させずに、「処理を任せるクラス」を保持させるプロパティ(名前は慣習的にdelegate)を定義します。
ここには処理を呼び出すまでの流れや条件分岐などを記述しておき具体的な処理はdelegate
プロパティで保持するクラスに任せます。。
処理を任されるクラスとは
処理を実際に実装する部分であり、デリゲートプロトコルに準拠させたクラスとして用意します。
このクラスは最初のdelegate
プロパティへの格納以降、「処理を任せるクラス」の中で使用される前提で定義されます。
デリゲートを実装するためには上記の3つを準備しておきます。肝となるのは「処理を任されるクラス」のdelegateプロパティです。
delegateプロパティに格納されている「処理を任されているクラス」の値によって処理を分岐したり、別の挙動をおこなせることが可能になります。
デリゲートの使い方
まずはデリゲートとなるプロトコルを定義します。プロトコルはprotocol
で宣言します。
protocol DemoDelegate {
var price:Double { get set }
// 具体的な処理は記述せず定義のみ
func printText()
func discountNumber(_ amount:Double)
}
今回は例として引数は「amount:Double」のみにしましたが、カスタムデリゲートを作る際は第一引数に委譲元のクラスを渡すのが規約のようです。
正しくは「func discountNumber(_ demodelegate:DemoDelegate,_ amount:Double)」なるのかもしれません。
処理を任せるクラスを作成する
処理を任せる側のクラス自体にはプロトコルを指定せず、クラスのプロパティ(delegateプロパティ)にプロトコルを準拠させます。これでdelegate
プロパティにはプロトコルに則った形式のクラスしか格納できないようになります。格納されなかった時に備えnil
を許容できるようにしておきます。また循環参照を避けるためにweak
をつけておきます。
class Manager {
weak var delegate: DemoDelegate? = nil
func execute() {
if let d = self.delegate {
d.printText()
d.discountNumber(5)
} else {
print("インスタンス未格納")
}
}
}
ここではdelegateプロパティの値によって処理を分岐させることができます。何かしらのクラスが格納されているかや、格納されているクラスでの識別も可能になります。
処理を任されるクラスを作成する
任されるクラスは定義したプロトコルに準拠させたクラスで宣言し、中にはデリゲートメソッドを実装します。デリゲートメソッドの定義は必須なので記述し忘れるとType 'ManagerA' does not conform to protocol 'DemoDelegate'
のようなエラーを吐きます。
class ManagerA: DemoDelegate {
var price: Double = 200
func printText() {
print("Hello World")
}
func discountNumber(_ amount: Double){
self.price = (self.price + amount) * 0.5
print(self.price)
}
}
デリゲートメソッドを実行してみる
使用する際は以下のようにインスタンス化したManagerA()
クラスをdelegate
プロパティに格納します。これで
let manager = Manager()
manager.delegate = ManagerA()
manager.execute() // デリゲートメソッド達が実行される
// Hello World
// 102.5
再利用して別の処理を作成してみます。
class ManagerB: DemoDelegate {
var price: Double = 1000
func printText() {
print("こんにちは世界")
}
func discountNumber(_ amount: Double){
self.price = (self.price + amount) * 0.2
print(self.price)
}
}
作成するのはプロトコルにより定型的に定義されている変更したい処理部分のみでそれに至るまでの流れや分岐などは変更しなくてすみます。
let manager = Manager()
manager.delegate = ManagerA()
manager.execute() // デリゲートメソッド達が実行される
// こんにちは世界
// 201.0
デリゲートプロパティに格納されているクラスで分岐させる
デリゲートプロパティに格納されているクラスを識別して処理を分岐させることも可能です。
class Manager {
weak var delegate: DemoDelegate? = nil
func execute() {
if let d = self.delegate {
if type(of: d) == ManagerA.self {
// ManagerAの場合のみ
d.printText()
} else if type(of: d) == ManagerB.self {
// ManagerBの場合のみ
d.discountNumber(5)
}
} else {
print("インスタンス未格納")
}
}
}
デリゲートの使用例
実際にデリゲートが組み込まれている処理を見てみるとわかりやすいかもしれません。
今回は例として「iOSアプリでユーザーの位置情報を取得する処理」を見ていきます。
位置情報を取得するためには地図を表示できるMapKit
フレームワークをインポートして位置情報を取得するためのCLLocationManager
クラスを使用可能にする必要があります。
下記のコードがデリゲートが絡んだ「iOSアプリでユーザーの位置情報を取得する処理」です。
import MapKit
// 現在地を取得するためのクラス
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
let manager = CLLocationManager()
@Published var region = MKCoordinateRegion()
override init() {
super.init() // スーパクラスのイニシャライザを実行
manager.delegate = self // 自身をデリゲートプロパティに設定
manager.requestWhenInUseAuthorization() // 現在地取得の許可を申請
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.distanceFilter = 3 // 更新距離(m)
manager.startUpdatingLocation()
}
// デリゲートメソッド
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
locations.last.map {
let center = CLLocationCoordinate2D(
latitude: $0.coordinate.latitude,
longitude: $0.coordinate.longitude)
region = MKCoordinateRegion(
center: center,
latitudinalMeters: 1000.0,
longitudinalMeters: 1000.0
)
}
}
}
それぞれの役割
- プロトコル:CLLocationManagerDelegate
- 処理を任せるクラス:LocationManager
- 処理を任されるクラス:LocationManager
- デリゲートメソッド:locationManagerメソッド
今回のデリゲートは「処理を任せるクラス」と「処理を任されるクラス」が同一になっています。
準拠しているCLLocationManagerDelegate
プロトコルの定義を見てみるとデリゲートメソッドがたくさん定義されています。メソッドの前にoptional
がついているのはプロトコルですが実装が必須ではないようにするキーワードです。
CLLocationManagerDelegateプロトコルの定義
public protocol CLLocationManagerDelegate : NSObjectProtocol {
@available(iOS 6.0, *)
optional func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
@available(iOS 3.0, *)
optional func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading)
.
.
.
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。