트랙
/
Swift
Swift
/
연습 문제
/
창 관리 시스템
창 관리 시스템

창 관리 시스템

학습 연습 문제

소개

구조체와 클래스

구조체와 클래스는 Swift 프로그래밍을 이루는 주요 구성 요소 두 가지예요. 둘 다 관련된 데이터와 함수를 하나의 독립적인 기능 단위로 묶는 방법이고요. 그리고 구조체나 클래스를 정의한다는 건, 이미 다뤄 본 Int나 String 같은 타입을 사용하듯이, Swift 안에서 사용할 새로운 타입을 정의하는 거예요.

Swift에서 구조체와 클래스는 비슷한 점이 아주 많아요. 그중에서도 둘 다 값을 _프로퍼티_에 저장하고, _메서드_를 통해 기능을 제공할 수 있어요.

각각 추가적인 기능도 제공하지만, 그 부분은 이 연습 문제의 범위를 벗어나요.

구조체와 클래스 정의하기

구조체와 클래스는 거의 같은 방식으로 정의해요. 알맞은 키워드로 시작해서 정의하려는 타입의 이름을 쓰고, 그다음 중괄호 사이에 구조체나 클래스의 본문을 작성해요.

본문은 저장 프로퍼티로 구성될 수 있는데, 저장 프로퍼티는 일반 상수나 변수와 똑같이 정의되고 동작해요.

struct CharacterStats {
  var health = 0.0
  var speed = 0
  var strength = 0
}

class GameCharacter {
  var stats = CharacterStats()
  var characterClass: String?
  var name: String?
  var active = false
  let id = makeRandomID()
}

인스턴스

위에서 언급했듯이, 구조체나 클래스를 정의하는 건 새로운 _타입_을 정의하는 것일 뿐이에요. 그 타입의 값이 어떤 모습일지에 대한 설계도일 뿐이고, 실제로 작업할 수 있는 그 타입의 값을 만들어 주지는 않아요.

그 타입의 _인스턴스_를 만들려면 타입 이름 뒤에 괄호 두 개를 써야 해요.

let someStats = CharacterStats()
let someCharacter = GameCharacter()

이렇게 하면 정의에서 제공한 기본값으로 프로퍼티가 채워진, 해당 타입의 값이 만들어져요. GameCharacter의 name 프로퍼티처럼 옵셔널인 경우에는 값을 제공하지 않으면, 값을 바로 주지 않고 일반 옵셔널 타입을 정의할 때처럼 그 프로퍼티의 기본값이 nil이 돼요.

구조체에서는 Swift가 자동으로 _멤버와이즈 이니셜라이저_라는 것을 제공하는데, 괄호 안에 구조체 프로퍼티의 값을 제공하면 정의의 기본값을 덮어쓸 수 있어요.

let differentStats = CharacterStats(health: 100.0, speed: 6, strength: 18)

프로퍼티 접근하기

구조체와 클래스의 프로퍼티는 _점 표기법_으로 접근할 수 있는데, 값의 이름 뒤에 점(.)을 찍고 프로퍼티 이름을 쓰는 방식이에요. 구조체나 클래스의 프로퍼티가 자체적인 프로퍼티를 가지고 있다면, 이 점 표기법으로 중첩된 프로퍼티에도 접근할 수 있어요.

이 표기법은 프로퍼티의 값을 가져올 때도, 허용된다면 값을 바꿀 때도 사용할 수 있어요.

someStats.health
// => 0
someCharacter.name
// => nil

someStats.health = 87.3
someStats.health
// => 87.3
someCharacter.name = "Luther"
someCharacter.name
// => "Luther"

someCharacter.id  = "new id"
// Error: Cannot assign to property: 'id' is a 'let' constant

메서드

구조체와 클래스에 데이터를 저장하는 프로퍼티처럼, 구조체나 클래스에 함수를 저장하는 _메서드_도 정의할 수 있어요.

메서드는 일반 함수와 같은 방식으로 정의하는데, 구조체나 클래스의 본문 안에만 정의해요. 구조체에서 함수가 프로퍼티의 값을 바꾼다면, 함수 앞에 mutating 키워드를 붙여야 한다는 점에 유의해요. 또한 프로퍼티가 메서드에 의해 바뀔 수 있다면, 그 프로퍼티는 일반 변수처럼 let 대신 var로 정의해야 해요.

struct CharacterStats {
  var health = 0.0
  var speed = 0
  var strength = 0

