[swift2.1] Array와 Dictionary 기초

Swift 2015. 12. 22. 00:43

Swift는 컬렉션 타입으로 Array, Dictionary, Set을 가지고 있습니다. 그중 Array와 Dictionary의 사용법을 간략히 알아보겠습니다.


Array

Array는 순서있는 아이템을 저장하는 컬렉션으로, Aarry에 저장되는 아이템들의 값은 중복을 허용합니다.


배열 생성

Array는 아이템의 타입(Int)을 제테릭 인자로 받아 선언할 수 있습니다.

var intArray = Array<Int>()

축약형식을 사용하면 좀더 간단하게 선언할 수 있습니다.

var ints = [Int]()

Array 리터럴을 사용에서 하나 이상의 요소를 갖는 배열을 선언해보겠습니다.

var shoppingList:[String] = ["Eggs", "Milk"]

할당되는 값에서 타입추론이 가능하기 때문에 [String] 타입을 명시하지 않아도 됩니다.

var shoppingList = ["Eggs", "Milk"]  

Array의 count 속성을 사용해서 배열 아이템의 개수를 알수 있습니다.

print("쇼핑리스트에는 \(shoppingList.count)개의 아이템이 들어있습니다.")

isEmpty 속성을 사용하면 배열이 비어있는지 유무를 알수 있습니다.

if shoppingList.isEmpty {
   print("쇼피리스트가 비어있습니다.")
} else {
   print("쇼피리스트가 비어있지 않습니다.")
}


아이템 추가

쇼핑리스트에 아이템을 추가해보겠습니다.

shoppingList.append("Flour")  
// ["Eggs", "Milk", "Flour"]

shoppingList += ["Baking Powder"]
// ["Eggs", "Milk", "Flour", "Baking Powder"]

이번에는 여러 아이템을 동시에 추가해보겠습니다.

shoppingList += ["Chocolate Spread", "Cheese", "Butter"]  
// ["Eggs", "Milk", "Flour", "Baking Powder", "Chocolate Spread", "Cheese", "Butter"]


아이템 삽입

이번에는 특정 인덱스에 아이템을 삽입해보겠습니다.

shoppingList.insert("Maple Syrup", atIndex: 0)
// ["Maple Syrup", "Eggs", "Milk", "Flour", "Baking Powder", "Chocolate Spread", "Cheese", "Butter"]


아이템 확인

인덱스를 사용해 배열 아이템에 접근할 수 있습니다.

var firstItem = shoppingList[0]  
// firstItem은 "Maple Syrup"

first 속성은 배열의 첫번째 아이템입니다.

if let first = shoppingList.first {
    // first는 "Maple Syrup"
}


아이템 변경

몇 가지 쇼핑품목을 변경해보겠습니다.

shoppingList[1] = "Six eggs"  
// ["Maple Syrup", "Six eggs", "Milk", "Flour", "Baking Powder", "Chocolate Spread", "Cheese", "Butter"]

range 연산자를 사용하면 특정범위의 아이템을 한번에 교체할 수 있습니다. 다음 구문은 4~6 인덱스 범위의 3개의 아이템을 ["Banana", "Apples"] 로 교체합니다.

shoppingList[4...6] = ["Bananas", "Apples"]
// ["Maple Syrup", "Six eggs", "Milk", "Flour", "Bananas", "Apples", "Butter"]

Array의 인덱스 범위를 초과하여 접근하면 Array index out of range 런타임 에러가 발생합니다.


아이템 삭제

배열의 첫번째와 마지막 아이템을 삭제해보겠습니다.

// 첫번째 아이템 삭제
let mapleSyrup = shoppingList.removeAtIndex(0)  

// 마지막 아이템 삭제 
let butter = shoppingList.removeLast()

// ["Six eggs", "Milk", "Flour", "Bananas", "Apples"]


배열 순회

가장 간단한 순회방법은 for 문을 사용하는 방법입니다.

for var i=0; i < shoppingList.count; i++ {
    print(shoppingList[i])
}

인덱스를 사용할 필요가 없다면, 좀더 심플한 for-in 문을 사용합니다.

for item in shoppingList {
    print(item)
}

나열자를 사용하면 좀더 심풀한 문법으로 순회가 가능합니다.

for (index, value) in shoppingList.enumerate() {
    print("index \(index) : \(value)")
}    

반대방향으로의 순회도 가능합니다.

for (index, value) in shoppingList.enumerate().reverse() {
    print("index \(index) : \(value)")
}

