swift Delegation 스위프트 델리게이트

 

먼저 공식문서(docs.swift.org/swift-book/LanguageGuide/Protocols.html#//apple_ref/doc/uid/TP40014097-CH25-ID276)에 따르면 Delegation에 대한 설명은 다음과 같다. (링크된 페이지의 가운데즈음에 Delagation에 대한 부분이 나와있다.)

 

Delegation

Delegation 은 클래스 또는 구조체가 책임(responsibility)의 일부를 다른 타입의 인스턴스에 넘겨주거나 위임할 수 있도록 하는 디자인 패턴이다. 이 디자인 패턴은 위임된 기능을 제공하기 위해 conforming type(준수 형식?)이 보장되도록 위임된 책임을 캡슐화하는 프로토콜을 정의하여 구현된다. 위임은 특정 작업에 응답하거나 해당 소스의 기본 유형을 몰라도 외부 소스에서 데이터를 검색하는 데 사용할 수 있다.

 

아래 예시는 주사위 기반 보드 게임에 사용할 두 가지 프로토콜을 정의한다.

protocol DiceGame {
  var dice: Dice { get }
  func play()
}

protocol DiceGameDelegate: AnyObject {
  func gameDidStart(_ game: DiceGame)
  func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
  func gameDidEnd(_ game: DiceGame)
}

 

DiceGame 프로토콜은 주사위를 포함해 어떤 게임에든 채택될 수 있는 프로토콜이다.

이 DiceGameDelegate 프로토콜은 DiceGame의 progress(진행상황)을 track(따라가기) 하기 위해 채택될 수 있다.

 

...

 

다음은 Dice 인스턴스를 사용하기 위해 적용된다. 또한 DiceGame 프로토콜을 채택하고 DiceGameDelegate 에게 진행상황을 알려주기 위해서이다. (아래 내용을 자세히 이해하려고는 하지 말자. - 필자)

class SnakesAndLadders: DiceGame {
    let finalSquare = 25
    let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
    var square = 0
    var board: [Int]
    init() {
        board = Array(repeating: 0, count: finalSquare + 1)
        board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
        board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
    }
    weak var delegate: DiceGameDelegate?
    func play() {
        square = 0
        delegate?.gameDidStart(self)
        gameLoop: while square != finalSquare {
            let diceRoll = dice.roll()
            delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)
            switch square + diceRoll {
            case finalSquare:
                break gameLoop
            case let newSquare where newSquare > finalSquare:
                continue gameLoop
            default:
                square += diceRoll
                square += board[square]
            }
        }
        delegate?.gameDidEnd(self)
    }
}

 

이 게임은 DiceGame 프로토콜을 적용한 SnakesAndLadders 라는 클래스로 귀결(wrapped)된다. 프로토콜을 준수하기 위해 gettable dice 속성과 play() 메서드를 제공한다. (이 dice 속성은 초기화 후 변경할 필요가 없기 때문에 상수 속성으로 선언되며 프로토콜은 가져오기만 가능해야 한다.)

 

The Snakes and Ladders 게임은 클래스의 init()이라는 초기화 메서드를 통해 setup 된다. 모든 게임 로직은 play 프로토콜의 필수 dice 속성을 사용하여 주사위 굴림 값을 제공하는 프로토콜의 메서드로 이동된다.

 

주목해야 할 것은 delegate 프로퍼티가 optional한 DiceGameDelegate로 정의된다는 것. 왜냐하면 delegate는 게임을 play 하기 위해 요구되는 것이 아니기 때문이다. 이것이 optional 타입이기 때문에, delegate 프로퍼티는 자동으로 nil로 초기설정되어 있다. 초기값이 nil로 설정되어 있으므로 게임 instantiator는 프로퍼티를 적절한 delegate로 설정할 수 있는 선택권을 가지게 된다. DiceGameDelegate 프로토콜은 class-only 하기 때문에 당신은 delegate를 weak 으로 선언함으로써 참조주기를 방지할 수 있다.

 

DiceGameDelegate는 게임의 progress를 따라가기 위한 세 가지 메서드를 가진다. 이 세 가지 메서드는 play() 메서드 안에 통합되어 있으며, 새 게임이 시작되거나 새 턴이 시작될 때, 그리고 게임이 종료될 때 호출된다.

 

delegate가 optional한 DiceGameDelegate이기 때문에, play() 메서드는 메서드를 호출할 때마다 옵셔널 체이닝을 사용한다. 만약에 delegate 프로퍼티가 nil인 경우 이런(메서드 호출 - 필자) delegate 호출은 에러 없이 우아하게 실패(fail)한다. 만약 delegate가 nil이 아닌 경우(non-nil), delegate 메서드는 호출되며 SnakesAndLadders 인스턴스를 파라미터로 넘겨준다.

 