  mutating func takeHit(_ damage: Double) {
    health = max(0.0, health - damage)
  }
  func canLift(_ weight: Int) -> Bool {
    weight < strength * 100
  }
}

class GameCharacter {
  var stats = CharacterStats()
  var characterClass: String?
  var name: String?
  var active = false
  let id: String = makeRandomID()

  func takesDamage(_ damage: Double) {
    stats.takeHit(damage)
    if stats.health <= 0 {
      active = false
    }
  }
  func sayName() -> String {
    return "My name is \(name ?? "no one"), my class is \(characterClass ?? "undetermined")"
  }
  func lift(_ weight: Int) -> String {
    if stats.canLift(weight) {
      return "No problem!"
    } else {
      return "Ooof! No way."
    }
  }
}

이 메서드들은 프로퍼티처럼 점 표기법으로 호출할 수 있어요.

var myChar = GameCharacter()
myChar.stats = CharacterStats(health: 72.8, speed: 19, strength: 6)
myChar.active = true

myChar.lift(750)
// => "Ooof! No way."
myChar.takesDamage(80)
myChar.active
// => false

self

구조체나 클래스의 모든 인스턴스에는 self라는 암시적 프로퍼티가 있는데, 이는 인스턴스 자기 자신을 가리켜요. self는 주로 인스턴스 프로퍼티와 이름이 같은 매개변수를 구분할 때 가장 많이 사용해요.

struct Position {
    var x = 0

    mutating func move(x: Int) {
        // Here, 'self.x' refers to the struct's property,
        // while 'x' refers to the parameter passed to the method.
        self.x = x
    }
}

Swift 타입에는 대문자로 시작하는 Self 키워드도 있는데, 이는 인스턴스 자체가 아니라 인스턴스의 타입을 가리켜요.

지침

이 연습 문제에서는 윈도우 기반 컴퓨터 시스템을 시뮬레이션해요. 이동하고 크기를 조절할 수 있으며 내용을 표시하는 창을 만들어 봐요. 다음 그림은 아래에서 다룰 값들을 나타내요.

                  <--------------------- screenSize.width --------------------->

       ^          ╔════════════════════════════════════════════════════════════╗
       |          ║                                                            ║
       |          ║          position.x,_                                      ║
       |          ║          position.y  \                                     ║
       |          ║                       \<----- size.width ----->            ║
       |          ║                 ^      *──────────────────────┐            ║
       |          ║                 |      │        title         │            ║
       |          ║                 |      ├──────────────────────┤            ║
screenSize.height ║                 |      │                      │            ║
       |          ║            size.height │                      │            ║
       |          ║                 |      │       contents       │            ║
       |          ║                 |      │                      │            ║
       |          ║                 |      │                      │            ║
       |          ║                 v      └──────────────────────┘            ║
       |          ║                                                            ║
       |          ║                                                            ║
       v          ╚════════════════════════════════════════════════════════════╝

1. Size 구조체 정의하기

width와 height라는 두 개의 Int 프로퍼티를 가진 Size라는 구조체를 정의해요. 각각 창의 현재 너비와 높이를 저장해요. 초기 너비와 높이는 각각 80과 60이어야 해요. 새 너비와 높이 매개변수를 받아 프로퍼티를 새 크기에 맞게 변경하는 resize(newWidth:newHeight:) 메서드를 포함해요.

let size1080x764 = Size(width: 1080, height: 764)
// Returns Size
var size1200x800 = size1080x764
// Returns Size
size1200x800.resize(newWidth: 1200, newHeight: 800)
size1200x800.height
// Returns 800

2. Position 구조체 정의하기

x와 y라는 두 개의 Int 프로퍼티를 가진 Position이라는 구조체를 정의해요. 각각 창의 왼쪽 위 모서리의 현재 가로 위치와 세로 위치를 저장해요. x와 y의 초기값은 각각 0이어야 해요. 위치 (0, 0)은 화면의 왼쪽 위 모서리이고, 오른쪽으로 갈수록 x 값이 커지고 아래로 갈수록 y 값이 커져요.

새 x와 y 매개변수를 받아 프로퍼티를 새 위치에 맞게 변경하는 moveTo(newX:newY:) 메서드를 포함해요.

var point = Position(x: 10, y: 20)
// Returns Position
point.moveTo(newX: 100, newY: -100)
point.y
// Returns -100

3. Window 클래스 정의하기

