Swift 프로토콜(protocol)

꼼꼼한 재은씨의 Swift3 기본편을 보며 Swift 문법 내용을 정리한 내용입니다.

프로토콜 : 객체의 설계도

클래스나 구조체가 어떤 기준을 만족하거나 특수한 목적을 다성하기 위해 구현해야 하는 메소드와 프로퍼티의 목록이다. 자바의 interface와 비슷한 개념이다. (이렇게 이해하니 쉬운 개념으로 와닿았다.)

각 이벤트를 관리하기 위해 대리자(delegate)를 지정하여 이벤트 처리를 위임하고, 실제 이벤트 발생 시, 지정된 대리자가 콜백 메소드를 호출해주는 델리게이트 패턴(Delegate Pattern)을 많이 사용하는데, 이 때 사용되는 것이 바로 프토콜이다.

기능에 대한 명세가 적혀 있어서 프로토콜 코드를 보면 어떤 기능을 어떻게 구현해야 하는지를 알 수 있다.

프토콜은 부모 클래스는 아니다. 프로토콜은 대상 클래스 전체를 책임지지 않으며, 일부 기능의 형식을 담당한다.

프로토콜의 정의

protocol 키워드를 사용한다.

1
2
3
4
5
6
7
8
9
10
11
protocol protocolName {
<구현해야할 프로퍼티 명세 1>
<구현해야할 프로퍼티 명세 2>
<구현해야할 프로퍼티 명세 3>

<구현해야할 메소드 명세 1 >
<구현해야할 메소드 명세 2 >
<구현해야할 메소드 명세 3 >
}

// 구조체, 클래스, 열거형, 익스텐션을 프로토콜로 구현할 수 있다.

프로토콜 프로퍼티

프로토콜에 선언되는 프로퍼티에는 초기값을 할당할 수 없다. 프로퍼티 종류 또한 구분하지 않는다. 프로퍼티의 종류, 이름, 변수/상수 구분, 타입, 읽기/쓰기 가능 여부를 정의한다.

1
2
3
4
5
6
7
8
9
10
11
12
protocol SomePropertyProtocol {
var name : String { get set } // 읽고 쓰기 가능
var descriptioin : String { get } // 읽기 가능
}

// 구현
struct RubyMember : SomePropertyProtocol {
var name = "아모개"
var description : String {
return "Name : \(self.name)"
}
}

프로토콜 메소드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protocol SomeMethodProtocol {
func run(command : String)
func showResult(param : Int) -> String
}

// 구현
struct RubyService : SomeMethodProtocol {
func run(command : String) {
if command == "start" {
print("시작")
}
}

func showResult(param : Int) -> String {
return "Result : \(param)"
}
}

mutating, static 사용

구조체 내 메소드가 프로퍼티를 변경하는 경우, 메소드 앞에 반드시 mutating 키워드를 붙여 이 메소드가 프로퍼티 값을 수정하는 메소드임 을 표시하도록 강제한다. 메소드가 만약 프로토콜에서 선언된 메소드라면 반드시 프로토콜에 mutating 키워드가 추가되어 있어야 한다.

클래스와 같은 참조 타입은 mutating 키워드 없이 자유롭게 메소드 내의 프로퍼티를 수정할 수 있지만, 구조체나 열거형은 프로토콜의 메소드에 mutating 키워드가 추가되어 있지 않을 경우 프로퍼티 값을 변경할 수 없다. 억지로 키워드를 붙인다면 오류가 날 것이다.

보통 mutating 키워드가 붙지 않는 경우는 다음과 같은 경위다.

  • 구조체나 열거형 등 값 타입의 객체에서 내부 프로퍼티의 값을 변경하기 원치 않을 때
  • 주로 클래스를 대상으로 간주하고 작성된 프로토콜일 때

프로토콜의 초기화 메소드

일반적인 메소드와 비슷하게 초기화하는 것이 가능하다. 실행블록 없이 이름과 매개변수 등을 작성한다.

1
2
3
4
protocol initProtocol {
init()
init(cmd : String)
}

초기화 메소드 작성 시 유의사항은 외부매개변수명까지는 완전히 일치해야 한다는 점이다. 임의로 변경 시 프로토콜을 제대로 구현하지 않은 것으로 간주된다. 그리고 클래스에서 초기화 메소드를 구현할 때는 required 키워드를 붙여야 한다.

  • 구현되는 초기화 메소드의 이름과 매개변수명은 프로토콜의 명세에 작성된 것과 완전히 일치해야 한다.
  • 프로토콜 명세에 선언된 초기화 메소드는 그것이 기본 제공되는 초기화 메소드여도 직접 구현해야 한다.
  • 클래스에서 초기화 메소드를 구현할 때는 required 키워드를 붙여야 한다.

또다른 예시를 살펴보겠다.

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
//initProtocol은 위에서 보았던 프로토콜에서 참조한 것이다.

struct SInit : initProtocol {
var cmd : String

init() {
self.cmd = "start"
}

init(cmd : String) {
self.cmd = cmd
}
}

class CInit : initProtocol {
var cmd : String

required init() {
self.cmd = "start"
}

required init(cmd : String) {
self.cmd = cmd
}
}

타입으로서의 프로토콜

때로는 프로토콜이 타입으로서의 역할을 할 때도 있다. 마치 프로토콜을 상위 클래스 타입으로 간주하여 사용하는 것과 유사하다.

  • 상수나 변수, 그리고 프로퍼티의 타입으로 사용할 수 있음
  • 함수, 메소드 또는 초기화 구문에서 매개변수 타입이나 반환 타입으로 프로토콜을 사용할 수 있음
  • 배열이나 사전, 혹은 다른 컨테이너의 타입으로 사용할 수 있음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protocol Wheel {
func spin()
func hold()
}

class Bicycle : Wheel {
var moveState = false

func spin() {
self.pedal()
}

func hold() {
self.pullBreak()
}

func pedal() {
self.moveState = true
}

func pullBreak() {
self.moveState = false
}
}

일반적인 경우 Bicycle 클래스는 다음과 같이 초기화된다.

1
2
3
4
5
let trans = Bicycle()

trans.moveState
trans.spin()
...

let trans : Wheel = Bicycle()

이 경우는 trans가 사용할 수 있는 프로퍼티와 메소드에 제한이 생긴다.

trans.spin()
trans.hold()

기존에 Wheel이라는 프로토콜에 존재하던 메소드만 사용이 가능하게 된다.

Swift 열거형(enum)과 익스텐션(extension)

꼼꼼한 재은씨의 Swift3 기본편을 보며 Swift 문법 내용을 정리한 내용입니다.

열거형과 익스텐션

  • 열거형 : 문자열이나 정수를 직접 입력받아야 할 정보들을 선택할 수 있도록 만들어주어서, 값의 범위를 제한하고 무작위로 값이 입력되는 것을 방지하여 코드의 안정성을 높여주는 역할을 한다. 이를 통해 입력값의 오류를 줄이는 것이 가능하다.
  • 익스텐션 : 기존 객체를 수정없이 기능 추가하는 것을 제공한다. 생산성을 높여주는 강력한 도구이다.

열거형

열거형(Enumeration)에서 데이터는 객체 정의 시점에 함께 정의된다. 삭제나 변경이 불가능하다. 수정 방법은 단 한 가지, 구문의 직접 수정 뿐이다. 열겨형 데이터들은 타입으로 사용할 수 있고, 컴파일러가 미리 인지할 수 있다.

다음과 같은 경우는 열거형을 사용하는 것을 권장한다.

  • 원치 않는 값이 잘못 입력되는 것을 막고 싶을 때
  • 입력받을 값을 미리 특정할 수 있을 때
  • 제한된 값 중에서만 선택하는 것을 유도하고 싶을 때(약간의 강제성)

성별, 국가, 지역, 직급, 색상, 방향 등이 이에 해당한다.

열거형의 정의

열거형 객체 정의는 enum 키워드를 사용한다.

1
2
3
4
5
6
enum 열거형 이름 {
// 열거형 멤버 정의
case 멤버값 1
case 멤버값 2
case ......
}

열거형 이름은 소문자로 작성하지만 첫 글자만 대문자로(카멜 표기법 - CamelCasing), 그리고 단어마다 첫 글자는 대문자로 작성한다.