SequenceType 타입의 forEach 함수를 사용해서도 배열을 순회할수 있습니다. (Array는 SequenceType 프로토콜을 구현하고 있음)

// forEach 함수에 trailing 클로져로 반복해서 처리할 몸체를 전달합니다.  
shoppingList.forEach {
    print($0)
}

forEach 함수는 루프가 아니기 때문에 몸체안에서 continue 또는 break 문을 사용하여 도중에 중단하거나 건너뛸수 없습니다. return 문을 사용해서 루프를 건너뛸수는 있습니다.


초기값을 갖는 Array 생성

count, repeatedValue 인자를 받는 생성자를 사용하여 초기화된 배열을 생성할수 있습니다.

var threeDoubles = [Double](count: 3, repeatedValue: 0.0)

var anotherThreeDoubles = Array(count: 3, repeatedValue: 2.5)    




Dictionary

Dictionary는 순서가 없는 키,값 쌍을 저장하는 자료구조입니다. Hashable 프로토콜을 구현하는 타입만이 Dictionary의 키로 사용될 수 있습니다. String, Int, Float, Double, Bool 및 연관값이 없는 enum 은 모두 디폴트로 Hashable 프로토콜을 구현하기 때문에 키로 사용될 수 있습니다.


Dictionary 생성

String 타입의 키와 값을 갖는 배열을 생성해보겠습니다.

var airports:Dictionary<String, String> = ["TYO":"Tokyo", "DUB":"Dublin"]

축약형태의 사용도 가능합니다.

var dictionary = [String:String]()

var airports = ["TYO":"Tokyo", "DUB":"Dublin"]


아이템의 개수

count 속성은 Dictionary에 저장된 아이템의 개수입니다.

print("airports 사전에는 \(airports.count) 개의 아이템을 포함하고 있습니다.")


키-값 설정

키-값 쌍을 추가해보겠습니다.

airports["LHR"] = "London"
airports["APL"] = "Apple International"


키-값 조회

Dictionary는 특정 키-값 쌍이 없을 수 있기 때문에 Optional 타입으로 반환됩니다.

airports["LHR"] = "London"  

print(airports["LHR"])
// Optional("London")

Dictionary에서 키값 조회는 안전하게 옵셔널 바인딩을 사용합니다.

if let airportName = airports["DUB"] {
    print("공합 이름은 \(airportName) 입니다.")
} else {
    print("해당 공항은 사전에 포함되어 있지 않습니다.")
}


키-값 변경

updateValue 함수를 사용하면 값을 변경하면서 이전 값을 반환해줍니다.

if let oldValue = airports.updateValue("Doublin International", forKey: "DUB") {
    print("DUB의 이전 값은 \(oldValue) 입니다.")
}


키-값 삭제

해당키의 값을 nil로 설정하여 삭제할 수 있습니다.

airports["APL"] = nil

print(airports["APL"])
// nil

삭제되는 값을 참조해야 한다면 removeValueForKey 함수를 사용합니다.

if let removedValue = airports.removeValueForKey("DUB") {
    print("삭제되는 값은 \(removedValue) 입니다.")
} else {    
    print("공항사전은 DUB 값을 포함하지 않습니다.")
}


Dictionary 순회

for-in 문을 사용해서 키,값을 순회할 수 있습니다.

for (airportCode, airportName) in airports {
    print("\(airportCode): \(airportName)")
}

keys 속성을 통해 Dictionary의 모든 키 배열을 얻어올 수 있습니다.

for airportCode in airports.keys {
    print("공항 코드: \(airportCode)")
}

values 속성으로 모든 값의 배열을 얻어올 수 있습니다.

for airportName in airports.values {
    print("공항 이름: \(airportName)")
}



Dictionary의 복사동작

Dictionary가 다른 변수/상수에 할당 되거나, 함수의 인자로 전달될 때 어떤 일이 발생할까요?

Value 타입은 값이 복사되고, Reference 타입은 참조가 복사된다

위와 같이 Swift의 기본규칙을 이해하면 Dictionary의 복사동작을 쉽게 이해할 수 있습니다. Dictionary의 키와 값들은 value 타입이면 복사되고, 클래스와 같은 참조타입이면 참조변수가 복사됩니다.

var ages = [ "Peter":23, "Wei":35, "Anish":65, "Katya":19 ]

ages Dictionary 를 다른 변수에 할당해보겠습니다.

var copiedAges = ages

