티스토리 뷰

iOS/Swift

[Swift] Protocol (2)

HarryJeonn 2022. 12. 16. 22:10

위임 (Delegation)

위임은 클래스 혹은 구조체 인스턴스에 특정 행위에 대한 책임을 넘길 수 있게 해주는 디자인 패턴 중 하나이다.

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의 행위와 관련된 구현을 DiceGameDelegate를 따르는 인스턴스에 위임한다. DiceGameDelegate를 AnyObject로 선언하면 클래스만 이 프로토콜을 따를 수 있게 만들 수 있다.

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)
    }
}

SnakesAndLadders는 DiceGame을 따르고 DiceGameDelegate를 따르는 delegate를 갖는다. 게임을 실행(play())했을 때 delegate?.gameDidStart(self), delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll), delegate?.gameDidEnd(self) 를 실행한다. delegate는 게임을 진행시키는데 반드시 필요한건 아니라서 optional로 정의되어 있다.

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")
    }
}

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

실제 DiceGameDelegate를 상속하는 delegate DiceGameTracker를 구현한 예다.

DiceGameTracker를 이용해 게임을 진행시킨다. 게임의 tracking 관련된 작업은 이 DiceGameTracker가 위임받아 그곳에서 실행된다.

익스텐션을 이용해 프로토콜 따르게 하기 (Adding Protocols Conformance with an Extension)

이미 존재하는 타입에 새 프로토콜을 따르게 하기 위해 extension을 사용할 수 있다. 원래 값에 접근 권한이 없어도 extension을 사용해 기능을 확장할 수 있다.

protocol TextRepresentable {
    var textualDescription: String { get }
}

extension Dice: TextRepresentable {
    var textualDescription: String {
        return "A \\(sides)-sided dice"
    }
}

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// Prints "A 12-sided dice"

extension을 이용해 Dice를 TextRepresentable 프로토콜을 따르도록 구현했다.

d12.textualDescription 와 같이 Dice에 추가한 extension을 자동으로 그대로 사용할 수 있다.

조건적으로 프로토콜을 따르기 (Conditionally Conforming to a Protocol)

특정 조건을 만족시킬 때만 프로토콜을 따르도록 제한할 수 있다. where절을 사용해 정의한다.

extension Array: TextRepresentable where Element: TextRepresentable {
    var textualDescription: String {
        let itemsAsText = self.map { $0.textualDescription }
        return "[" + itemsAsText.joined(separator: ", ") + "]"
    }
}
let myDice = [d6, d12]
print(myDice.textualDescription)
// Prints "[A 6-sided dice, A 12-sided dice]"

TextRepresentable을 따르는 Array 중 Array의 각 원소가 TextRepresentable인 경우에만 따르는 프로토콜을 정의한다.

익스텐션을 이용해 프로토콜 채용 선언하기 (Declaring Protocol Adoption with an Extension)

어떤 프로토콜을 충족에 필요한 모든 조건을 만족하지만 아직 그 프로토콜을 따른다는 선언을 하지 않았다면 그 선언을 빈 extension으로 선언할 수 있다. 아래 코드는 프로토콜을 따른다는 선언은 extension에 하고 실제 프로토콜을 따르기 위한 구현은 구조체 원본에 구현한 예다.

struct Hamster {
    var name: String
    var textualDescription: String {
        return "A hamster named \\(name)"
    }
}
extension Hamster: TextRepresentable {}

let harryTheHamster = Hamster(name: "Harry")
let somethingTextRepresentable: TextRepresentable = harryTheHamster
print(somethingTextRepresentable.textualDescription)
// Prints "A hamster named Harry"

프로토콜 타입 컬렉션 (Collections of Protocol Types)

프로토콜을 Array, Dictionary 등 Collection 타입에 넣기위한 타입으로 사용할 수 있다.

let things: [TextRepresentable] = [game, d12, harryTheHamster]

for thing in things {
    print(thing.textualDescription)
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dice
// A hamster named Simon

Array의 모든 객체는 TextRepresentable을 따르므로 textualDescription 프로퍼티를 갖는다.

프로토콜 상속 (Protocol Inheritance)

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // protocol definition goes here
}

클래스를 상속하듯이 프로토콜도 상속할 수 있다.

protocol PrettyTextRepresentable: TextRepresentable {
    var prettyTextualDescription: String { get }
}

TextRepresentable 프로토콜을 상속받아 PrettyTextRepresentable을 구현한다.

extension SnakesAndLadders: PrettyTextRepresentable {
    var prettyTextualDescription: String {
        var output = textualDescription + ":\\n"
        for index in 1...finalSquare {
            switch board[index] {
            case let ladder where ladder > 0:
                output += "▲ "
            case let snake where snake < 0:
                output += "▼ "
            default:
                output += "○ "
            }
        }
        return output
    }
}

print(game.prettyTextualDescription)
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○

square가 0보다 큰지, 작은지 혹은 0인지에 대해 각각 알맞은 기호를 반환한다.

클래스 전용 프로토콜 (Class-Only Protocols)

protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
    // class-only protocol definition goes here
}

클래스 타입에서만 사용 가능한 프로토콜을 선언하기 위해서는 AnyObject를 추가한다.

프로토콜 합성 (Protocol Composition)

동시에 여러 프로토콜을 따르는 타입을 선언할 수 있다.