1
2
3
4
5
6
7
8
enum Gender {
case man
case woman
}

// enum 사용법
let m = Gender.man
let w = Gender.woman

열거형 사용은 위 예시 말고도 여러 가지 방법이 있다.

1
2
3
4
var m1 = Gender.man
var m2 : Gender = Gender.man
var m3 = .man
var m4 : Gender = .man

열거형은 switch 구문에 활용이 가능하다 ?

1
2
3
4
5
6
switch m1 {
case Gender.man :
// something
case Gendar.woman :
// something
}

멤버와 값 분리

http 코드와 같은 값은 코드만 보고 의미를 파악하기 어려운 경우가 있다. 이러한 경우는 코드를 멤버와 값으로 나누는 방법이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
enum HTTPCode : Int {
case OK = 200
case NOT_MODIFY = 304
case INCORRECT_PAGE = 404
case SERVER_ERROR = 500
}

// 사용할 때는 rawValue를 활용한다.
HTTPCode.OK.rawValue // 200
HTTPCode.NOT_MODIFY.rawValue // 304
HTTPCode.INCORRECT_PAGE.rawValue // 404
HTTPCode.SERVER_ERROR.rawValue // 500

열거형 객체의 멤버와 값은 선언할 때 정의되지만, 사용 시에 멤버에 보조 값을 설정할 수 있다. 이를 Associated Values(연관된 값)이라고 한다.

1
2
3
4
5
6
7
8
enum ImageFormat {
case JPEG
case PNG(Bool)
case GIF(Int, Bool)
}

var image = ImageFormat.PNG(true)
imgae = .GIF(256, false)

열거형은 클래스나 구조체처럼 내부에 연산 프로퍼티와 메소드 또한 정의가 가능하다. 인스턴스를 만들 수는 없지만, 인스턴스처럼 사용할 수 있어서 인스턴스 프로퍼티, 타입 프로퍼티, 메소드를 모두 정의할 수 있다.

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
32
33
34
enum httpCode : Int {
case OK = 200
case NOT_MODIFY = 304
case INCORRECT_PAGE = 404
case SERVER_ERROR = 500

var value : String {
return "httpCode is \(self.rawValue)"
}

func getDescription() -> String {
switch self {
case .OK :
return "\(self.rawValue)"
case .NOT_MODIFY :
return "\(self.rawValue)"
case .INCORRECT_PAGE :
return "\(self.rawValue)"
case .SERVER_ERROR :
return "\(self.rawValue)"
}
}

static func getName() -> String {
return "httpCode"
}
}

var res = httpCode.OK
res = .NOT_MODIFY
res.value // 304
res.getDescription()

httpCode.getName()

열거형의 활용

열거형은 추후 나오는 코코아터치 프레임워크에서 자주 쓰이는 객체이다. 메소드 호출 옵션이나 스타일을 설정할 때 열거형으로 미리 정의해 둔 멤버들을 속성값으로 사용하는 경우가 많다.

1
2
3
4
5
enum UIImagePickerControllerSourceType : Int {
case photoLibrary
case camera
case savedPhotosAlbum
}

위 소스는 이미지 피커 컨트롤러에서 이미지를 어디서 가져올 것인가를 결정하는 열거형 객체이다. 만약에 이것을 열거형이 아닌 다른 것으로 정의한다면 외부에서 수정이 가능할 것이다. 가변적인 성격을 띄는 것이 아니므로 열거형으로 해야 옳다.

이처럼 지정된 값만 허용되게 할 때는 열거형 객체를 이용하는 것이다. 적절한 경우에 열거형을 적극 활용하도록 하자.

익스텐션

익스텐션은 확장 구문이라고 번역된다. 클래스나 구조체, 열거형 등의 객체에 새로운 기능을 추가하여 확장하는 구문이다. 이미 존재하는 객체에 추가적인 요소를 가미하는 것이 특징이고, 거의 제약 없이 가능하여 꽤 강력한 것이라 볼 수 있다. (오브젝티브-C의 ‘카테고리’와 비슷하다고 한다.)

익스텐션은 다음과 같은 것을 구현할 수 있다.

  1. 연산 프로퍼티를 추가할 수 있다.
  2. 새 메소드 정의가 가능하다.
  3. 새로운 초기화 구문을 추가할 수 있다.
  4. 기존 객체를 수정하지 않고 프로토콜을 구현할 수 있다.

익스텐션 extension 키워드를 사용한다.

1
2
3
extension <확장할 기존 객체명> {
// 추가 기능에 대한 구현 코드 작성
}

익스텐션과 연산 프로퍼티

프로퍼티를 기존 객체에 추가 가능하지만, 연산 프로퍼티로 제한이 된다. 저장 프로퍼티는 불가능하다. 대신 인스턴스 프로퍼티든 타입 프로퍼티든 연산 프로퍼티라면 모두 추가 가능하다.

1
2
3
4
5
6
7
8
9
extension Double {
var km : Double { return self * 1_000.0 }
var m : Double { return self }
var cm : Double { return self / 100.0 }
var mm : Double { return self / 1_000.0 }
var description : String {
return "\(self)km는 \(self.km)m, \(self)cm는 \(self.cm)m, \(self)mm는 \self.mm)mm입니다."
}
}

위 예시는 Double 구조체 확장 구문이다. 숫자의 1_000.0의 언더바는 자릿수를 구분해주는 구분자이다. 숫자 값에는 영향이 없다.

익스텐션과 메소드

기존 객체에 새로운 인스턴스 메소드나 타입 메소드를 정의할 수 있다. 오버로딩 특성을 이용해서 새로운 메소드를 저으이할 수 있고, 매개변수명을 바꿔서 새로운 메소드를 작성할 수 있다. 그러나 기존 객체에서 사용된 같은 메소드를 익스텐션에서 재정의 하는 것은 허용되지 않는다.

1
2
3
4
5
6
7
extension Int {
func repeatRun(task : () -> Void) {
for _ in 0 ..< self {
task()
}
}
}

인스턴스 메소드는 익스텐션에서도 mutating 키워드를 사용하여 인스턴스 자신을 수정하도록 허용할 수 있다. 구조체나 열거형에서 정의된 메소드가 자신의 인스턴스를 수정하거나 프로퍼티를 변경해야할 때 mutating 키워드를 사용하는데, 익스텐션이 구조체, 열거형을 확장의 대상으로 삼았을 때 사용한다.

1
2
3
4
5
6
7
8
extension Innt {
mutating func circle() {
self = self * self * 3.14
}
}

var radius = 1
value.circle() // 3.14

익스텐션은 특히 직접 수정할 수없는 객체나 라이브러리 등을 수정할 자주 사용된다. 익스텐션을 적지적소에 사용하는 것은 권장되지만, 남용은 금물이다.

Swift 구조체와 클래스

꼼꼼한 재은씨의 Swift3 기본편을 보며 Swift 문법 내용을 정리한 내용입니다.

클래스와 구조체

구조체와 클래스는 하나의 큰 코드 블록이다. 안에 변수 또는 상수를 넣어 값을 정의하거나, 함수를 넣어서 기능을 정의할 수도 있다. 스위프트는 객체지향 언어이기 때문에 구조체와 클래스는 문법 상 중요한 요소 중 하나이다.

  • 프로퍼티(Properties) : 구조체와 클래스 내부의 변수와 상수
  • 메서드(Method) : 구조체와 클래스 내부에서 정의된 함수
  • 멤버(Member) : 프로퍼티와 메소드를 합하여 구조체나 클래스의 멤버라고 한다.

구조체 vs 클래스

  • 공통점
    • 프로퍼티
    • 메소드
    • 서브스크립트 : 속성값에 접근할 수 있는 방법을 제공
    • 초기화 블록 : 객체를 원하는 초기 상태로 설정
    • 확장 : 객체에 함수적 기능을 추가하는 확장(extends) 구문 사용
    • 프로토콜 : 특정 형식의 함수적 표준을 제공
  • 클래스만 할 수 있는 기능
    • 상속 : 클래스의 특성을 다른 클래스에게 물려줄 수 있다.
    • 타입 캐스팅 : 실행 시 컴파일러가 클래스 인스턴스의 타입을 미리 파악하고 검사할 수 있다.
    • 소멸화 구문 : 인스턴스가 소멸되기 직전에 처리해야 할 구문을 미리 등록할 수 있다.
    • 참조에 의한 전달 : 클래스 인스턴스가 전달될 때에는 참조 형식으로 제공되며, 이 때 참조가 가능한 개수는 제약이 없다.