할당문을 만나는 순간 ages를 이루는 키,값들은 모두 복사됩니다.(String, Int는 value 타입.)

copiedAges["Peter"] = 24

print(copiedAges["Peter"]) // 24
print(ages["Peter"])       // 23

copiedAges Dictionary의 "Peter" 키의 값을 변경해도 ages의 동일 키의 값이 변경되지 않는 것을 통해 Dictionary가 복사되었음을 알 수 있습니다.



Array의 복사동작

Array의 동작도 Dictionary와 유사합니다. 할당 또는 함수의 인자로 전달되면 아이템이 value 타입인 경우 복사됩니다.

var a = [1, 2, 3]
var b = a
var c = a  

배열 a를 변수 b와 c에 할당합니다.

print(a[0]) // 1
print(b[0]) // 1
print(c[0]) // 1

배열 a의 첫번째 아이템을 변경해보겠습니다.

a[0] = 42

b, c의 첫번째 아이템들은 어떻게 되었을까요?

print(a[0]) // 42
print(b[0]) // 1
print(c[0]) // 1

b,c 모두 첫번째 아이템이 변경되지 않았습니다. 즉 할당과 동시에 복사가 이루어졌다는 것을 알수 있습니다. Swift 레퍼런스 문서에서는 성능 최적화의 일환으로 "Array의 크기를 변경하는 연산이 발생한 경우에만 복사가 이루어진다" 고 하는데, 문서가 업데이트 되지 않은 건지, 최적화 옵션에 따라 동작이 다른건지는 의문입니다.

다음 시간에는 Set에 대해 정리해보겠습니다.





[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





[swift2.1] Xcode7.1 Playground 변경사항

Swift 2015. 10. 27. 21:19

Xcode7.1 에서 변경된 Playground 기능에 대해 알아보겠습니다.


1. 파일, 이미지, 컬러를 editor로 드래그해서 객체 리터럴을 생성할 수 있습니다.

  • 리터럴은 플랫폼 특정 타입으로 아래와 같이 변환됩니다.
  • Color 리터럴 -> NSColor 또는 UIColor
  • File 리터럴 -> NSURL
  • Image 리터럴 -> NSImage 또는 UIImage


이미지 리터럴

  • Resources 폴더에서 이미지 파일을 에디터로 드래그 하면 그림과 같은 리터럴 형태로 추가됩니다.

UIImage 객체의 imageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate) 메소드는 템플릿 형태로 렌더링 된 UIImage를 반환합니다. 검정-투명 으로만 이루어진 이미지에서 black 영역을 UIImageView 의 tintColor 속성을 사용해서 원하는 색으로 변경할수 있습니다.




컬러 리터럴

  • Editor > Insert Color Literal 메뉴를 선택하면 컬러 선택창이 나타나 컬러를 변경할 수 있습니다.




파일 리터럴

  • Resources 폴더의 파일을 드래그하면, 아래와 같이 파일 리터럴 형태로 추가됩니다.






2. XCPlayground 가 제공하는 API가 상당수 변경되었습니다.

  • API들이 전역함수에서 XCPlaygroundPage 클래스의 메소드 형태로 변경되었습니다.

API 변경사항으로 XCPCaptureValue, XCPShowView, XCPSetExecutionShouldContinueIndefinitely, XCPExecutionShouldContinueIndefinitely 함수와 XCPSharedDataDirectoryPath 전역상수는 deprecated 되었음. Xcode 이후 버전에서 제거될 예정.


1) 현재 페이지에 대한 참조는 XCPlaygroundPage.currentPage 를 사용합니다.

import XCPlayground

XCPlaygroundPage.currentPage 


2) 타임라인으로 값을 캡처하려면XCPlaygroundPage.captureValue(_:withIdentifier:) 을 사용합니다.

// XCPCaptureValue("식별자", value: i) : deprecated

for i in 1...10 {
    XCPlaygroundPage.currentPage.captureValue(i, withIdentifier: "식별자")
}  




3) 비동기 코드를 포함할 경우 XCPlaygroundPage.needsIndefiniteExecution 을 사용하세요.

  • XCPlaygroundPage.needsIndefiniteExecution 를 true로 설정하면, 실행흐름이 playground 파일 끝에 도달해도 실행을 중단하지 않고, 대기합니다.
// XCPSetExecutionShouldContinueIndefinitely(true) : deprecated

XCPlaygroundPage.currentPage.needsIndefiniteExecution = true