다음 프로퍼티를 가진 창 클래스를 정의해요:

  • title : String, 초기값은 "New Window"
  • screenSize : Size, width = 800이고 height = 600인 상수값
  • size : Size, 초기값은 Size 구조체의 기본값
  • position : Position, 초기값은 Position 구조체의 기본값
  • contents : String?, 초기값은 nil

클래스에 대한 빈 이니셜라이저도 정의해야 해요.

let window = Window()
window.title
// Returns "New Window"

4. 창 크기를 조절하는 메서드 추가하기

  • resize(to:) : (Size) -> () - 이 메서드는 Size 구조체를 입력으로 받아 창을 지정된 크기로 조절하려고 해요. 하지만 새 크기는 특정 범위를 넘을 수 없어요.
  • 허용되는 최소 높이와 너비는 1이에요. 1보다 작게 요청된 높이나 너비는 1로 잘려요.
  • 최대 높이와 너비는 창의 현재 위치에 따라 달라져요. 창의 가장자리는 화면의 가장자리를 넘어갈 수 없어요. 이 범위보다 큰 값은 가질 수 있는 가장 큰 크기로 잘려요. 예를 들어 창의 위치가 x = 400, y = 300이고 height = 400, width = 300으로 크기 조절을 요청하면, 화면이 y 방향으로 요청을 완전히 수용할 만큼 크지 않기 때문에 창은 height = 300, width = 300으로 조절돼요.

5. 창을 이동하는 메서드 추가하기

  • move(to:) : (Position) -> () - resize(to:)와 비슷하지만, 이 메서드는 크기가 아니라 창의 _위치_를 요청된 값으로 조절해요. resize와 마찬가지로 새 위치는 특정 한계를 넘을 수 없어요.
  • 가장 작은 위치는 x와 y 모두 0이에요.
  • 어느 방향이든 최대 위치는 창의 현재 크기에 따라 달라져요. 창의 가장자리는 화면의 가장자리를 넘어갈 수 없어요. 이 범위보다 큰 값은 가질 수 있는 가장 큰 값으로 잘려요. 예를 들어 창의 크기가 x = 250, y = 100이고 x = 600, y = 200으로 이동을 요청하면, 화면이 x 방향으로 요청을 완전히 수용할 만큼 크지 않기 때문에 창은 x = 550, y = 200으로 이동해요.

6. 창 텍스트를 갱신하고 창 정보를 표시하는 메서드 추가하기

  • update(title:) : (String) -> () - 이 메서드는 title 프로퍼티를 전달받은 문자열 값으로 설정해요.
  • update(text:) : (String?) -> () - 이 메서드는 contents 프로퍼티를 전달받은 옵셔널 문자열 값으로 설정해요.
  • display() : () -> String - 이 메서드는 창의 현재 상태를 설명하는 문자열을 반환해요. 예를 들어, 창의 title이 "My First Window"이고 위치가 x = 10, y = 100, 크기가 width = 200, height = 150, contents가 "I 😍 my window"라면 "My First Window\nPosition: (10, 100), Size: (200 x 150)\nI 😍 my window\n" 문자열을 반환해야 해요. contents가 nil이면 마지막 줄은 "[This window intentionally left blank]"여야 해요.

7. Window 클래스의 이니셜라이저 만들기

윈도우 시스템에는 사용자 지정 입력으로 창을 만들 수 있도록 하는 이니셜라이저가 있어야 해요. Window 클래스에 또 다른 이니셜라이저를 만들어요. 이니셜라이저는 title, contents, 그리고 두 개의 옵셔널 매개변수 size와 position을 받아야 해요. size와 position 매개변수는 Size 구조체와 Position 구조체의 기본값으로 기본 설정되어야 해요.

let window = Window(title: "My First Window", contents: "I 😍 my window")
window.display()
// Returns "My First Window\nPosition: (0, 0), Size: (80 x 60)\nI 😍 my window\n"

let window2 = Window(title: "My Second Window", contents: "I 😍 my window", size: Size(width: 200, height: 150), position: Position(x: 10, y: 100))
window2.display()
// Returns "My Second Window\nPosition: (10, 100), Size: (200 x 150)\nI 😍 my window\n"
GitHub에서 편집 링크가 새 창이나 탭에서 열려요
Swift Exercism

창 관리 시스템 문제를 시작해 볼 준비가 됐나요?

Exercism에 가입하고 Swift 트랙을 개념 35개연습 문제 116개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.