기본 개념

정의

구조체와 클래스의 이름은 대문자로 시작한다. 명명 규칙에 따르도록 하자.

1
2
3
4
5
6
7
8
9
// 구조체
struct 구조체 이름 {
// 정의 내용
}

// 클래스
class 클래스 이름 {
// 정의 내용
}

메소드와 프로퍼티

선언된 값에 따라 타입 선언이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Resolution {
var width = 0
var height = 0

func desc() -> String {
return "Resolution 구조체"
}
}

class VideoMode {
var interlaced = false
var frameRate = 0.0
var name : String?
}

인스턴스

클래스와 구조체를 생성한 후 여러 개를 찍어내듯이 만들고 할당할 수 있다. 이 찍어낸 복사본과 같은 개념을 인스턴스라고 한다. 같은 건물의 설계도를 가지고 여러 개의 건물을 세우는 것과 유사한 방식이라고 보면 되겠다.

1
2
3
let g = guJoChae()

let c = makeClass()

. (dot)을 이용하여클래스와 구조체 내의 프로퍼티에 접근할 수 있다. 예를 들어 g.name, c.width와 같이 말이다.

초기화

구조체나 클래스 뒤에 빈괄호를 붙이면 기본적인 인스턴스가 만들어진다. 때로는 파라미터를 괄호 속에 넣어주기도 하는데, 이를 초기화(Initialize)하기 위해 반드시 필요한 값들입니다.

구조체나 클래스를 초기화할 때는 매개변수의 값 역시 초기화가 필요하다.

1
let initTest = testMethod(a: 1, b: 2)

구조체의 값 전달 방식: 복사에 의한 전달

구조체와 클래스의 차이는 값 전달 방식이다. 구조체는 인스턴스를 생성한 후 이를 변수나 상수에 할당하거나 함수의 인자값으로 전달할 때 값을 복사하여 전달하는 방식이다. 이를 값 타입(Value Type) , 또는 복사에 의한 전달 이라고 한다. 앞서 자료형은 모두 복사를 통해 값이 전달된다고 언급한 적이 있는데, 이는 자료형 자체가 실제로는 구조체로 구현되었기 때문이다.

구조체 인스턴스는 변수에 대입 시, 복사한 값으로 대입되는 것이다. 그러므로 기존 인스턴스와 대입된 인스턴스는 독립적이다. 한 쪽에서 변경이 되어도 다른 쪽에 영향을 주지 않는다.

클래스의 값 전달 방식 : 참조에 의한 전달

메모리 주소를 사용하여 객체 자체를 전달한다. 따로 주소값을 전달할 필요 없이, 객체 주소값만 넘긴다면, 자유롭게 사용이 가능하다. 클래스 초기화와 동시에 프로퍼티를 사용하는 것이 가능하다.

인스턴스 비교 연산자

1
2
// === : 동일 인스턴스인지 비교
// !== : 다른 인스턴스인지 비교

일반적으로 다음 조건에 하나 이상 해당한다면 구조체 사용을 권장한다.

  1. 서로 연관된 몇 개의 기본 데이터 타입들을 캡슐화하여 묶는 것이 목적일 때
  2. 캡슐화된 데이터에 상속이 필요하지 않을 때
  3. 캡슐화된 데이터를 전달하거나 할당하는 과정에서 참조 방식보다는 값이 복사되는 것이 합리적일 때
  4. 캡슐화된 원본 데이터를 보존해야 할 때

프로퍼티(Property)

클래스와 구조체를 구성하는 중요 요소이다. 프로퍼티는 값을 저장하기 위한 목적으로 클래스와 구조체 내에서 정의된 변수나 상수를 의미한다. 정확히는 값을 제공 한다.

프로퍼티에는 저장 프로퍼티와 연산 프로퍼티가 있다.

  • 저장 프로퍼티

    • 입력된 값을 저장, 저장된 값을 제공함
    • 상수 및 변수를 사용하여 정의가 가능함
    • 클래스와 구조체에서 사용이 가능하지만, 열거형에서는 사용할 수 없음
  • 연산 프로퍼티

    • 특정 연산을 통해 값을 만들어 제공하는 역할
    • 변수만 사용해서 정의 가능함
    • 클래스, 구조체, 열거형 모두에서 사용 가능함

저장 프로퍼티

클래스 내에서 선언된 변수나 상수를 부르는 이름이다. 변수는 속성 변수이고, 상수는 속성 상수라고 한다. 저장 프로퍼티는 일반적인 변수, 상수를 초기화하듯이 초기화 값을 지정할 수 있다. 단, 필수는 아니다.

초기값을 할당하지 않으면 유의해야 할 점들이 있다. 초기값이 할당되지 않은 저장 프로퍼티는 반드시 옵셔널 타입으로 선언해야 한다. 클래스의 프로퍼티에 값이 비어있다면 인스턴스를 생성할 때 무조건 nil로 초기화한다. 물론 처음부터 값 할당을 하면 옵셔널로 할 필요가 없다.

초기값을 주지 않으면서 옵셔널 타입으로 선언하지 않을 수 있는 방법도 존재한다. 초기화구문에서 프로퍼티의 값을 초기화하는 것이다. 클래스 프로퍼티는 인스턴스를 생성할 때 초기화하기 때문에 프로퍼티 초기값은 인스턴스를 생성하기 전까지만 할당하면 된다. 초기화 구문에서 프로퍼티 값 할당이 가능하다면 옵셔널로 선언하지 않아도 된다.

1
2
3
4
class User {
var name : String
// 클래스 프로퍼티의 초기값 설정이 안되어서 Error
}
1
2
3
4
5
6
7
8
/* 올바른 예시 */
class User {
var name : String

init() {
self.name = ""
}
}
1
2
3
4
5
class User {
// 옵셔널의 예시
var name : String?
// var name : String! 도 가능.
}
1
2
3
4
class User {
// 아예 초기화하는 방법
var name : String = ""
}

저장 프로퍼티의 분류

  • var 키워드로 정의되느 변수형 저장 프로퍼티(멤버 변수)
  • let 키워드로 정의되는 상수형 저장 프로퍼티(멤버 상수)
1
2
3
4
5
6
7
8
9
10
11
// 구조체의 저장 프로퍼티

struct guJoChae1 {
var intValue1 : Int
let intValue2 : Int
}

struct guJoChae2 {
let intValue3 : Int
var intValue4 : Int
}

지연 저장 프로퍼티

저장 프로퍼티는 클래스 인스턴스가 생성될 때 함께 초기화되지만, lazy키워드가 있다면 예외가 된다. 키워드 이름처럼 초기화를 지연시키는 역할을 한다. 프로퍼티가 호출되는 순간에 초기화된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class OnCreate {
init() {
print("OnCreate")
}
}

class LazyTest {
var base = 0
lazy var late = OnCreate()

init() {
print("Lazy Last")
}
}

클로저를 이용한 저장 프로퍼티 초기화

1
2
3
4
5
// 프로퍼티 참조 시에 최초 한 번만 실행한다.
let/var 프로퍼티명: 타입 = {
정의 내용
return 리턴값
}()

연산 프로퍼티

저장 프로퍼티와는 다르게 다른 프로퍼티의 값을 연산 처리하여 간접적으로 값을 제공한다. 프로퍼티의 값을 참조하기 위해 구문이 get구문이다.

set 구문을 추가할 수 있다. 값을 할당하거나 변경할 때 실행된다. set 구문은 필요에 따라 생략이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
class/struct/enum 객체명 {
var 프로퍼티명 : 타입 {
get {
//......
return 리턴값
}

set ( 매개변수 ) {
//......
}
}
}

다음은 진짜 예시이다.

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
import Foundation

// 나이 계산
struct UserInfo {
var birth : Int!

var thisYear : Int {
get {
let df = DateFormatter()
df.dateFormat = "yyyy"
return Int(df.string(from: Date()))!
}
}

var age : Int {
get {
return (self.thisYear - self.birth) + 1
}
}
}

let info = UserInfo(birth: 1990)
print(info.age)

// 28

