Swift with Majid

the power of Delegate design pattern

29 maja 2019

ostatni tydzień przed WWDC i wszyscy tak podekscytowani nowymi funkcjami, które będziemy mieli tylko za kilka dni. Zachowajmy jednak posty związane z WWDC na przyszły tydzień. W tym tygodniu porozmawiamy o moim ulubionym delegacie wzorca projektowego. Delegowanie to najprostszy i najpotężniejszy wzór.

w inżynierii oprogramowania wzorzec delegowania jest zorientowanym obiektowo wzorcem projektowym, który umożliwia skład obiektowy w celu osiągnięcia tego samego ponownego użycia kodu jako dziedziczenia. W przypadku delegowania obiekt obsługuje żądanie, delegując je do drugiego obiektu (delegata). Delegat jest obiektem pomocniczym, ale z oryginalnym kontekstem.

protokoły

codziennie używamy wzorca delegatów, a iOS SDK używa go w wielu miejscach. Na przykład UITableView deleguje do uitableviewdatasource wypełniając tabelę komórkami, deleguje również wybór komórek i inne akcje do UITableViewDelegate. Innym doskonałym przykładem wzorców delegatów jest FlowController lub Coordinators. ViewControllers deleguje logikę nawigacji do koordynatora. Oddzieliłem post o rozpakowywaniu logiki nawigacji do kontrolerów przepływu.

zagłębimy się w próbki kodu. Załóżmy, że pracujesz nad grą. Wyodrębniłeś logikę gry do oddzielonej klasy Gry i chcesz przekazać zmiany stanu gry do UIViewController, który renderuje tę grę.

protocol GameDelegate: AnyObject { func stateChanged(from oldState: Game.State, to newState: Game.State)}class Game { private var state: State = .notStarted { didSet { delegate?.stateChanged(from: oldValue, to: state) } } weak var delegate: GameDelegate? private(set) var value: Int = 0 func start() { state = .started } func generateNextValue() { value = Int.random(in: 0..<1000) state = generateState(using: value) }}extension Game { enum State { case notStarted case started case right case win case lost }}

oto kod źródłowy prostej gry, która generuje losowe wartości. Silnik gry generuje stan na podstawie losowych wartości. Każda zmiana stanu wzywa delegata do przekazania starych i nowych stanów. Definiujemy nasz protokół delegata rozszerzony z AnyObject, co oznacza, że jedyna instancja klasy może go zaakceptować. Używam również słabego słowa kluczowego do definiowania zmiennej holding delegate. Konieczne było przerwanie cyklu utrzymania między delegatem a klasą gry. Rzućmy okiem na GameViewController teraz.

class GameViewController: UIViewController { private let game: Game init(game: Game) { self.game = game super.init(nibName: nil, bundle: nil) } @IBAction func play() { game.start() } @IBAction func next() { game.generateNextValue() } override func viewDidLoad() { super.viewDidLoad() game.delegate = self }}extension GameViewController: GameDelegate { func render(_ state: Game.State) { switch state { case .lost: renderLost() case .right: renderRight() case .win: renderWin() case .started: renderStart() case .notStarted: renderNotStarted() } } func stateChanged(from oldState: Game.State, to newState: Game.State) { render(newState) }}

tutaj mamy klasę GameViewController, która karmi grę działaniami użytkownika i renderuje zmiany stanu. GameViewController jest zgodny z GameDelegate i implementuje wszystkie potrzebne renderowanie w rozszerzeniu. W rezultacie mamy composable codebase za pomocą delegowania wzorca projektowego.

zamknięcia

czasami, gdy masz tylko jedną metodę w delegacie, możesz zastąpić ją zamknięciem. Pomysł jest taki sam, ale teraz wywołujesz zamknięcie i przekazujesz stan zamiast wywoływać metodę przez protokół. Spójrzmy na przykład z zamknięciem.

class Game { typealias StateHandler = (State) -> Void var handler: StateHandler? private var state: State = .notStarted { didSet { handler?(state) } }}class GameViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() game.handler = { state in self?.render(state) } }}

jak widać, przekazujemy zamknięcie instancji klasy gry, która obsługuje zmiany stanu. Używamy słabego do przerwania cyklu retain podczas przechwytywania kontekstu zamknięcia. Inną opcją może być wykorzystanie faktu, że każda funkcja Swift jest zamknięciem. Zamiast tworzyć oddzielne zamknięcie, możemy przekazać nazwę funkcji. Należy jednak uważać, aby ta metoda tworzy kółko zachowawcze. Oto przykład, jak możemy to zrobić.

class GameViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() game.handler = render }}extension GameViewController { func render(_ state: Game.State) { switch state { case .lost: renderLost() case .right: renderRight() case .win: renderWin() case .started: renderStart() case .notStarted: renderNotStarted() } }}

podsumowanie

dzisiaj omówiliśmy najpotężniejszy i najprostszy wzorzec projektowy w rozwoju iOS. Podoba mi się, jak proste to jest i jak przydatne może być w komponowaniu utworów, aby codebase oddzielone. Zapraszam do śledzenia mnie na Twitterze i zadawania pytań związanych z tym postem. Dziękujemy za przeczytanie i do zobaczenia w przyszłym tygodniu!



+