Swift: Swiftプロトコル:インターフェース定��、プロトコル継承とデリゲートパターン
プロトコルはメソッド、プロパティ、その他の要件の設計図を定義します。これらはSwiftのプロトコル指向プログラミング(POP)の基礎です。このレッスンで��プロトコルの定義と準拠方法、そしてデリゲートパターンの実践をカバーします。
1. 学習目標
protocolを使ってプロパティとメソッドの要件を定義する- クラスと構造体をプロトコルに準拠させる
- プロトコルの継承と合成
extensionによるデフォルト実装の提供- デリゲートデザインパターン
2. 決済開発者の実話
(1) 課題:各支払い方法が独自のインターフェースを持つ
Aliceのチームはクレジットカード、PayPal、WeChat Payの複数の支払い方法を��合する必要があります:
SWIFT
class CreditCardPayment {
func processCardPayment(amount: Double, cardNumber: String) -> Bool {
// Credit card processing
return true
}
}
class PayPalPayment {
func payWithPayPal(amount: Double, email: String) -> Bool {
// PayPal processing
return true
}
}
class WeChatPayment {
func wechatPay(amount: Double, code: String) -> Bool {
// WeChat Pay processing
return true
}
}
3つのクラス、3つの異なるメソッド名、3つの異なるパラメータリスト。 ビジネスロジックは支払いタイプのif-elseチェックで溢れています。支払い方法を1つ追加するにはすべての呼び出し箇所を変更する必要があります。
(2) 解決策:プロトコル
SWIFT
protocol PaymentMethod {
func pay(amount: Double) -> Bool
}
struct CreditCard: PaymentMethod {
let cardNumber: String
func pay(amount: Double) -> Bool { /* Process */ true }
}
struct PayPal: PaymentMethod {
let email: String
func pay(amount: Double) -> Bool { /* Process */ true }
}
// Unified handling
func checkout(amount: Double, using method: PaymentMethod) {
method.pay(amount: amount)
}
支払い方法が何かを気にする必要はありません — PaymentMethodに準拠していることだけが重要です。
(3) 利点:統一インターフェース、拡張可能
| 観点 | 独立した実装 | プロトコル統一 |
|---|---|---|
| メソッド名 | すべて異なる | pay(amount:) に統一 |
| メソッドの追加 | すべての呼び出し箇所を変更 | 準拠型を追加するだけ |
| テスト | それぞれにテストスイートを書く | プロトコルモックで統一テスト |
| 結合度 | 高い | 低い(プロトコル指向プログラミング) |
3. プロトコルの定義と準拠
(1) プロトコル構文
graph TB
A[protocol キーワード] --> B[プロトコル名]
B --> C[プロパティ要件]
B --> D[メソッド要件]
B --> E[サブスクリプト要件]
C --> F["var name: String { get set }"]
D --> G["func work()"]
E --> H["subscript(...) -> Type"]
| プロトコル要件 | 構文 | 備考 |
|---|---|---|
| 読み書きプロパティ | { get set } |
var またはコンピューテッドプロパティ(get+set) |
| 読み取り専用プロパティ | { get } |
let、var、または読み取り専用コンピューテッドプロパティ |
| インスタンスメソッド | func name() |
シグネチャのみ、実装なし |
| mutatingメソッド | mutating func name() |
値型がselfを変更可能 |
▶ サンプル: プロトコルの定義と準拠
SWIFT
// ============================================
// Defining and conforming to a protocol
// ============================================
// 1. Define the protocol
protocol Describable {
var description: String { get }
func summarize() -> String
}
// 2. Struct conforming to the protocol
struct Book: Describable {
let title: String
let author: String
var description: String {
return "\"\(title)\" by \(author)"
}
func summarize() -> String {
return "Book: \(description)"
}
}
// 3. Class conforming to the protocol
class Movie: Describable {
let title: String
let director: String
init(title: String, director: String) {
self.title = title
self.director = director
}
var description: String {
return "\(title) (directed by \(director))"
}
func summarize() -> String {
return "Movie: \(description)"
}
}
let book = Book(title: "1984", author: "George Orwell")
let movie = Movie(title: "Inception", director: "Christopher Nolan")
print(book.summarize())
print(movie.summarize())
出力:
TEXT 📖 参照専用Book: "1984" by George Orwell Movie: Inception (directed by Christopher Nolan)
4. プロトコル継承と合成
(1) プロトコル継承
プロトコルは1つ以上の他のプロトコルから継承できます:
graph TB
A[プロトコル: Payable] --> B["プロパティ: amount"]
A --> C["メソッド: process()"]
B --> D[プロトコル: Refundable]
C --> D
D --> E["メソッド: refund()"]
D --> F[型: CreditCard]
F --> G["Payable + Refundable を実装"]
▶ サンプル: プロトコル継承
SWIFT
// ============================================
// Protocol inheritance — a refundable payment protocol
// ============================================
protocol Payable {
var amount: Double { get }
func process() -> Bool
}
protocol Refundable: Payable { // Inherits Payable
func refund() -> Bool
}
// CreditCard implements refundable payment
struct CreditCard: Refundable {
let amount: Double
let cardNumber: String
func process() -> Bool {
print("Processing $\(amount) on card \(cardNumber)")
return true
}
func refund() -> Bool {
print("Refunding $\(amount) to card \(cardNumber)")
return true
}
}
// GiftCard can pay but not refund
struct GiftCard: Payable {
let amount: Double
let code: String
func process() -> Bool {
print("Processing $\(amount) with gift card \(code)")
return true
}
}
let payments: [Payable] = [
CreditCard(amount: 100, cardNumber: "1234"),
GiftCard(amount: 50, code: "GIFT-001")
]
for payment in payments {
payment.process()
// Check if also Refundable
if let refundable = payment as? Refundable {
print(" This payment can be refunded")
}
}
出力:
TEXT 📖 参照専用Processing $100.0 on card 1234 This payment can be refunded Processing $50.0 with gift card GIFT-001
(2) プロトコル合成
& 演算子を使って複数のプロトコルを結合し、型がそれらすべてに同時に準拠することを要求します:
SWIFT
protocol Identifiable {
var id: String { get }
}
protocol Loggable {
func log()
}
// Type must conform to both Identifiable and Loggable
func saveItem(_ item: Identifiable & Loggable) {
print("Saving item \(item.id)")
item.log()
}
struct User: Identifiable, Loggable {
let id: String
func log() { print("User logged: \(id)") }
}
let user = User(id: "U-001")
saveItem(user)
出力:
TEXT 📖 参照専用Saving item U-001 User logged: U-001
5. extensionによるデフォルト実装とデリゲートパターン
(1) extensionによるデフォルト実装の提供
extension を使ってプロトコルメソッドにデフォルト実装を提供します — 準拠型は実装しないことを選択できます:
SWIFT
// ============================================
// Adding default implementations to protocols via extension
// ============================================
protocol Greetable {
var name: String { get }
func greet() -> String
}
// Default implementation
extension Greetable {
func greet() -> String {
return "Hello, \(name)!"
}
}
// Person uses the default implementation
struct Person: Greetable {
let name: String
// No need to implement greet() — uses the default version
}
// Robot provides a custom implementation
struct Robot: Greetable {
let name: String
func greet() -> String {
return "Beep boop, I am \(name)"
}
}
print(Person(name: "Alice").greet())
print(Robot(name: "R2-D2").greet())
出力:
TEXT 📖 参照専用Hello, Alice! Beep boop, I am R2-D2
(2) デリゲートパターン
デリゲートパターンは、あるオブジェクトが自身の作業の一部をプロトコルに準拠した別のオブジェクトに委譲する設計パターンです:
sequenceDiagram
participant A as クラスA
participant D as デリゲート (protocol)
participant B as クラスB
A->>D: イベント発生
D->>B: デリゲートメソッドを呼び出す
B-->>A: 処理結果を返す
▶ サンプル: デリゲートパターンの実装
SWIFT
// ============================================
// Delegate pattern: Download manager
// ============================================
// 1. Define the delegate protocol
protocol DownloadDelegate: AnyObject {
func downloadDidStart(_ url: String)
func downloadDidProgress(_ url: String, percent: Double)
func downloadDidComplete(_ url: String, data: String)
func downloadDidFail(_ url: String, error: String)
}
// 2. Download manager — delegates events to an external handler
class DownloadManager {
weak var delegate: DownloadDelegate?
func download(from url: String) {
delegate?.downloadDidStart(url)
// Simulate download progress
for i in 1...5 {
let percent = Double(i) / 5.0 * 100
delegate?.downloadDidProgress(url, percent: percent)
}
// Simulate completion
delegate?.downloadDidComplete(url, data: "Downloaded content from \(url)")
}
}
// 3. View controller — acting as the delegate
class ViewController: DownloadDelegate {
func downloadDidStart(_ url: String) {
print("[UI] Download started: \(url)")
}
func downloadDidProgress(_ url: String, percent: Double) {
print("[UI] \(url): \(Int(percent))%")
}
func downloadDidComplete(_ url: String, data: String) {
print("[UI] Download complete: \(data.prefix(20))...")
}
func downloadDidFail(_ url: String, error: String) {
print("[UI] Download failed: \(error)")
}
}
let manager = DownloadManager()
let ui = ViewController()
manager.delegate = ui // Set the delegate
manager.download(from: "https://example.com/file.zip")
出力:
TEXT 📖 参照専用[UI] Download started: https://example.com/file.zip [UI] https://example.com/file.zip: 20% [UI] https://example.com/file.zip: 40% [UI] https://example.com/file.zip: 60% [UI] https://example.com/file.zip: 80% [UI] https://example.com/file.zip: 100% [UI] Download complete: Downloaded content...ヒント:
AnyObject制約はデリゲートがクラス型であることを要求し(構造体ではない)、weak varを使って循環参照を回避できるようにします。これはデリゲートパターンの標準的な方法です。
6. 完全な例:設定可能なデータバリデーター
SWIFT
// ============================================
// Complete example: Data validation system
// Features: Protocol + protocol inheritance + extension defaults + delegate pattern
// ============================================
import Foundation
// 1. Validation protocol hierarchy
protocol Validatable {
var value: Any { get }
func validate() -> Bool
}
// Validation with detailed error reporting
protocol DetailedValidatable: Validatable {
func errorMessage() -> String
}
// Default implementation
extension Validatable {
func validate() -> Bool { return true }
}
// 2. Concrete validators
struct EmailValidator: DetailedValidatable {
let value: Any
func validate() -> Bool {
guard let email = value as? String else { return false }
return email.contains("@") && email.contains(".")
}
func errorMessage() -> String {
return "Invalid email format"
}
}
struct AgeValidator: DetailedValidatable {
let value: Any
func validate() -> Bool {
guard let age = value as? Int else { return false }
return age >= 18 && age <= 120
}
func errorMessage() -> String {
return "Age must be between 18 and 120"
}
}
struct NonEmptyValidator: Validatable {
let value: Any
// Uses default validate() which returns true, but we customize it
func validate() -> Bool {
guard let text = value as? String else { return false }
return !text.isEmpty
}
}
// 3. Delegate — validation result handler
protocol ValidationDelegate: AnyObject {
func validationDidSucceed(for field: String)
func validationDidFail(for field: String, error: String)
}
// 4. Validation manager
class ValidationManager {
weak var delegate: ValidationDelegate?
private var validators: [(field: String, validator: Validatable)] = []
func addValidator(for field: String, validator: Validatable) {
validators.append((field, validator))
}
func runAll() -> Bool {
var allValid = true
for (field, validator) in validators {
if validator.validate() {
delegate?.validationDidSucceed(for: field)
} else {
allValid = false
let error = (validator as? DetailedValidatable)?.errorMessage() ?? "Validation failed"
delegate?.validationDidFail(for: field, error: error)
}
}
return allValid
}
}
// 5. Usage
class FormController: ValidationDelegate {
func validationDidSucceed(for field: String) {
print("[OK] \(field) is valid")
}
func validationDidFail(for field: String, error: String) {
print("[FAIL] \(field): \(error)")
}
}
let manager = ValidationManager()
manager.delegate = FormController()
manager.addValidator(for: "email", validator: EmailValidator(value: "alice@example.com"))
manager.addValidator(for: "age", validator: AgeValidator(value: 25))
manager.addValidator(for: "name", validator: NonEmptyValidator(value: "Alice"))
let allValid = manager.runAll()
print("Form valid: \(allValid)")
出力:
TEXT 📖 参照専用[OK] email is valid [OK] age is valid [OK] name is valid Form valid: true
❓ よくある質問
Q プロトコルと基本クラスの違いは何ですか?
A プロトコルはインターフェースの設計図のみを定義します — プロパティを保存できません(
{ get set } の宣言のみ)。基本クラスはストアドプロパティとメソッド実装を提供できます。クラスは複数のプロトコルに準拠できますが、継承できる基本クラスは1つだけです。プロトコル指向プログラミング(POP)はSwiftの中核的な設��思想です。Q プロトコルのデフォルト実装と基本クラスはいつ使い分けるべきですか?
A 共有ストアドプロパティや継承イニシャライザが必要な場合は基本クラスを使います。デフォルト実装付きの動作インターフェースを定義するだけで良い場合はプロトコルエクステンションを使います。プロトコルはより柔軟なので(複数プロトコル準拠可能)、優先してください。
Q デリゲートにweak varを使うのはなぜですか?
A 循環参照を避けるためです。
DownloadManager が delegate を保持し��delegate も DownloadManager を保持すると、強参照サイクルが形成されます。weak はデリゲート参照が参照カウントを増やさないようにします。デリゲートにクラスを要求することで、構造体が弱参照をサポートしないため weak を使用できるようになります。Q プロトコル内のmutatingメソッドの目的は何ですか?
A 準拠型自身のプロパティを変更できるメソッドをマークします。構造体と列挙型のメソッドはデフォルトではプロパティを変更できません —
mutating を追加することで可能になります。クラスの実装では mutating を書く必要はありません — クラスメソッドはデフォルトでプロパティを変更できます。Q @objcプロトコルは何に使われますか?
A
@objc プロトコルは、そのプロトコルをObjective-Cコードから使用できるようにします。UIKitのデリゲートプロトコルでよく見られます。@objcプロトコルは optional 要件(実装が任意のメソッド)を持てます — 純粋なSwiftプロトコルにはない機能です。📖 まとめ
- プロトコルはプロパティとメソッドの要件を実装なしで定義する
- クラス、構造体、列挙型はすべてプロトコルに準拠できる
- プロトコルは継承をサポート — 1つのプロトコルが複数の親プロトコルから継承可能
&によるプロトコル合成は型が複数のプロトコルに同時に準拠することを要求するextensionはプロトコルにデフォルト実装を提供し、準拠型がオーバーライド可能- デリゲートパターンは弱いプロトコル参照を使って循環参照を回避する
📝 練習問題
- 基本:
fly()メソッドを持つFlyableプロトコルを定義してください。それに準拠し、fly()を実装するBird構造体とAirplaneクラスを作成してください。 - 中級:
Encryptableプロトコル(func encrypt(_: String) -> String)とDecryptableプロトコル(func decrypt(_: String) -> String)を定義してください。両方に準拠するCaesarCipherクラスを作成してください(シーザー暗号、シフト3)。���ロトコル合成Encryptable & Decryptableをパラメータ型として使用してください。 - 発展: 「ソート可能データソース」デリゲートパターンを実装してください:
SortableDataSourceプロトコル(func numberOfItems() -> Intとfunc item(at index: Int) -> String)とSortDelegateプロトコル(func didSort(items: [String]))を定義します。SortableDataSourceに準拠するNameListクラスとSortDelegateに準拠するTableViewクラスを作成してください。NameListはソート後にTableViewに通知します。