프로퍼티 옵저버

특정 프로퍼티를 관찰하다가 값 등이 변경되면 이에 반응한다. 직접 변경, 자동 변경 모두 반응하여 호출된다. 동일한 값이 재할당되어도 마찬가지이다.

  • willSet : 프로퍼티의 값이 변경되기 직전 호출되는 옵저버
  • didSet : 프퍼티의 값이 변경된 직후 호출되는 옵저버
1
2
3
4
5
6
7
// willSet 옵저버

var <프로퍼티명> : <타입> [ = <초기값> ] {
willSet [ (<인자명>) ] {
<프로퍼티 값이 변경되기 전에 실행할 내용>
}
}
1
2
3
4
5
6
7
// didSet

var <프로퍼티명> : <타입> [ = <초기값> ] {
didSet [ (<인자명>) ] {
<프로퍼티 값이 변경된 후에 실행할 내용>
}
}

타입 프로퍼티

인스턴스에 관련된 값이 아니라 클래스나 구조체, 또는 열거형과 같은 객체 자체에 관련된 값을 다루는 때도 있다. 이 때는 인스턴스를 생성하지 않고 클래스나 구조체 자체에 값을 저장하게 되며 이를 타입 프로퍼티라고 한다.

저장된 값은 모든 인스턴스가 공통적으로 사용할 수 있다. 타입 프로퍼티는 인스턴스가 아무리 많아도 모든 인스턴스가 하나의 값을 공용으로 사용한다.

static키워드가 핵심이다. 다른 언어에서와 같이 어느 곳에서나 접근이 가능하다.

1
static let/var 프로퍼티명 = 초기값
1
2
3
4
5
6
7
8
class let/var 프로퍼티명 : 타입 {
get {
return 반환값
}
set {
// set 내용
}
}

메소드

함수의 일종이다. 구조체, 클래스 등에서 함수가 선언되면 메소드라고 지칭한다. 허나 함수와는 차이가 있다. 함수는 독립적으로 기능을 구현하지만, 메소드는 객체에서 정의된 다른 메소드와 함께 함수적인 기능을 수행한다.

메소드는 인스턴스 메소드타입 메소드 로 구분된다.

  • 인스턴스 메소드 : 객체의 인스턴스를 생성해야 사용할 수 있는 메소드.
  • 타입 메소드 : 객체의 인스턴스를 생성하지 않아도 사용 가능한 메소드.

인자값이 있는 메소드를 호출할 때에는 함수와 동일하다. 호출 시 인자값 앞에 인자 레이블을 붙여준다.

1
2
let temp = tempMethod()
temp.isParameter(a : 5)

구조체나 열거형의 인스턴스 메소드 내부에서 프로퍼티의 값을 수정할 때는 반드시 앞에 mutating 키워드를 추가해야 한다. 이 키워드를 통해 내부 프로퍼티 값을 수정하는 메소드라는 것을 컴파일러에게 지시한다. 당연한 말이지만, 인스턴스가 상수라면 mutating 키워드는 필요가 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Point {
var x = 0.0, y = 0.0
mutating func moveByX(x deltaX: Double, y deltaY: Double) {
self.x += deltaX
self.y += deltaY
}
}

var point = Point(x: 10.5, y: 12.0)
point.moveByX(x: 3.0, y: 4.5)

