Delegate-Pattern in Swift

von @ralfebert · aktualisiert am 30. Juli 2019

In vielen iOS-Frameworks kommt das Delegate-Pattern zum Einsatz. Dabei benachrichtigt ein Objekt ein konfigurierbares Delegate-Objekt über aufgetretene Ereignisse. Oft geht es dabei um Ereignisbenachrichtigung (eigentlich Observer-Pattern) - der Begriff Delegate kommt eher aus der Historie von Objective-C und versteht sich als "ein Objekt delegiert die Abarbeitung von Ereignissen an ein externes Objekt".

So kann z.B. bei einem UITextView in UIKit ein UITextViewDelegate registriert werden, um eine Benachrichtigung bei Änderungen am Textfeld zu erhalten:

class TextViewController : UIViewController, UITextViewDelegate {

    override func loadView() {
        let textView = UITextView()
        textView.text = "Hello World!\nHello Playground!"
        textView.delegate = self

        self.view = textView
    }

    // MARK: - UITextViewDelegate

    func textViewDidChange(_ textView: UITextView) {
    debugPrint("Text view did change!")
}

}

Auch die Klasse AppDelegate, die zu jedem iOS-Projekt gehört, ist ein solches Delegate. Dieses wird über das Attribut @UIApplicationMain automatisch bei der UIApplication registriert - die Eigenschaft delegate muss nicht manuell gesetzt werden:

@UIApplicationMain
class ExampleAppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // ...
    }

}

Delegate Pattern implementieren

Dieses Pattern ist auch nützlich für das Zusammenspiel zwischen eigenen Objekten, z.B. zwischen Modell und Controller oder zwischen verschiedenen Controllern.

Nehmen wir beispielsweise eine einfache Klasse Countdown an, die einen sekündlich herunterzählenden Timer implementiert:

class Countdown {

    var seconds : TimeInterval = 0

    init(seconds: TimeInterval) {
        self.seconds = seconds
    }

    func start() {
        self.scheduleTick()
    }

    func scheduleTick() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            self.seconds -= 1
            if self.seconds > 0 {
                self.scheduleTick()
            }
        }
    }

}

Möchte diese Klasse nun die Möglichkeit schaffen, das Herunterzählen und Erreichen des Countdown-Endes zu beobachten, wäre dies ein Anwendungsfall für ein Delegate.

Sie würde ein Protokoll CountdownDelegate definieren:

protocol CountdownDelegate : AnyObject {

    func secondsChanged(countdown: Countdown)
    func finished(countdown: Countdown)

}

Es ist üblich, das das Objekt, welches das Ereignis delegiert, als Parameter mitgegeben wird, damit bei einer Mehrfachregistrierung bei verschiedenen Objekten eine Unterscheidung möglich ist.

Es wird zudem eine Eigenschaft definiert, über die ein Delegate registriert werden kann:

class Countdown {

    weak var delegate : CountdownDelegate?

}

Hinweis: Wegen der iOS-Speicherverwaltung sollte ein solches Delegate-Property als weak deklariert werden, damit kein Specherleck durch sich gegenseitig besitzende Objekte (Retain Cycle) zustande kommt. Daher sollten Delegate-Protokolle auch mit der Angabe : AnyObject als Protokoll-nur-für-Klassen deklariert werden.

Tritt nun ein Ereignis auf, kann das Delegate benachrichtigt werden. In diesem Beispiel könnte man dafür ein Property Observer verwenden, um die Veränderung an der Eigenschaft seconds mitzubekommen:

class Countdown {

    // ...

    var seconds: TimeInterval = 0 {
        didSet {
    self.delegate?.secondsChanged(countdown: self)
}
    }

    // ...

}

Ein Objekt, welches Interesse an den Ereignissen von TimerModel hat, wie z.B. ein CountdownViewController, könnte nun konform zu dem Delegate-Protokoll implementiert werden und sich beim Modellobjekt als Delegate registrieren:

class CountdownViewController: UIViewController, CountdownDelegate {

    @IBOutlet weak var timeLabel: UILabel!

    let countdown = Countdown(seconds: 5)

    override func viewDidLoad() {
        super.viewDidLoad()

        self.countdown.delegate = self
    }

    // MARK: - CountdownDelegate

    func secondsChanged(countdown: Countdown) {
    self.timeLabel.text = String(countdown.seconds)
}

func finished(countdown: Countdown) {
    self.timeLabel.text = "Timer finished!"
}

}

So kennt das Model den Controller nur mittelbar über das Delegate-Protokoll:

Delegate Pattern in Swift

Tipp: Die Klasse könnte auch mit einer Extension konform zu dem Protokoll gemacht werden, dies ist etwas übersichtlicher, da die Delegate-Methoden von den View-Controller-Methoden getrennt sind:

class CountdownViewController: UIViewController {
    // ...
}

extension CountdownViewController : TimerModelDelegate {

    func timeChanged(timer: TimerModel) {
        self.timeLabel.text = "\(Int(timer.timeRemaining))"
    }

    func stopped(timer: TimerModel) {
        self.timeLabel.text = "Bing!"
    }

}

Weitere Informationen