4) XCPlaygroundPage.liveView 를 설정하면 타임라인에 노출됩니다.

  • liveView 속성에 nil이 아닌 값을 설정하면 .needsIndefiniteExecution 가 true 로 설정됩니다.
  • StackView 를 타임라인에 노출하는 예제입니다.

class StackView: UIView {
    var kElementHeight = CGFloat(50)
    var elements:[Int] = []

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = UIColor.blackColor()
        super.layer.borderColor = UIColor.blueColor().CGColor
        super.layer.borderWidth = 1
        self.layout()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func layout() {
        self.subviews.map { $0.removeFromSuperview() }

        for i in 0..<self.elements.count {
            let value = self.elements[i]

            let elementLabel = UILabel(frame:CGRectZero)
            elementLabel.backgroundColor = UIColor.greenColor()
            elementLabel.text = "\(value)"
            elementLabel.textAlignment = NSTextAlignment.Center
            elementLabel.font = UIFont.systemFontOfSize(15)
            elementLabel.textColor = UIColor.whiteColor()
            self.addSubview(elementLabel)

            elementLabel.frame = CGRectMake(8, self.frame.size.height-(8+kElementHeight)*CGFloat(i+1), self.frame.size.width-8*2, kElementHeight)
        }
    }

    func push(element:Int) {
        self.elements.append(element)
        self.layout()
    }
    func pop() -> Int? {
        if self.elements.count > 0 {
            let topElement = self.elements.removeLast()
            self.layout()
            return topElement
        } else {
            return nil
        }
    }
}

let bounds = CGRectMake(0, 0, 320, 400)
let view = UIView(frame: bounds)
view.backgroundColor = UIColor.whiteColor()

// liveView 속성을 설정하면, Timeline 에 표시됩니다.
// XCPlaygroundPage.currentPage.needsIndefiniteExecution = true 가 자동으로 설정됩니다.

XCPlaygroundPage.currentPage.liveView = view

let stackView = StackView(frame: CGRectInset(view.bounds, 30, 30))
view.addSubview(stackView)

stackView.push(1)
stackView.push(2)
stackView.push(3)  




5) NSURL 객체인 XCPlaygroundSharedDataDirectoryURL 전역상수가 추가되었습니다.

플랫폼 설정이 OS X 인 경우에만 유효한 상수입니다.

  • XCPlaygroundSharedDataDirectoryURL 는 playground 공유 데이터 디렉토리 경로를 나타냅니다.
  • OS X 에서는 ~/Document 경로에 Shared Playground Data 디렉토리를 수동으로 생성해야 합니다.
  • iOS 플랫폼에서는 playground 파일마다 경로가 다르기 때문에 데이터를 공유할 수 없습니다.






3. XCPlayground는 라이브 뷰로 뷰컨트롤러를 지원합니다.

LiveView 는 XCPlaygroundLiveViewable 프로토콜을 구현하는 객체입니다.

  • UIView, UIViewController 는 디폴트로 XCPlaygroundLiveViewable 프로토콜을 구현합니다.
  • XCPlaygroundLiveViewable 프로토콜 명세는 아래와 같습니다.
public protocol XCPlaygroundLiveViewable {
    public func playgroundLiveViewRepresentation() -> XCPlayground.XCPlaygroundLiveViewRepresentation
}

public enum XCPlaygroundLiveViewRepresentation {
    // 루트뷰를 갖지 않는 최상위뷰 
    case View(UIView)

    // 루트 뷰컨트롤러를 갖지 않는 최상위 뷰컨트롤러 
    case ViewController(UIViewController)
}
  • 위에서 구현했던 StackView 를 활용하여 네비게이션 컨트롤러를 라이브 뷰로 설정하는 예제입니다.
let stackView = StackView(frame: CGRectMake(0, 0, 200, 400))
stackView.push(1)
stackView.push(2)
stackView.push(3)

let contentViewController = UIViewController()
contentViewController.title = "StackView"
contentViewController.view.addSubview(stackView)

let liveViewController = UINavigationController(rootViewController: contentViewController)

XCPlaygroundPage.currentPage.liveView = liveViewController






4. XCPlaygroundPage.currentPage.finishExecution()를 호출해서 프로그래밍적으로 playground 실행을 중단 할 수 있습니다.

  • 이 메서드는 Xcode가 현재 playground 페이지 실행을 중단하도록 지시하여, 적절히 클린업을 수행하고 종료할 수 있도록 합니다.
XCPlaygroundPage.currentPage.finishExecution()