print("New Point (\(point.x), \(point.y)))

// 클래스 인스턴스

상속 (Inheritance)

클래스와 구조체가 구분되는 특성이다. 하나의 클래스가 다른 클래스에게 특성을 물려줄 수가 있다. 자식 클래스가 부모 클래스의 속성과 메소드를 물려받게 되고, 사용할 수 있다.

  • 프로퍼티와 메소드를 물려준 클래스는 부모 클래스, 상위 클래스, 슈퍼 클래스, 기본 클래스
  • 프로퍼티와 메소드를 물려준 클래스는 자식 클래스, 하위 클래스, 슈퍼 클래스, 파생 클래스

서브클래싱

기존 클래스를 기반으로 새로운 클래스를 작성하는 것을 의미한다.

1
2
3
class 클래스명 : 부모클래스 {
// 추가 구현 내용
}

뒤에 나오는 코코아 터치 프레임워크의 클래스 정의 구문에서 콜론 : 다음에 여러 개의 클래스가 나열되는데, 가장 첫 번째는 상속, 나머지는 자바의 implements 키워드를 의미한다.

오버라이딩

재정의 라는 의미로, 부모 클래스에서 상속받은 내용을 덮어쓴다. 재정의한 내용은 서브클래싱한 하위 클래스에만 적용된다. 부모 클래스까지의 영향이 없다는 의미이다. 스위프트에서는 오버라이딩하는 메소드나 프로퍼티 앞에 override 키워드를 붙인다. 없다면 오류로 간주한다.

만약 오버라이딩 할 수 있는 메소드나 프로퍼티가 아닌 경우는 컴파일러가 override 키워드를 오류로 간주한다. 상위 클래스에서 선언된 것만 오버라이딩이 가능하다.

프로퍼티를 오버라이딩 할 때 허용되는 것과 허용되지 않는 사항이 존재한다.

  • 허용되는 것

    1. 저장 프로퍼티를 get, set 구문이 모두 있는 연산 프로퍼티로 오버라이딩 하는 것
    2. get, set 구문이 모두 제공되는 연산 프로퍼티를 get, set 구문이 모두 제공되는 연산 프로퍼티로 오버라이딩 하는 것.
    3. get 구문만 제공되는 연산 프로퍼티를 get, set 구문이 모두 제공되는 연산 프로퍼티로 오버라이딩하는 것
    4. get 구문만 제공되는 연산 프로퍼티를 get 구문만 제공되는 연산 프로퍼티로 오버라이딩 하는 것
  • 허용되지 않는 것

    1. 저장 프로퍼티를 저장 프로퍼티로 오버라이딩하는 것
    2. get, set 구문과 관계없이 연산 프로퍼티를 저장 프로퍼티로 오버라이딩하는 것
    3. 저장 프로퍼티를 get 구문만 제공되는 연산 프로퍼티(=readonly)로 오버라이딩하는 것
    4. get, set 구문을 모두 제공하는 연산 프로퍼티를 get 구문만 제공되는 연산 프로퍼티로 오버라이딩하는 것

복잡하지만 단순하다. 상위 클래스의 기능을 하위 클래스가 확장하거나 변경은 할 수 있지만 제한하는 방식이 되어서는 안된다는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class LongBoard : SkateBoard {
var deck = 0
var wheel = 0

override var currentSpeed : Double {
get {
return Double(self.wheel * 50)
}
set {
// nothing
}
}

override var description : String {
get {
return "LongBoard : wheel\(self.wheel), so currentSpeed=\(self.currentSpeed)"
}
set {
print("New Value is \(newValue)")
}
}
}

오버로딩 : 같은 메소드이름이지만 매개변수 변화로 새로운 메소드를 만들어 적재할 수 있도록 한다.

오버로딩은 다음과 같다.

1
2
3
4
5
6
func currentSpeed()
func currentSpeed(param : Int)
func currentSpeed(param : String)
func currentSpeed(param : Double) -> String
func currentSpeed(param : Double, second : String)
func currentSpeed(param : Double, secondParam : String)

하위 클래스에서 오버라이딩을 차단하기 위해서는 final 키워드를 사용한다.

초기화 구문

구조체나 클래스는 항상 인스턴스를 생성해서 할당 후 사용해야 하는 데, 이를 초기화라 한다.

이 과정에서 저장 프로퍼티는 생성 과정에서 초기화되어야 한다. 그 후에 구조체나 클래스의 초기화가 가능하다.

Resolution(), Video(), Car() 등은 초기화 구문이다.

저장 프로퍼티를 초기화하는 구조체/클래스의 초기화다.

Board(id: 1, category: 3)

초기화를 할 때 주의할 점은 미리 정의된 형태로 해야 한다는 것이다.

init 메소드 (초기화)

1
2
3
4
init(매개변수 : 타입 ... ) {
// 매개변수 초기화
// 인스턴스 생성 후 처리할 로직
}

일반적인 메소드와 비슷한 모양이지만 init 메소드만의 특징이 있다.

  1. 초기화 메소드의 이름은 init으로 통일한다.
  2. 매개변수의 개수, 이름, 타입은 임의로 정의할 수 있다.
  3. 매개변수의 이름과 개수, 타입이 서로 다른 여러 개의 초기화 메소드를 정의할 수 있다.
  4. 정의된 초기화 메소드는 직접 호출되지만, 대부분 인스턴스 생성 시 간접 호출된다.
  • 여러 모양의 초기화 모양을 두고, 가까운 모양으로 호출된다.

초기화 구문의 오버라이딩

초기화 구문 역시 자식 클래스에서 오버라이딩이 가능하다. 동일하게 override 키워드를 붙여야 한다. 상위 클래스에서 선언된 적이 없는 초기화 형식이라면 붙이면 안 된다.

1
2
3
4
5
6
7
8
9
class Base {

}

class ExBase : Base {
override init() {
// some codes
}
}

메소드와는 달리 초기화 구문에서 오버라이딩은 더 이상 부모 클래스에서 정의한 초기화 구문이 실행되지 않는 오류를 일으킬 수 있다. 부모 클래스의 기본 초기화 구문에서 프로퍼티를 초기화했다면, 자식 클래스에서 기본 초기화 구문을 오버라이딩함으로써 부모클래스 프로퍼티의 초기화가 누락된다. 이는 곧 오류를 발생시키므로, 초기화 구문을 오버라이딩할 경우 부모 클래스에서 정의된 초기화 구문을 내부적으로 호출해야 하는데, 오버라이딩된 초기화 구문 내부에 super.init 구문을 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
class Base {
var baseValue : Double
init(inputValue : Double) {
self.baseValue = inputValue
}
}

class ExBase : Base {
override init(inputValue: Double) {
super.init(inputValue: 3.14)
}
}

Swift 함수

꼼꼼한 재은씨의 Swift3 기본편을 보며 Swift 문법 내용을 정리한 내용입니다.

함수와 클로저 (Closure)

독립적으로 처리될 수 있는 것을 분리하여 구조화한 객체.

함수의 이점

  • 동일 코드가 여러 번 실행될 때, 재작성이 필요가 없다.
  • 기능 단위로 함수화하게 되면, 가독성이 좋아진다.
  • 유지 보수 차원에서 이점이 있다.

함수의 정의

func 키워드를 사용하여 함수를 정의한다.

1
2
3
4
5
6
7
8
9
10
11
12
/*
func 함수명(매개변수) -> 반환타입 {
// 함수 내용
}
*/

func funcExample(name: String, age: Int) -> String {
var string = ""
string += "Hello \(name) !"

return string
}

함수의 호출

일반적으로 함수를 호출하듯이 정의된 함수를 호출하여 실행한다.

1
funcExample(name: "hwiVeloper", age: 28)

함수 호출 시 사용하는 파라미터 이름과 함수 내부에서 사용하는 파라미터 이름을 다르게 하고 싶다면 다음과 같은 방법을 사용한다.

1
2
3
4
5
func funcExample(userName name: String, userAge age: Int) {
print(name)
}

funcExample(userName: "hwiVeloper", userAge: 28)

파라미터 이름을 _ (언더바)로 지정하면 함수 호출 시 이름을 생략할 수 있다.

1
2
3
4
5
6
7
func funcExample(_ name: String) {
print(name)
}

funcExample("devhwi")

// 다른 언어에서 호출하는 방식과 비슷하다.

파라미터 기본값을 지정하면 호출 시 생략이 가능하다.

1
2
3
4
5
func funcExample(_ name: String, age: Int = 28) {
print(name)
}

funcExample("devhwi")

...를 사용하면 개수가 정해지지 않은 파라미터를 받을 수 있다.

1
2
3
4
5
6
7
8
func funcExample(_ numbers: Int...) -> Int {
var sum = 0
for number in numbers {
sum += number
}

return sum
}

함수 중첩이 가능하다.

1
2
3
4
5
6
7
8
9
func funcOuter(message: String) -> (String) -> String {
func funcInner(name: String) -> String {
return name + message
}
return hello
}

let hello = helloGenerator(message: "hello!")
hello("devhwi")

눈여겨 볼 점은 funcOuter 의 return type이 (String) -> String이라는 점이다. funcOuter는 funcInner의 String 을 받아서 String으로 return 하는 함수라는 것이다.

같은 예시지만 중첩 내부에서 다수의 파라미터를 받아서 처리하는 경우이다.

1
2
3
4
5
6
7
func funcOuter(message: String) -> (String, String) -> String {
func funcInner(firstName: String, lastName: String) -> String {
return lastName + firstName + message
}

return funcInner
}

기존의 (String) -> String에서 (String, String) -> String으로 변화했다. 두 파라미터를 받아 최종적으로 하나로 처리한다는 뜻이다.

함수의 튜플 반환

함수는 여러 개의 값을 반환할 수 있다.

1
2
3
4
5
6
7
8
9
10
func getIndvInfo() -> (Int, String) {
let height = 177
let name = "hwiVeloper"

return (height, name)
}

var info = getIndvInfo()
info.0 // 177
info.1 // "hwiVeloper"

클로저 (Closure)

함수를 좀 더 간결하게 짤 수 있게끔 도와주는 개념이다. 클로저는 { }로 감싸여진 실행 가능한 코드블럭 이다.

위에서 살펴보았던 함수 중첩을 클로저를 사용하여 재구성한다면,

1
2
3
4
5
func funcOuter(message: String) -> (String, String) -> String {
return { (firstName: String, lastName: String) -> String in
return lastName + firstName + message
}
}

일반적인 함수와는 다르게 함수명이 존재하지 않는다. 허나, 함수와 기능은 동일하게 수행이 가능하다. 파라미터를 받아 return 값을 뱉어낼 수 있다는 점에서 크게 다를 건 없지만, 모양이 다르다. 결국 클로저는 이름 없는 함수 라고 볼 수 있고, 함수는 이름 없는 클로저 라고 볼 수 있다.

클로저는 중괄호( { } )로 감싸져 있고, 파라미터를 괄호로 감싸서 정의한다. 함수와 동일하게 ->로 return type을 정의할 수 있다. in 키워드는 파라미터, return type과 실제 클로저 코드를 분리하고 있다.

단순히 비주얼적으로 본다면 이걸 왜 쓰나 하는 생각이 들 수 있다.

사실 클로저의 장점은 간결함유연함 이다. 이제, 생략 가능한 요소들을 제거해보자.

1
2
3
4
5
func funcOuter(message: String) -> (String, String) -> String {
return { firstName, lastName in
return lastName + firstName + message
}
}

매우 간결해졌다. 이미 funcOuter에서 정의한 중첩된 내부 함수의 타입이 선언이 되었으므로, 내부에서는 다시 한 번 타입을 정의해 줄 필요가 없는 것이다. return type 역시 마찬가지로 funcOuter에서 정의해주었기 때문에 생략된 것이다.

swift 클로저를 알게 되면서 더욱 놀라웠던 점은 여기서 더 생략이 가능하다는 것이다. 위 코드에서 더 생략한 결과이다.

1
2
3
4
5
func funcOuter(message: String) -> (String, String) -> String {
return {
return $1 + $0 + message
}
}

이제서야 클로저를 쓰는 의미가 보인다. 타이핑해야 할 코드의 양이 줄어들었다.

클로저는 또한 변수처럼 정의가 가능하다.

1
2
3
let funcOuter: (String, String) -> String = { $1 + $0 + ", hello!" }

funcOuter("hwi", "developer")

옵셔널로도 정의가 가능하다.

1
2
3
let funcOuter: ((String, String) -> String)?

funcOuter?("hwi", "dev")

클로저를 변수로 정의하고 함수에서 return이 가능하듯이, 파라미터로 역시 받을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func closureExample(number: Int, using block: Int -> Int) -> Int {
return block(number)
}

closureExample(number: 10, using: { (number: Int) -> Int in
return number * 2
})

// 생략을 해보자.
closureExample(number: 10, using: {
$0 * 2
})

// 함수의 마지막 파라미터가 클로저라면 괄호와 파라미터 이름까지 생략 가능하다.

closureExample(number: 10) {
$0 * 2
}

// 이게 근데 유지보수 측면에서 다소 불편할 가능성이 있다고 한다.

클로저 활용하기

  • sort()
  • filter()
  • map()
  • reduce()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let numbers = [1, 3, 2, 6, 7, 5, 8, 4]

let sortedNumbers = numbers.sort { $0 < $1 }
print(sortedNumbers) // [1, 2, 3, 4, 5, 6, 7, 8]

let evens = numbers.filter { $0 % 2 == 0 }
print(evens) // [2, 6, 8, 4]

let arr1 = [1, 3, 6, 2, 7, 9]
let arr2 = arr1.map { $0 * 2 } // [2, 6, 12, 4, 14, 18]

arr1.reduce(0) { $0 + $1 } // 28

arr1.reduce(0, +) // 28

Swift 옵셔널(optional)

꼼꼼한 재은씨의 Swift3 기본편을 보며 Swift 문법 내용을 정리한 내용입니다.

옵셔널 (잠재적 오류를 다루다)

옵셔널(Optional)은 스위프트에 도입된 새로운 개념이다. 프로그램의 안정성을 높이기 위해 사용되는 것이다.
성공적인 결과를 보장하지 못할 경우 에 옵셔널 타입이라는 객체를 감싼 후 반환하는 개념이다. (Optional Wrapping)

1
2
3
4
Int(바꿀 문자열)

let num = Int("123")
// 이 경우는 변환이 가능하다.

그러나 숫자로 바꿀 수 없는 경우는 난감하다.

1
let num = Int("Swift")

다른 언어와 달리 스위프트는 앱이 중간에 꺼져버리거나 다른 예기치 못한 오류를 범할 수 있기 때문에 오류를 지양하고 어떠한 값이든 return하려고 노력한다.

그러나 0도, 다른 어떤 값도 return하기는 다소 부담이 있다. 그렇기 때문에 Null, 스위프트에서는 nil을 반환한다. 값이 없음을 의미한다.

1
let num = Int("Swift") // nil을 반환한다.

단, 안정성 보장을 위해 nil의 사용에 제약을 걸어두었다. nil은 자료형을 가질 수 없기 때문에 문자열이나 숫자에 억지로 대입할 순 없다.

결국 옵셔널이 가질 수 있는 값은 두 가지이다.

  • nil이 아닌 정상적인 값
  • nil

알아서 해주니까 편하다고 모든 값을 옵셔널 처리하면 또 다른 문제가 생기기 때문에 필요한 곳에만 사용한다. nil에 대한 처리를 일일이 해줘야 하는 복잡함이 생긴다.

옵셔널 타입의 선언과 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 옵셔널 Int 타입
var optInt : Int?

// 옵셔널 String 타입
var optStr : String?

// 옵셔널 Double 타입
var optDouble : Double?

// 옵셔널 Array 타입
var optArr : [String]?

// 옵셔널 Dictionary 타입
var optDic1 : Dictionary<String, String>?
var optDic2 : [String : String]?

// 옵셔널 Class 타입
var optClass : AnyObject?

/**
* 선언한 후에는 자유롭게 값을 대입할 수 있다.
*/

주의할 점

  • 옵셔널 타입은 결합 연산 또는 더하기 연산이 가능한 데이터 타입이 아니다.
  • Int?와 Int는 서로 다른 타입이므로 연산이 불가능하다.

옵셔널 사용하기

옵셔널 값을 사용하기 위해서는 옵셔널을 해제하는 것이 우선이다. 해제 방식은 명시적 해제묵시적 해제로 나뉜다.

  • 명시적 해제
    • 강제적 해제
    • 비강제적 해제
  • 묵시적 해제
    • 컴파일러에 의한 자동 해제
    • ! 연산자를 이용한 자동 해제

옵셔널 강제 해제

옵셔널 타입의 변수나 상수 뒤에 !만 붙이면 된다.

1
2
Int("123")! + Int("123")! // 246
Int("123")! + 30 // 153

그렇다면 옵셔널 결과가 nil인데도 불구하고 ! 연산자를 붙인다면? 이건 좀 말이 안된다. 그 경우를 대비하여 값이 nil인지 체크 후에 다음 로직을 실행해야 한다.

다소 재밌는 부분은, 옵셔널 뒤에 비교 연산자 != 를 사용할 시에, 의무적으로 공백 한 칸이 있어야 오류가 나지 않는다.

1
2
intFromstr != nil // Good
intFromstr!=nil // Bad

딕셔너리의 옵셔널 역시 ! 연산자를 이용하여 해제한다.

옵셔널 바인딩

비강제 해제 구문으로 바꾸어 작성 역시 가능하다. 비강제 해제 구문은 if 구문 내에서 조건식 대신 옵셔널 타입의 값을 일반 타입의 변수나 상수에 할당하는 구문을 사용하는 방식으로, 옵셔널 바인딩(Optional Binding)이라고 한다.

옵셔널 바인딩은 일반 상수에 옵셔널 값을 대입하여 결과가 true/false를 반환하도록 만드는데, 이를 체크하는 방식이다.

1
2
3
4
5
6
var str = "Swift"
if let intFromStr = Int(str) { // 이 부분이 중요하다.
print("값이 변환되었다. 변환된 값은 \(intFromStr)")
} else {
print("변환실패")
}

guard 구문을 이용하여 옵셔널 바인딩을 구현할 수 있다. guard 구문은 메서드에서 사용 가능하여 메서드를 정의 후 구문을 작성한다. 앞으로 앱의 대부분이 메서드로 이루어지기 때문에 이는 눈여겨 볼 필요가 있다.

1
2
3
4
5
6
7
8
func intStr(str:String) {
guard let intFromStr = Int(str) else {
print("값 변환 실패!")
return
}

print("값이 변환되었다. 변환된 값은 \(intFromStr)")
}

컴파일러에 의한 옵셔널 자동 해제

컴파일러에서 자동으로 옵셔널을 해제하는 경우도 존재한다.

1
2
3
4
5
6
7
let optInt = Int("123")

if(optInt == 123) {
print("optInt == 123")
} else {
print("optInt != 123")
}

else 영역이 실행될 것으로 예상되지만, 실제로 optInt == 123 이 print된다. 앞서 살펴본 것과 딴 얘기다.

굳이 강제로 해제하지 않아도 무방할 경우가 존재한다. 비교 연산자를 사용할 때가 그 경우이다.

1
2
3
4
5
6
let tempInt = Int("123")

tempInt == 123 //true
tempInt == Optional(123) //true
tempInt! == 123 //true
tempInt! == Optional(123) //true

옵셔널의 묵시적 해제

앞서 굳이 ! 연산자를 사용하지 않고 해제하는 경우를 보았다.(컴파일러가 해결해 주는 경우였다.) 이와 비슷하지만, 묵시적 해제는 옵셔널 변수를 사용하는 모든 경우에 적용할 수 있고, 옵셔널 변수의 타입을 선언할 때 묵시적 해제를 미리 선언해 주어야 한다는 차이점이 있다.

1
2
3
4
5
// 묵시적 옵셔널 선언
var str : String! = "Swift"
print(str)

// 결과 : Swift

타입 어노테이션 뒤에 연산자 하나로 결과가 달라진다.

묵시적 옵셔널을 사용하면 옵셔널 타입 변수의 연산을 처리할 수 있다.

1
2
3
4
5
var value01 : Int? = 10
value01 + 5 // Error

var value02 : Int! = 10
value02 + 5 // 15

일반적으로는 옵셔널 타입의 연산은 오류이지만, 묵시적 해제를 선언한 옵셔널 타입은 아니다. 묵시적 옵셔널 해제를 사용하는 경우는 ‘형식상 옵셔널로 정의해야 하지만, 실제로 사용할 때는 절대 nil 값이 대입될 가능성이 없을 변수에.’

묵시적 옵셔널은 클래스나 구조체에서다. 주로 멤버 변수를 정의할 때 선언과 초기화를 분리시켜야 하는 경우에 대항한다.(아직은 자세히 알 필요는 없다.)

옵셔널은 까다롭고 귀찮은 개념일 수도 있지만, 옵셔널의 강점은 안전성과 코드 간결성이다.

다음 예시는 Objective-C와 swift의 옵셔널 사용에 대한 비교이다.

1
2
3
4
5
6
/* Objective-C */
if (myDelegate != nil) {
if([myDelegate respondsToSelector:@selector(scrollViewDidScroll:)] {
[myDelegate scrollViewDidScroll:myScrollView];
}
}
1
2
/* Swift */
myDelegate?.scrollViewDidScroll?(myScrollView)

Swift 집단 자료형

꼼꼼한 재은씨의 Swift3 기본편을 보며 Swift 문법 내용을 정리한 내용입니다.

집단 자료형

  • 배열 : 일련번호로 구분되는 순서에 따라 데이터가 정렬된 목록 형태의 자료형
  • 집합 : 중복되지 않은 유일 데이터들이 모인 집합 형태의 자료형
  • 튜플 : 종류에 상관없이 데이터들을 모은 자료형. 수정 및 삭제 불가능
  • 딕셔너리 : 배열과 유사. 일련번호 대신 키를 사용하여 키-값으로 연관된 데이터들이 순서 없이 모인 자료형

튜플 외에는 모든 데이터의 타입이 동일해야 한다. 여러 타입 섞어쓰기가 불가하다.

배열

  • 순서를 가지는 리스트 형식의 값을 저장하는 데 사용된다.
  • 다른 언어의 배열과 사용법은 같다.
  • 저장하는 값의 타입이 모두 같아야 한다.
  • 선언 시 배열에 저장할 값 타입을 정의해야 한다.
  • 배열의 크기는 동적으로 지정이 가능하다.
1
2
3
4
5
6
var cities = ["Seoul", "New York", "LA", "Beijing"]
cities.count // 4 -> 배열의 길이

for i in 0..<length {
print("\(i)번째 배열 원소는 \(cities[i])")
}

배열의 선언

1
2
3
4
5
6
7
8
9
10
11
12
var 배열이름 = Array<데이터 타입>()

var 배열이름 : Array<데이터 타입>
배열이름 = Array() // 배열 초기화

var 배열이름 : [데이터 타입] // 선언
배열이름 = [데이터 타입]()
배열이름 = []

var cities = Array<String>()
var cities : Array<String>
cities = Array()

위와 같이 배열의 선언과 초기화는 다양한 방법이 존재한다.

1
2
3
4
5
6
7
8
9
10
11
var arr1 : [String] // 선언
arr1 = [String]() // 초기화

var arr2 : [String] // 선언
arr2 = [] // 초기화

var arr3 : [Int] = [] // 타입 어노테이션 + 초기화

var arr4 : Array<Float> = [Float]() // 타입 어노테이션 + 제네릭 + 초기화

var arr5 : [String] = Array() // 타입 어노테이션 + 구방식의 초기화

배열에 아이템이 없다면 isEmpty는 true를 리턴한다.

배열 아이템 동적 추가

1
2
3
append( _:)
insert( _:, axt:)
append(contentsOf:)
  • append( _ :)메서드는 값을 배열 맨 뒤에 추가한다.
  • insert( _ :at:) 메서드는 값을 원하는 위치에 추가하고 그 뒤 아이템 인덱스는 증가한다.
  • append(contentsOf:) 메서드는 배열 맨 마지막에 배열 째로 추가한다.(여러 개 아이템)

언더바( _ )는 추가될 아이템을 지칭한다.

Array(repeating:count:)

배열의 인덱스가 개수만큼 미리 정의되고, 여기에 기본값이 추가된 상태로 배열이 생성된다.

1
2
3
4
5
6
var arr = [String](repeating: "None", count: 3)
/*
0 -> "None"
1 -> "None"
2 -> "None"
*/

집합 (Set)

중복이 없는 같은 타입의 값을 저장하고자 할 때 사용된다.
데이터 타입은 해시 연산을 할 수 있는 타입이어야 한다.(대부분 타입은 해시 연산이 가능하다.)

1
Set <데이터 타입> ()

집합의 동적 추가와 삭제

insert

파라미터 값을 추가하지만 이미 있는 아이템이라면 아무 처리를 하지 않는다.

remove

파라미터 값을 삭제한다. 없는 아이템이라면 아무 처리를 하지 않는다.

removeAll

집합의 모든 아이템을 삭제한다.

집합 연산

기본 집합 연산

  • intersection : 교집합. 공통된 부분
  • symmetricDifference : 어느 한쪽에만 있는 아이템을 선택(합집합-교집합)
  • union : 합집합
  • subtract : 차집합 앞에서 뒤를 차집합시킨다.

부분집합과 포함관계 판단 연산

  • isSubset : 부분집합인지의 여부
  • isSuperset : 상위집합인지의 여부
  • isStrictSubset & isStrictSuperset : 위 두개와 같지만 반대로 반환
  • isDisjoint : 공통값을 확인. 있다면 false, 없다면 true

튜플(Tuple)

튜플은 독특한 자료형이다. 파이썬에도 등장하는 이 튜플은 한 가지 타입만을 위한 자료형이 아니다.

소괄호를 이용하여 튜플을 형성한다. 인덱스 넘버가 존재한다.

1
2
let tupleValue:(String, Character, Int, Float, Bool) = ("a", "b", 1, 2.5, true)
let (a, b, c, d, e) = tupleValue

결과값으로 튜플을 반환하는 함수

1
2
3
4
5
6
func getTupleValue() -> (String, String, Int) {
return ("t", "v", 100)
}

// 함수가 반환하는 튜플을 튜플 상수로 바인딩
let (a, b, c) = getTupleValue()

딕셔너리

1
[ 키 : 데이터, 키 : 데이터, ... ]
  • 하나의 키는 하나의 데이터에만 연결되어야 한다.
  • 하나의 딕셔너리에서 키는 중복될 수 없다. 중복 선언 시, 아이템 추가가 아닌 수정이 이루어져 기존 키에 연결된 데이터가 삭제된다.
  • 저장할 수 있는 데이터 타입에는 제한이 없다. 하나의 딕셔너리에 저장하는 데이터 타입은 모두 일치해야 한다.
  • 딕셔너리의 아이템에는 순서가 없지만 키에는 내부적으로 순서가 있어서 for 구문을 이용한 탐색이 가능하다.
  • 키의 타입은 대부분 가능하지만, 해시 연산이 가능해야 한다.
1
2
3
4
5
6
7
var capital = ["KR":"Seoul", "EN":"London", "FR":"Paris"]

capital["KR"] // "Seoul"
capital["EN"] // "London"
capital["FR"] // "Pairs"

Dictionary <키의 타입, 값의 타입> ()

딕셔너리 동적으로 제어하기

1
2
3
4
5
6
7
8
9
10
11
<딕셔너리 객체>.updateValue(<저장할 데이터>, forKey:<데이터 참조 및 저장하는 데 사용할 키>)

newCapital.updateValue("Seoul", forKey: "KR")

newCapital.updateValue("London", forKey: "EN")

newCapital.updateValue("Sapporo", forKey: "JP")

// 삭제
// removeValue(forKey:)
newCapital.removeValue(forKey: "CA")

Swift 흐름 제어 구문

꼼꼼한 재은씨의 Swift3 기본편을 보며 Swift 문법 내용을 정리한 내용입니다.

흐름 제어 구문

  • 반복문 (Loop Statements
  • 조건문 (Conditional Statements)
  • 제어 전달문 (Control Transfer Statements)

반복문

for ~ in 구문

1
2
3
4
5
6
7
8
9
10
11
for <루프 상수> in <순회 대상> {
// 실행할 내용
}

for row in 1...5 {
print(row)
}

for row in 1...9 {
print("2 X \(row) = \(row * 2)")
}
  • 순회 대상으로 사용할 수 있는 타입
    • 배열
    • 딕셔너리
    • 집합
    • 범위 데이터
    • 문자열
  • 순회 대상의 값들이 루프 상수에 저장되어 쓰인다.
1
2
3
4
var chars = "swift"
for char in chars.characters {
print("개별 문자는 \(char)입니다.")
}

루프 상수의 생략

1
2
3
4
5
6
7
8
9
let size = 5
let padChar = "0"
var keyword = "3"

for _ in 1...size {
keyword = padChar + keyword
}

print("\(keyword)")

순회 시 대상 아이템이 필요하지 않은 경우에 생략 가능하다.

중첩을 활용한 구구단 코드

1
2
3
4
5
for i in 1..<10 {
for j in 1..<10 {
print("\(i) X \(j) = \(i * j)")
}
}

while 구문

  • 실행 횟수가 명확하지 않을 때
  • 직접 실행해보기 전까지는 실행 횟수를 결코 알 수 없을 때
  • 실행 횟수를 기반으로 할 수 없는 조건일 때
1
2
3
4
while <조건식> {
<실행할 구문>
}
// 조건식이 true 일 시, 무한 반복

repeat ~ while 구문

다른 언어의 do ~ while 과 같은 구문이다.

1
2
3
4
repeat {
<실행할 구문>
}
while <조건식>

조건문

  • if
  • guard
  • switch

if 구문

다른 언어에서의 if와 동일하게 사용된다.

1
2
3
if <조건식> {
<실행할 구문>
}
  • if ~ else
  • if 중첩
  • if ~ else if ~ else

guard 구문

표현식의 결과가 참인지 거짓인지에 따라 구문의 실행 여부를 결정한다.
if와의 차이점은 참일 때의 실행 구문이 없다는 것이다.

결국 false일 때 블록 안쪽을 실행한다.

1
2
3
guard <조건식 또는 표현식> else {
<조건식 또는 표현식의 결과가 false일 때 실행될 코드>
}

#available 구문

1
2
3
4
5
if #available(<플랫폼이름 버전>, <...>, <*>) {
<해당 버전에서 사용할 수 있는 API 구문>
} else {
<API를 사용할 수 없는 환경에 대한 처리>
}

호출 연산자()를 통해 플랫폼 이름과 버전 등의 인자값을 입력할 수 있다.
API 버전에 따라 처리하는 구문을 구별해준다.

사용할 만한 플랫폼

  • 아이폰, 아이패드 등 터치 기반 스마트 기기에 사용되는 iOS
  • 맥 컴퓨터에 사용되는 OSX
  • 애플 시계에 사용되는 watchOS
  • 애플 TV에 사용되는 tvOS
1
2
3
4
5
if #available (iOS 9, OSX 10.10, watchOS 1, *) {
// iOS 9용 API 구문 또는 OS X 10.10용 API 구문, watchOS 1용 API 구문
} else {
// API를 사용하지 못했을 때에 대한 실패 처리
}

switch 구문

1
2
3
4
5
6
7
8
switch <비교 대상> {
case <비교 패턴1> :
<비교 패턴1이 일치했을 때 실행할 구문>
case <비교 패턴2>, <비교 패턴3> :
<비교 패턴2 또는 3이 일치했을 때 실행할 구문>
default :
<어느 비교 패턴과도 일치하지 않았을 때>
}

제어 전달문

한 부분에서 다른 부분으로 제어 흐름을 전달하여 코드 실행 순서를 변경해준다.

break, continue, fallthrough, return 등이 있다.

Swift 연산자

꼼꼼한 재은씨의 Swift3 기본편을 보며 Swift 문법 내용을 정리한 내용입니다.

Swift 기본 문법

연산자

산술 연산자

단항 연산자 : -
이항 연산자 : + , - , * , / , %

비교 연산자

크기를 비교하는 연산자

<, >, <=, >=, ==, !=

논리 연산자

!(NOT), &&(AND), ||(OR)

범위 연산자

닫힌 범위 연산자 : 주어진 피연산자 a, b를 포함하는 범위를 나타낸다.

1
2
1...5 // 1, 2, 3, 4, 를 나타낸다.

반 닫힌 범위 연산자 : 주어진 피연산자 a, b 중 a만 포함된다.

1
2
1...< 5 // 1, 2, 3, 4
1 >...5 // error. 사용할 수 없다. 오직 뒤에 위치한 피연산자만 가능

범위연산자는 주로 반복문 for 등에 사용하기 좋은 연산자이다.

대입 연산자

값을 변수에 대입할 때 사용한다. 좌변 객체에 우변 값을 대입한다.

증감 연산자

++, – 두 연산자가 있다. 변수에 1을 증가, 감소시킨다.

3버전에서는 증감 연산자가 삭제되었다

Swift 변수와 자료형

꼼꼼한 재은씨의 Swift3 기본편을 보며 Swift 문법 내용을 정리한 내용입니다.

Swift 기본 문법

기초 문법

변수의 대소문자를 구분한다. (모든 객체)

1
2
3
4
5
var a = 30;
var A = 30;

// 함수와 메소드, 인스턴스명 첫 글자 -> 소문자
// 클래스와 구조체, 프로토콜 등 객체의 첫 글자 -> 대문자

구문 끝의 세미콜론(;)은 옵션

써도 되고 안 써도 된다. 인터프리터가 자동으로 끝을해석해 준다.
가독성의 이유로 주로 붙이기도 한다.

main() 메서드가 없다.

@UIApplicationMain 어노테이션을 사용하여 시작 객체를 지정한다.
@UIApplicationMain 어노테이션이 붙는 객체는 하나다.

문자열, 문자 모두 큰따옴표(“ “)를 사용한다.

자바를 생각하고 쓴다면 헷갈릴 수 있다. character 타입도 큰따옴표로 표시한다.

import 키워드는 라이브러리와 프레임워크 참조 용도이다.

ex) import UIKit