protocol Named {
    var name: String { get }
}
protocol Aged {
    var age: Int { get }
}
struct Person: Named, Aged {
    var name: String
    var age: Int
}
func wishHappyBirthday(to celebrator: Named & Aged) {
    print("Happy birthday, \\(celebrator.name), you're \\(celebrator.age)!")
}
let birthdayPerson = Person(name: "Harry", age: 26)
wishHappyBirthday(to: birthdayPerson)
// Prints "Happy birthday, Harry, you're 26!"

wishHappyBirthday 메소드의 celebrator 파라미터는 Named 프로토콜과 Aged 프로토콜을 동시에 따르는 타입으로 선언하기 위해 & 를 사용했다.

프로토콜 순응 확인 (Checking for Protocol Conformance)

어떤 타입이 특정 프로토콜을 따르는지 확인할 수 있다.

  • is 연산자를 이용하면 어떤 타입이 특정 프로토콜을 따르는지 확인할 수 있다. 특정 프로토콜을 따르면 true을 그렇지 않으면 false을 반환한다.
  • as? 는 특정 프로토콜을 따르는 경우 그 옵셔널 타입의 프로토콜 타입으로 다운캐스트 하게되고 따르지 않으면 nil을 반환한다.
  • as! 는 강제로 특정 프로토콜을 따르도록 정의한다. 만약 다운캐스트에 실패하면 에러가 발생한다.
protocol HasArea {
    var area: Double { get }
}

class Circle: HasArea {
    let pi = 3.1415927
    var radius: Double
    var area: Double { return pi * radius * radius }
    init(radius: Double) { self.radius = radius }
}
class Country: HasArea {
    var area: Double
    init(area: Double) { self.area = area }
}

class Animal {
    var legs: Int
    init(legs: Int) { self.legs = legs }
}

let objects: [AnyObject] = [
    Circle(radius: 2.0),
    Country(area: 243_610),
    Animal(legs: 4)
]

for object in objects {
    if let objectWithArea = object as? HasArea {
        print("Area is \\(objectWithArea.area)")
    } else {
        print("Something that doesn't have an area")
    }
}
// Area is 12.5663708
// Area is 243610.0
// Something that doesn't have an area

area 값을 필요로 하는 HasArea 프로토콜을 선언했다.

HasArea 프로토콜을 따르고 있는 Circle, Country 클래스를 선언한다.

그리고 HasArea 프로토콜을 따르지 않는 Animal 클래스도 선언했다.

클래스들을 배열에 넣고 배열을 순회하며 as? HasArea 구문을 사용해 HasArea 프로토콜을 따르는지 확인한다.

HasArea를 따르는 클래스인 Circle, Country 클래스는 area값이 반환되고 그렇지 않은 Animal 클래스는 else 절로 실행된다.

선택적 프로토콜 요구조건 (Optional Protocol Requirements)

프로토콜을 선언하면서 필수 구현이 아닌 선택적 구현 조건을 정의할 수 있다.

@objc 키워드를 프로토콜 앞에 붙이고 개별 함수 혹은 프로퍼티에는 @objc와 optional 키워드를 붙인다.

@objc 프로토콜은 클래스 타입에서만 따를 수 있고 구조체나 열거형에서는 사용할 수 없다.

@objc protocol CounterDataSource {
    @objc optional func increment(forCount count: Int) -> Int
    @objc optional var fixedIncrement: Int { get }
}

class Counter {
    var count = 0
    var dataSource: CounterDataSource?
    func increment() {
        if let amount = dataSource?.increment?(forCount: count) {
            count += amount
        } else if let amount = dataSource?.fixedIncrement {
            count += amount
        }
    }
}

class ThreeSource: NSObject, CounterDataSource {
    let fixedIncrement = 3
}

var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
    counter.increment()
    print(counter.count)
}
// 3
// 6
// 9
// 12

Counter 클래스에서는 increment 함수만 구현했고, ThreeSource 클래스에서는 fixedIncrement만 구현이 가능하다.

그리고 counter객체를 만들어 ThreeSource객체를 사용해 값을 할당하면 Counter의 count를 증가 시킬 수 있다.

프로토콜 익스텐션 (Protocol Extensions)

extension을 이용해 프로토콜을 확장할 수 있다.

extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}

let generator = LinearCongruentialGenerator()
print("Here's a random number: \\(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And here's a random Boolean: \\(generator.randomBool())")
// Prints "And here's a random Boolean: true"

RandomNumberGenerator 프로토콜을 확장해 randomBool()을 구현했다.

generator에서 random()과 randomBool() 모두 사용 가능하다.

extension을 이용해 구현을 추가할 수는 있어도 다른 프로토콜로 확장/상속할 수 없다. 그렇게 하려면 extension이 아닌 프로토콜 자체에 구현해야한다.

기본 구현 제공 (Providing Default Implementations)

extension을 기본 구현을 제공하는데 사용할 수 있다. 특정 프로토콜을 따르는 타입 중에서 그 프로토콜의 요구사항에 대해 자체적으로 구현한게 있으면 그것을 사용하고 아니면 기본 구현을 사용하게 된다.

즉, 프로토콜에서는 선언만 할 수 있는데익스텐션을 이용해 기본 구현을 제공할 수 있다.

extension PrettyTextRepresentable  {
    var prettyTextualDescription: String {
        return textualDescription
    }
}

'iOS > Swift' 카테고리의 다른 글

[Swift] 고차함수 (2) - flatMap, compactMap  (0) 2022.12.28
[Swift] 고차함수 (1) - map, filter, reduce  (0) 2022.12.26
[Swift] Protocol (1)  (0) 2022.12.14
[Swift] 열거형 (Enumerations)  (1) 2022.12.07
[Swift] Strong, Weak, Unowned 참조  (0) 2022.12.05
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함