검색결과 리스트
2015/12에 해당되는 글 3건
- 2015.12.29 [swift2.1] Swiftris - swift로 만드는 테트리스 (1)
- 2015.12.22 [swift2.1] Array와 Dictionary 기초
- 2015.12.09 [swift2.1] Swift 프로토콜 지향 프로그래밍 2
글
[swift2.1] Swiftris - swift로 만드는 테트리스
Swiftris는 Swift 언어 버전의 iOS 테트리스 게임입니다. Swift 언어 학습을 위해 개발되었으며 완전한 소스는 GitHub 페이지에서 확인하실 수 있습니다. 간단하지만 임팩트 있는 앱을 만들면서 Swift를 재미있게 공부해보세요.
1. Swiftris 영상
2. 게임조작
- Play: play 버튼 터치.
- Pause: pause 버튼 터치.
- Stop: stop 버튼 터치.
- Move Left: 벽돌의 왼쪽 영역 터치.
- Move Right: 벽돌의 오른쪽 영역 터치.
- Rotate : 벽돌의 상단 영역 터치.
- Drop: 화면 롱 터치.
3. 소스코드 저장소
4. 클래스 구조
- ViewController.swift
- Swiftris.swift
- GameView.swift
- GameBoard.swift
- GameScore.swift
- NextBrick.swift
- Brick.swift
- GameTimer.swift
- SoundManager.swift
GameBoard
- GameBoard는 22행 x 10열로 구성된 UIColor의 이차원 배열입니다.
class GameBoard: UIView {
static let rows = 22
static let cols = 10
...
var board = [[UIColor]]()
...
}
Brick
- 벽돌은 모양을 본따 I, J, L, T, Z, S, O 로 7가지 종류가 있습니다.
- 벽돌종류는 고유의 색을 가진 enum 타입으로 정의됩니다.
enum BrickType {
case I(UIColor)
case J(UIColor)
case L(UIColor)
case T(UIColor)
case Z(UIColor)
case S(UIColor)
case O(UIColor)
}
class Brick: NSObject {
...
static var bricks = [
BrickType.I(UIColor(red:0.40, green:0.64, blue:0.93, alpha:1.0)),
BrickType.J(UIColor(red:0.31, green:0.42, blue:0.80, alpha:1.0)),
BrickType.L(UIColor(red:0.81, green:0.47, blue:0.19, alpha:1.0)),
BrickType.T(UIColor(red:0.67, green:0.45, blue:0.78, alpha:1.0)),
BrickType.Z(UIColor(red:0.80, green:0.31, blue:0.38, alpha:1.0)),
BrickType.S(UIColor(red:0.61, green:0.75, blue:0.31, alpha:1.0)),
BrickType.O(UIColor(red:0.88, green:0.69, blue:0.25, alpha:1.0))
]
...
}
Swiftris
- 게임로직과 인터렉션 처리.
class Swiftris: NSObject {
...
// 터치 - 벽돌 왼쪽,오른쪽 이동, 벽돌 드랍
func touch(touch:UITouch!) {
guard self.gameState == GameState.PLAY else { return }
guard let curretBrick = self.gameView.gameBoard.currentBrick else { return }
let p = touch.locationInView(self.gameView.gameBoard)
let half = self.gameView.gameBoard.centerX
let top = curretBrick.top()
let topY = CGFloat( (Int(top.y) + curretBrick.ty) * GameBoard.brickSize )
if p.y > topY {
if p.x > half {
self.gameView.gameBoard.updateX(1)
} else if p.x < half {
self.gameView.gameBoard.updateX(-1)
}
} else {
self.gameView.gameBoard.rotateBrick()
}
}
// 롱프레스 - 벽돌 드랍
func longPressed(longpressGesture:UILongPressGestureRecognizer!) {
if self.gameState == GameState.PLAY {
if longpressGesture.state == UIGestureRecognizerState.Began {
self.gameView.gameBoard.dropBrick()
}
}
}
}
GameScore
- 게임점수는 클리어되는 라인수에 따라 10점, 30점, 60점, 100점이 지급됩니다.
- 예제에서는 레벨클리어 기능을 제공하지 않았지만, 자신만의 레벨클리어 기능을 구현해보세요.
class GameScore: UIView {
...
var scores = [0, 10, 30, 60, 100]
func lineClear(noti:NSNotification!) {
if let userInfo = noti.userInfo as? [String:NSNumber] {
if let lineCount = userInfo["lineCount"] {
self.lineClearCount += lineCount.integerValue
self.gameScore += self.scores[lineCount.integerValue]
self.update()
}
}
}
...
}NextBrick
- 게임 play중 다음으로 사용할 수 있는 벽돌 3개를 미리 볼수 있습니다.
class Brick: NSObject {
...
static var nextBricks = [Brick]()
static var nextBrickCount = 3
// 벽돌 대기열에서 첫번째(index 0) 벽돌을 사용하고, 대기열을 3개로 채웁니다.
static func generate() -> Brick! {
while self.nextBricks.count < self.nextBrickCount {
self.nextBricks.append(self.newBrick())
}
let brick = self.nextBricks.removeAtIndex(0)
self.nextBricks.append(self.newBrick())
return brick
}
...
}
SoundManager
- 게임의 생생함을 더하기 위해 몇가지 효과음을 제공합니다.
- 게임 배경음, 벽돌 떨어지는음, 게임오버음.
class SoundManager: NSObject {
var bgmPlayer:AVAudioPlayer?
var effectPlayer:AVAudioPlayer?
var gameOverPlayer:AVAudioPlayer?
...
}
5. 피드백
'Swift' 카테고리의 다른 글
| [swift2.2] Xcode 7.3 Swift 언어 변경사항 (0) | 2016.03.24 |
|---|---|
| [swift2.1] Swiftris - swift로 만드는 테트리스 (1) | 2015.12.29 |
| [swift2.1] Array와 Dictionary 기초 (0) | 2015.12.22 |
| [swift2.1] Swift 프로토콜 지향 프로그래밍 2 (0) | 2015.12.09 |
| [swift2] 키워드로 보는 Swift (0) | 2015.11.30 |
| [swift2] Currying 함수 (0) | 2015.11.25 |
설정
트랙백
댓글
글
[swift2.1] Array와 Dictionary 기초
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에 대해 정리해보겠습니다.
'Swift' 카테고리의 다른 글
| [swift2.2] Xcode 7.3 Swift 언어 변경사항 (0) | 2016.03.24 |
|---|---|
| [swift2.1] Swiftris - swift로 만드는 테트리스 (1) | 2015.12.29 |
| [swift2.1] Array와 Dictionary 기초 (0) | 2015.12.22 |
| [swift2.1] Swift 프로토콜 지향 프로그래밍 2 (0) | 2015.12.09 |
| [swift2] 키워드로 보는 Swift (0) | 2015.11.30 |
| [swift2] Currying 함수 (0) | 2015.11.25 |
설정
트랙백
댓글
글
[swift2.1] Swift 프로토콜 지향 프로그래밍 2
이 글은 아래의 자료를 참고하여 작성하였습니다.
지난 시간에 이어, 프로토콜 지향 프로그래밍에 또다른 예제를 살펴보겠습니다. 프로토콜과 구조체가 어떻게 클래스를 대체할 수 있는지 좀더 자세히 알아보기 위해, 도형 그리기를 목적으로 설계된 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
'Swift' 카테고리의 다른 글
| [swift2.1] Swiftris - swift로 만드는 테트리스 (1) | 2015.12.29 |
|---|---|
| [swift2.1] Array와 Dictionary 기초 (0) | 2015.12.22 |
| [swift2.1] Swift 프로토콜 지향 프로그래밍 2 (0) | 2015.12.09 |
| [swift2] 키워드로 보는 Swift (0) | 2015.11.30 |
| [swift2] Currying 함수 (0) | 2015.11.25 |
| [swift2] Core Image Filter 예제 (0) | 2015.11.10 |