한 줄 주석, 여러 줄 주석 모두 사용 가능하다.

1
2
3
4
5
// 한 줄 주석

/*
* 여러 줄 주석
*/

try ~ catch 와 같이 오류 처리를 지원한다.

이는 2.0에서 처음 추가되어 직접 오류를 throw 할 수 있도록 하는 기능

자바 등에서는 일반적이지만 스위프트에서는 처음에 이러한 것을 지원하지 않았다.

변수와 상수

변수는 값이 변할 수 있는 것, 상수는 한 번 정하면 값을 바꿀 수 없다.

변수는 지정된 타입에 해당하는 값만 넣을 수 있다.

선언, 초기화

1
2
3
4
5
6
7
8
// 변수
var year = 2017
var message = "Hello, World"

// 상수
let birthYear = 1990
let welcomeMsg = "환영합니다."
// 이후에 변경을 시도하면 에러.

변수와 상수의 이름짓기

  1. 알파벳, 한글, 아라비아 숫자, 특수기호, 한자, 이미지용 바이너리 코드 사용 가능
  2. 연산자, 공백 등은 사용 불가능하지만 언더바는 사용 가능하다.
  3. 예약어, 키워드 등은 사용 가능하지만 기존 것에서 대소문자 등을 바꾸면 가능하다.
  4. 첫째자리에 숫자가 올 수 없다.
