[swift2.1] Swift 프로토콜 지향 프로그래밍 2

Swift 2015. 12. 9. 01:00



이 글은 아래의 자료를 참고하여 작성하였습니다.



지난 시간에 이어, 프로토콜 지향 프로그래밍에 또다른 예제를 살펴보겠습니다. 프로토콜과 구조체가 어떻게 클래스를 대체할 수 있는지 좀더 자세히 알아보기 위해, 도형 그리기를 목적으로 설계된 Renderer 를 소개합니다.



프로토콜 Renderer

  • Renderer 프로토콜은 그려지는 대상과 그리는 방법을 알고 있습니다. (어디에 어떻게 그릴까? 도화지와 물감)
  • Renderer는 특정 점으로 이동하고, 라인을 그리고, 호를 그리는 함수 인터페이스를 정의합니다.
protocol Renderer {
    func moveTo(p:CGPoint)
    func lineTo(p:CGPoint)
    func arcAt(center:CGPoint, radius:CGFloat, startAngle:CGFloat, endAngle:CGFloat)
}



프로토콜 Drawble

  • Drawable 프로토콜은 그릴 객체(대상)을 표현합니다. (무엇을 그릴까? 그림체의 데이터를 제공)
  • draw 함수는 renderer 프로토콜이 제공하는 인터페이스를 사용하여 도형의 정보를 제공합니다.
protocol Drawable {
    func draw(renderer:Renderer)
}
  • 이제 Drawable을 구현하는 도형을 정의합니다.



다각형

  • Polygon은 정점들의 배열 corners를 갖습니다.
  • draw 함수에서 가장 마지막 정점부터 이웃하는 정점으로 선을 그려 다각형을 그립니다.
struct Polygon: Drawable {

    func draw(renderer: Renderer) {
        renderer.moveTo(corners.last!)
        for p in corners {
            renderer.lineTo(p)
        }
    }
    var corners:[CGPoint] = []
}



  • Circle은 중점과 반지름을 정의합니다.
  • draw 함수에서 중점을 기준으로 0~360도 호를 그려 원을 그립니다.
let twoPi = CGFloat(M_PI * 2)

struct Circle: Drawable {

    func draw(renderer: Renderer) {
        renderer.arcAt(center, radius: radius, startAngle: 0.0, endAngle: twoPi)
    }
    var center:CGPoint
    var radius:CGFloat
}



다이어그램(Drawable 조합을 그릴수 있는 객체)

  • Diagram은 그릴수 있는 도형 리스트인 elements 를 갖습니다.
  • 도형리스트를 순회하며 도형들에게 draw 함수를 위임합니다.
struct Diagram:Drawable {
    func draw(renderer: Renderer) {
        for f in elements {
            f.draw(renderer)
        }
    }
    var elements:[Drawable] = []
}

지금까지 정의한 도형들을 실제로 그려보아야 겠지요? 첫번째로 컨솔에 단순히 좌표를 찍는 TestRenderer 를 정의해보겠습니다.



컨솔에 그리기

  • TestRenderer는 Renderer의 함수 인터페이스를 구현하며 단순히 파라미터들을 print 문으로 출력합니다.
struct TestRenderer:Renderer {
    func moveTo(p:CGPoint) { print("moveTo(\(p.x), \(p.y))") }

    func lineTo(p:CGPoint) { print("lineTo(\(p.x), \(p.y))") }

    func arcAt(center:CGPoint, radius:CGFloat, startAngle:CGFloat, endAngle:CGFloat) {
        print("arcAt:(\(center), radius: \(radius), startAngle: \(startAngle), endAngle:\(endAngle))")
    }
}

이름은 Renderer인데 컨솔에 좌표만을 출력하고 있으니 영 심심하죠? 이제 실제로 UI로 그리기 위한 Renderer를 구현해보겠습니다.



코어 그래픽으로 그리기

  • 재미있는 점은 CGContext에 그리는 CGContextRenderer를 구현하지 않고, 이미 존재하는 CGContext가 Renderer 프로토콜을 구현하도록 확장했다는 것입니다.
  • 이렇게 swift의 extension을 활용하면 Retroactive modeling 이 가능합니다.

    Retroactive modeling은 원래 코드를 수정하지 않고 타입을 확장할 수 있는 기능입니다.

extension CGContext:Renderer {

    func moveTo(p: CGPoint) {
        CGContextMoveToPoint(self, p.x, p.y)
    }
    func lineTo(p: CGPoint) {
        CGContextAddLineToPoint(self, p.x, p.y)
    }
    func arcAt(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat) {
        let arc = CGPathCreateMutable()
        CGPathAddArc(arc, nil, center.x, center.y, radius, startAngle, endAngle, true)
        CGContextAddPath(self, arc)
        CGContextClosePath(self)
    }
}
  • extension의 또 한가지 재미있는 특징은 프로토콜의 디폴트 구현을 제공할 수 있다는 것입니다.

Renderer 프로토콜에 중점과 반지름만으로 원을 그리는 circleAt 함수를 추가해보겠습니다.

extension Renderer {
    func circleAt(center: CGPoint, radius: CGFloat) {
        arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
    }
}

프로토콜에 저장 속성을 정의할수 없다는 제약이 있지만, 글로벌 상수와 다른 멤버함수를 조합하여 디폴트 구현을 적용할수 있다는 것은 큰 장점이라 할수 있겠죠? 공통의 디폴트 구현을 제공하고, 특정 함수들만 프로토콜 구현체에서 제공하는 방식으로 코드량을 줄일 수 있기 때문입니다.



Playground에서 어떻게 그려지는지 볼까요?

  • 원과 삼각형을 정의합니다.
var circle = Circle(center: CGPoint(x: 187.5, y: 333.5), radius: 93.75)

var triangle = Polygon(corners: [
        CGPoint(x: 187.5,  y: 427.25),
        CGPoint(x: 268.69, y: 286.625),
        CGPoint(x: 106.31, y: 286.625)
    ])
  • RendererView를 정의하고, circle과 triangle을 Diagram으로 그룹핑합니다.
  • drawRect 함수에서 얻은 CGContext는 Renderer 프로토콜을 구현하기 때문에, draw 함수에 전달할 수 있습니다.
class RendererView:UIView {
    var diagram = Diagram(elements: [circle, triangle])

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = UIColor.blackColor()
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.backgroundColor = UIColor.blackColor()
    }
    override func drawRect(rect: CGRect) {
        let ctx = UIGraphicsGetCurrentContext()!

        UIColor.magentaColor().setStroke()
        CGContextSetLineWidth(ctx, 2)

        diagram.draw(ctx)

        CGContextDrawPath(ctx, CGPathDrawingMode.FillStroke)
    }
}




  • 자! 이제 결과를 볼까요?
import UIKit
import XCPlayground

...

var rendererView = RendererView(frame: CGRectMake(0, 0, 320, 568))

// deprecated
// XCPShowView("rendererView", view: rendererView)

XCPlaygroundPage.currentPage.liveView = rendererView




프로토콜 지향 프로그래밍에 대한 감이 조금 오셨나요? 그렇지 않았더라도 사고의 전환을 환기시키는 예제라는 생각이 드네요!


ProtocolOriented2.playground.zip