다음 예제는 DiceGameTracker 클래스를 보여준다. 이 클래스는 DiceGameDelegate 프로토콜을 상속받는다.

class DiceGameTracker: DiceGameDelegate {
    var numberOfTurns = 0
    func gameDidStart(_ game: DiceGame) {
        numberOfTurns = 0
        if game is SnakesAndLadders {
            print("Started a new game of Snakes and Ladders")
        }
        print("The game is using a \(game.dice.sides)-sided dice")
    }
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
        numberOfTurns += 1
        print("Rolled a \(diceRoll)")
    }
    func gameDidEnd(_ game: DiceGame) {
        print("The game lasted for \(numberOfTurns) turns")
    }
}

 

DiceGameTracker는 DiceGameDelegate가 요구하는 세 가지 메서드를 모두 구현한다. 그리고 이 메서드들은 게임이 몇 번째 턴인지 추적(track)하기 위해 사용된다. 게임이 시작될 때 DiceGameTracker는 numberOfTurns를 0으로 초기화하고 턴이 진행될 때마다 그 값을 하나씩 올린다. 그리고 gameDidEnd 메서드에서 몇 턴 진행했는지를 출력한다.

 

gameDidStart(_ :) 위에 구현된 것은 game 파라미터를 게임에 필요한 기본적인 설명을 print한다. game 파라미터는 SnakesAndLadders가 아니라 DiceGame 타입의 파라미터를 가진다. 이에 따라 gameDidStart(_ :)는 DiceGame 프로토콜의 일부로서만 구현된 메서드와 프로퍼티에 접근하고 사용할 수 있는 것이다. 그러나 메서드는 type casting을 이용하여 기본 인스턴스의 형식에 대해 질문(query)할 수 있다. 이 예에서는 game이 SnakesAndLadders의 인스턴스인지 확인하여 그런 경우 적절한 메시지를 print한다.

 

이 gameDidStart(:) 메서드는 전달된 game parameter 내의 dice 프로퍼티에 접근할 수 있다. gaem이 DiceGame 프로토콜을 준수하는 것으로 되어있기(conformed) 때문에, game은 dice 프로퍼티를 가진다는 것이 보장되고, gameDidStart(\ :) 메서드 또한 dice의 sides 프로퍼티에 접근 및 print 할 수 있게 된다. 어떤 게임이 진행되건 간에 상관없이.

 

Here’s how DiceGameTracker looks in action:

let tracker = DiceGameTracker()
let game = SnakesAndLadders()
game.delegate = tracker
game.play()
// Started a new game of Snakes and Ladders
// The game is using a 6-sided dice
// Rolled a 3
// Rolled a 5
// Rolled a 4
// Rolled a 5
// The game lasted for 4 turns

 


야곰님의 설명

 

공식문서에 나와있는 것만 봐서는 아직 이해가 잘 안 되는 느낌이었다. 그래서 야곰님의 설명을 찾아보기로 했다:

 

야곰님의 정의에 따르면, Delegate는 다음과 같다:

Delegate라는 단어의 뜻에서 예측할 수 있듯이, 델리게이션 디자인 패턴은 하나의 객체가 다른 객체를 대신해 동작 또는 조정할 수 있는 기능을 제공.

또한 부연설명으로

  • 주로 프레임워크 객체가 위임을 요청하며, (주로 애플리케이션 프로그래머가 작성하는)커스텀 컨트롤러 객체가 위임을 받아 특정 이벤트에 대한 기능을 구현합니다.
  • 델리게이션 디자인 패턴은 커스텀 컨트롤러에서 세부 동작을 구현함으로써 동일한 동작에 대해 다양한 대응을 할 수 있게 해줍니다.

라고 설명한다.

 

특히 다음의 그림이 이해에 많은 도움을 주었다.

imgs

사용자의 터치가 이루어질 때 textFieldShouldBeginEditing 메서드는 UITextFieldDelegate에게 해당 텍스트 필드를 편집해도 되는지 묻고, true라는 응답을 받으면 비로소 텍스트를 입력할 수 있게 만든다.

 

이 정도로 delegate에 대해 완벽한 설명이 되지는 않았겠지만, 도움이 많이 되었길 바란다. 그리고 더 delegate 메서드를 활용할 때마다 좀더 수정을 하여 delegate에 대한 완벽한 설명을 할 수 있도록 할 것이다.

 

refs -

https://www.edwith.org/boostcourse-ios/lecture/16856/

https://docs.swift.org/swift-book/LanguageGuide/Protocols.html#//apple_ref/doc/uid/TP40014097-CH25-ID276

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기
// custom