1
2
3
4
5
var Class = 1 // okay
var class = 1 // Error

var Enum = 2 // okay
var enum = 2 // Error

자료형

기본 자료형

Int

Integer. 부호 있는 정수값 (127 ~ -128 까지 저장이 가능하다.)

자료형 저장할 수 있는 값 범위 크기
Int8 127 ~ -128 8bit
Int16 32767 ~ -32768 16bit
Int32 2147483647 ~ -2147483648 32bit
Int64 9223372036854775807 ~ -9223372036854775808 64bit

UInt

Integer와 유사하지만 부호가 없는 정수. (0 ~ 255)

음수의 범위를 양수가 가져간다고 보면 되겠다.

Double, Float

실수 범위이다. Float는 32bit, Double은 그 두 배인 64bit까지 표현 가능하다.

Bool

true/false의 값을 가질 수 있는 자료형이다.

String

아마 모든 언어에서 제일 많이 쓰는 자료형일 것이다.(C 제외)

Character

한 개 문자를 저장하는 자료형이다. 앞서 언급했듯이 큰따옴표(“ “)를 사용한다.

타입 추론, 어노테이션

1
2
3
4
5
6
7
var age : Int // Int 타입임을 명시한다.

var age
age = 28 // 초기화를 하면서 자료형을 정한다.

var weight : Float = 28
var hello : Character = "안" // 이와 같이 선언 가능하다.

타입이 다른 변수끼리 결합

1
2
3
4
var format = "당신의 나이는 "
var age = 28

var msg = format + String(age) // 타입이 다르다면 변형 후 결합한다.

문자열 템플릿

1
2
3
4
5
6
7
var name = "휘"
var year = 1990
var month = 8
var day = 17

// 문자열 템플릿을 사용해보자.
var birth = "\(name)\(year)\(month)\(day)일에 출생했습니다."

문자열 템플릿 안에서의 연산 역시 가능하다.