App

Swift 둘러보기

DuL2 2023. 7. 10. 18:52

변수와 상수

var name = "Donguk Lee"
let birthyear = 1992

`var`는 변수(variable), `let`은 상수(constant)를 의미한다. 자바스크립트 개념과 같다고 보면된다.

 

Swift는 정적 타이핑 언어이다. 따라서 타입을 명시해주어야 하는데 다음과 같이 타입을 명시한다.

var name: String = "Donguk Lee"
let birthyear: Int = 1992
var height: Float = 179.5

 콜론(:)을 붙이고 타입명을 명시하면 된다(타입 어노테이션). 

 일반적으로 다른 타입간의 연산은 불가능하다. 다만, String의 경우 다음과 같이 문자열을 만들어 사용할 수 있다.

String(birthyear) + "년에 태어난 " + name + "씨!" // 1992년에 태어난 이동욱씨!
"\(birthyear)년에 태어난 \(name)씨!" // 1992년에 태어난 이동욱씨!

타입 추론(Type Inference)

 다만 첫 예시처럼 컴파일러가 알아서 타입을 인식할 수 있다. 예를들어 큰 따옴표(")는 String으로 숫자는 Int로 인식해준다.

 

배열(Array)과 딕셔너리(Dictionary)

언어에 기초인 배열과 딕셔너리를 가지고 있다. 정의하는 법은 대괄호를 이용한다 같다.

var fruits = ["Apple", "Banana", "Coconut"]
var myInfo = [
    "name": "이동욱",
    "birthday": "0525",
    "height": "179"
]

배열에 접근하고 싶을 때는 대괄호에 0부터 시작하는 숫자를 넣어(fruits[1]) 접근한다. 딕셔너리의 경우에는 key값을 넣어 접근할 수 있다.

fruits[0] = Abocado

myInfo["name"] = "Donguk Lee"

빈배열로 정의하려면 위의 예시에서 값만 비운 후 대괄호만 명시하여 정의하면 된다. 타입 명시의 경우에도 대괄호를 씌워 타입 정의를 하면된다.

var fruits: [String] = []
var myInfo: [String: String] = [:]

var fruits: [String]()
var myInfo: [String: String]()

혹은 함수처럼 괄호를 붙여 사용하게 되면 생성자(Initializer)를 호출하여 만들어지게 된다.

 

조건문과 반복문

조건 검사시에는 if, switch를 사용한다.

var age = 19
var student = ""

if age >= 8 && age < 14 {
  student = "초등학생"
} else if age < 17 {
  student = "중학생"
} else if age < 20 {
  student = "고등학생"
} else {
  student = "기타"
}

student // 고등학생

if 조건절에는 Bool 타입을 사용하여 참, 거짓 값이 들어가야한다.

 

`switch`문은 패턴 매칭이 가능하다. 범위(Range)값을 넣어 확인할 수 있다. 위의 코드를 변경해보면 다음과 같다.

switch age {
case 8..<14:
  student = "초등학생"
case 14..<17:
  student = "중학생"
case 17..<20:
  student = "고등학생"
default:
  student = "기타"
}

 

반복문에는 for와 while을 사용한다. 배열과 딕셔너리 예시를 사용해보면 다음과 같이 배열을 돌아가며 print하는 반복문을 작성해볼 수 있다.

for i in 0..<100 {
  i
}

for fruit in fruits {
  print("\(fruit)")
}

for (info, value) in myInfos {
  print("\(info)은(는) \(value)입니다.")
}

만약 첫 예시에서 `i`를 굳이 사용하고 싶지 않다면 `_`로 대체해 생략할 수도 있다.

for _ in 0..<10 {
  print("Hello! World!")
}

 

while문의 경우에는 조건이 true일 때 반복된다.

 

Optional

 Swift도 자바8처럼 Optional을 가질 수 있다. 다만 더욱 용이하게 자체적으로 정의할 수 있다. Optional은 값이 있을수도 없을수도 있는 타입을 말한다. Swift는 값이 비어있을 때 `nil`이라고 표시한다.

 

 String 타입에서 ""와 nil은 엄연히 다른 값이다. `빈 값`과 `값이 없음`을 나타내기 때문이다.

var name: String = nul // 컴파일 에러

// 옵셔널 변수 정의
var name: String?	// 초기값: nil
name = "이동욱"		// 값 이동욱 정의

옵셔널 변수는 nil과 다른 타입을 가지고 있으므로 기존 값에 다시 선언할 수 없다. 따라서 독립적 타입이라고 생각해야한다.

 

옵셔널 바인딩 (Optional Binding)

 옵셔널 바인딩이란 옵셔널의 값을 일반적인 변수에 대입하는 것을 말한다. 즉, String? 타입의 값이 있는지 없는지 확인한 후 String 타입 변수에 재선언 해주는 것을 말한다.

if let email = optionalEmail {
  print(email) // optionalEmail의 값이 존재한다면 해당 값이 출력
}

 또한 다중으로 콤마(,)를 사용하여 여러 옵셔널을 동시에 바인딩해줄 수 있다. 

if let email = optionalEmail,
	name = optionalName {	// 두번째 Let부터는 생략 가능하다.
    	// 이름과 이메일 모두 값이 존재시 출력
	print(\(name)의 이메일은 \(email)이다.)
}

 if 문이므로 Bool 타입이라면 조건도 함께 정의할 수 있다. 

 

옵셔널 체이닝(Optional Chaining)

 옵셔널 체이닝은 변수의 값이 가질 수 있는 상태 즉, nil, true, false를 확인하는 것을 의미한다.

 

 변수의 상태를 확인하기 위해 코드를 풀어 작성해 보자면 다음과 같이 작성할 수 있다.

let array: [String]? = []
var isEmptyArray = false

if let array = array, array.isEmpty {
  isEmptyArray = true
} else {
  isEmptyArray = false
}

isEmptyArray

위의 코드를 통해 array가 nil인 경우, 빈 배열인 경우, 요소가 있는 경우 세 가지 경우를 확인할 수 있다.

Swift는 이를 짧게 줄일 수 있다.

let isEmptyArray = array?.isEmpty == true

`array?.isEmpty`를 통해 Bool값을 변환하게 되고 array는 ?(옵셔널)이었기 때문에 즉 Bool?의 결과를 반환하게 된다. 마지막으로 `==` 연산자를 이용해 실제로 값이 존재하는지 아닌지를 확인하여 isEmptyArray에 선언해준다.

 

옵셔널 벗기기

옵셔널을 사용할 때마다 바인딩을 통해 nil 체크를 해주는 것이 바람직하지만 개발을 하다보면 분명히 값이 존재할 것임에도 불구하고 옵셔널로 사용해야 하는 경우가 종종 있는데 Swift에는 이 때 옵셔널에 값이 있다고 가정하고 값에 바로 접근할 수 있도록 도와주는 키워드가 있다. `!`를 붙여 사용해주면 값에 바로 접근이 가능하다.

print(optionalName) // Optional("이동욱")
print(optionalName!) // 이동욱

`!`를 사용할 때 주의할 점이 있는데, 만약 값이 nil인 경우에는 자바의 nullPointerException과 같은 런타임 에러가 발생한다.

var optionalName: String?
print(optionalName!) // 런타임 에러 발생

런타임 에러가 발생하는 경우에는 iOS 앱이 강제로 종료(크래시)되므로 아주 주의해서 사용해야한다.

 

암묵적으로 벗겨진 옵셔널(Implicitly Unwrapped Optional)

 옵셔널을 ?가 아닌 직접적으로 `!`로 정의할 수 있다. 이렇게 정의한 옵셔널은 `ImplicitlyUnwrappedOptional` 이라는 옵셔널로 정의된다. 즉, 암묵적으로 벗겨진 옵셔널이다.

 

 기본적으로 개발을 할 때는 보수적으로 항상 nil 체크를 위해 일반적 옵셔널을 사용하여 바인딩이나 체이닝을 통해 값을 체크하고 사용하는 것이 바람직한 개발이 될 것이다.

 

버전별 옵셔널
Swift 3에서부터  ImplicitlyUnwrappedOptional로 문자열을 포매팅할 경우 Optional로 포매팅 되므로 주의해서 사용해야 한다. 이는 ImplicitlyUnwrappedOptional를 일반 Optional과 거의 동일하게 취급했기 때문이다. 

관련 링크 : SE-0054 git 문서

 

함수와 클로저

 Swift는 javascript와 유사하다. 함수는 `func` 키워드를 사용해서 정의하며 `->`를 통해 함수의 반환 타입을 지정할 수 있다.

func testFunc(to name: String, height height: Int) {
	print(name + "의 키는 \(height)입니다.")
}

testFunc(to: "이동욱", height: 179)

 파라미터의 이름을 생략하고 싶다면 `_` 언더바를 사용하여 파라미터 이름을 생략할 수도 있고, 기본값을 설정할 수도 있다. 만약 파라미터이름과 함수에서 사용하는 파라미터의 이름이 같다면 생략 가능하다.

func testFunc(_ name: String, height: Int = 179) {
	print(name + "의 키는 \(height)입니다.")
}

testFunc("이동욱")

 개수가 정해지지 않은 여러 개의 파라미터(Variadic Parameters), 가변인자를 사용하고 싶다면 다음과 같이 `...`을 사용해 명시할 수 있다.

func sum(_ numbers: Int...) -> Int {
  var sum = 0
  for number in numbers {
    sum += number
  }
  return sum
}

sum(1, 2)	// return -> 3
sum(3, 4, 5)	// return -> 12

 Swift는 javascript처럼 함수가 값이 될 수 있으므로 함수 안에 함수가 존재 가능하고 변수에 선언 가능하며 return 값으로 함수를 반환할 수도 있다.

func helloGenerator(message: String) -> (String, String) -> String {
  func hello(firstName: String, lastName: String) -> String {
    return lastName + firstName + message
  }
  return hello
}

let hello = helloGenerator(message: "님 안녕하세요!")
hello("Donguk", "Lee")

자바스크립트를 사용했다면 익숙할 수 있으나 자바에서 보기엔 생소한 방식이므로 잘 기억해두자.

 

helloGenerator가 String을 받아 (String,String)을 반환하고 내부 함수에서 다시 `-> String`으로 반환되는 것을 확인하자.

 

클로저 (Closure)

 클로저를 사용하게 되면 위에서 작성했던 hello 함수를 압축하여 더욱 간결한 코드로 변환할 수 있다. 클로저는 중괄호(`{}`)로 감싼 `실행가능한 코드 블럭`을 의미한다.

func helloGenerator(message: String) -> (String, String) -> String {
  return { (firstName: String, lastName: String) -> String in
    return lastName + firstName + message
  }
}

 복잡해보이지만 Swift의 컴파일러의 타입 추론 능력을 통해 굳이 명시해도 되지 않는 것들을 생략하게 되면 다음과 같이 중괄호와 한줄의 코드로 변환이 가능하다.

func helloGenerator(message: String) -> (String, String) -> String {
  return { $1 + $0 + message }
}

 타입 추론을 통해 불필요한 String 타입 명시가 생략 가능하고, 파라미터의 순서에 따라 `$` 기호를 붙여 순서대로 사용이 가능하다. 또한 코드가 한줄이라면 return마저도 생략 가능하기에 위와 같이 생략하여 압축할 수 있다.

 

 위에서 언급했듯 함수는 변수에 정의할 수 있다. 따라서 클로저도 다음과 같이 정의할 수 있다.

let hello: (String, String) -> String = { $1 + $0 + "님 안녕하세요!" }
hello("동욱", "이")

 또한 클로저를 파라미터로도 받을 수 있다.

func calculate(number: Int, using closure: Int -> Int) -> Int {
  return closure(number)
}

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

 위와 같이 변하는 클로저에 따라서 계산을 하는 함수를 만들 수 있다. 예를들어 이벤트 기간에 따라 할인율에 따라 계산을 해야할 때 상위 함수는 그대로 두고 할인율이 다른 클로저를 적용하여 사용할 수 있을 것이다.

 

 위에서처럼 이를 더욱 생략할 수도 있다.

calculate(number: 10, using: {
  $0 * 2
})

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

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

이런 구조로 만들어진 예시가 sort()와 filter()이다. 다음 예시를 보면 클로저를 사용하여 sorting의 순서(오름차순, 내림차순)을 결정한다던지 조건에 맞는 값들을 filtering할 수도 있다.

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]

 `by`는 위에서 언급했듯 마지막 클로저이므로 생략 가능하기 때문에 위의 예시코드처럼 작성할 수 있다.

참고 링크 : Swift Docs Library - sort

 

sort() | Apple Developer Documentation

Sorts the collection in place.

developer.apple.com

 

클로저 활용하기

 sort(), filter() 이외에도 클로저는 많은 곳에 쓰인다 대표적인 다른 예로는 map()과 reduce()가 있다. 자바의 stream이나 javascript, python을 써본 사람이라면 익숙할 것이다.

 

 Swift에서도 같은 기능을 사용할 수 있는데, map()은 파라미터로 받은 클로저를 모든 요소에 적용시켜 그 결과를 반환해준다. 따라서 만약 배열에 map을 사용하게 되면 주어지는 클로저 파라미터에 따라 반환되는 배열의 값이 달라진다. 예제를 통해 확인해보자.

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

 reduce()는 초기값이 주어지고 초기값과 배열의 첫번째 값을 연산한 결과를 클로저 파라미터에 넣어 연산 후 이 과정을 모든 배열의 값을 거쳐가며 진행하는 것이다. 예를들어 초기값부터 모든 열을 더한다면 다음과 같이 작성할 수 있다.

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

 초기값으로는 0이 주어졌다 따라서 첫 연산시 `$0 + $1`은 `0 + 1`을 의미한다. 1은 배열의 첫번째 값이다. 이후 1은 다시 배열의 두번째 값인 3과 더해져 4가 된다. 이 과정을 배열이 끝날 때까지 반복한 후 28이 반환된다.

참고사항
Swift에서는 연산자(+,-,*,/)도 함수이다. 함수는 곧 클로저이기 때문에 연산자는 클로저이다. 따라서 연산자의 계산의 경우 따로 중괄호에 작성하는 것이 아닌 연산자 자체를 파라미터로 넣을 수 있다.
arr1.reduce(0, +) // 28​

 

클래스와 구조체

"Class는 참조타입이고 ARC로 메모리 관리를 한다. Struct는 값 타입이다."

 개발자가 앱을 만들 때 가장 중요하게 고려해야 할 것 중 하나는 성능이다. 성능 개선을 위해 클래스와 구조체의 차이점을 알고 효율적인 개발을 해야한다. 

TIP: 면접 단골 질문으로 단순 Swift 공식문서에 나와있는 기본적인 내용뿐만 아니라 좀 더 깊게 공부했는지 파악할 수 있다.

 

 클래스와 구조체의 공통점으로는 다음과 같다.

  • 값을 저장할 프로퍼티를 선언할 수 있다.
  • 함수적 기능을 하는 메서드를 선언할 수 있다.
  • 내부 값에 .을 사용하여 접근할 수 있다.
  • 생성자를 사용해 초기 상태를 설정할 수 있다.
  • extension을 사용하여 기능을 확장할 수 있다.
  • Protocol을 채택하여 기능을 설정할 수 있다.

 다음은 둘간의 차이점이다.

클래스 구조체
- 참조타입
- ARC로 메모리를 관리한다.
- 같은 클래스 인스턴스를 여러 개의 변수에 할당한 뒤 값을 변경시키면 할당한 모든 변수에 영향을 준다.(메모리만 복사)
- 상속이 가능하다.
타입 캐스팅을 통해 런타임에서 클래스 인스턴스의 타입을 확인할 수 있다.
- deinit을 사용해 클래스 인스턴스의 메모리 할당을 해제할 수 있다.
- 값 타입
- 구조체 변수를 새로운 변수에 할당할 때마다 새로운 구조체가 할당된다.
- 즉 같은 구조체를 여러 개의 변수에 할당한뒤 변경시키더라도 다른 변수에 영향을 주지 않는다.(값 자체를 복사, 독립적)

클래스와 구조체를 정의해보자.

class Dog {
  var name: String?
  var age: Int?

  func simpleDescription() -> String {
    if let name = self.name {
      return "🐶 \(name)"
    } else {
      return "🐶 No name"
    }
  }
}

struct Coffee {
  var name: String?
  var size: String?

  func simpleDescription() -> String {
    if let name = self.name {
      return "☕️ \(name)"
    } else {
      return "☕️ No name"
    }
  }
}

var myDog = Dog()
myDog.name = "찡코"
myDog.age = 3
print(myDog.simpleDescription()) // 🐶 찡코

var myCoffee = Coffee()
myCoffee.name = "아메리카노"
myCoffee.size = "Venti"
print(myCoffee.simpleDescription()) // ☕️ 아메리카노

클래스는 상속이 가능하다.

class Animal {
  let numberOfLegs = 4
}

class Dog: Animal {
  var name: String?
  var age: Int?
}

var myDog = Dog()
print(myDog.numberOfLegs) // Animal 클래스로부터 상속받은 값 (4)

클래스는 참조(Reference)하고, 구조체는 복사(Copy)한다.

var dog1 = Dog()  // dog1은 새로 만들어진 Dog()를 참조합니다.
var dog2 = dog1   // dog2는 dog1이 참조하는 Dog()를 똑같이 참조합니다.
dog1.name = "찡코" // dog1의 이름을 바꾸면 Dog()의 이름이 바뀌기 때문에,
print(dog2.name)  // dog2의 이름을 가져와도 바뀐 이름("찡코")이 출력됩니다.

var coffee1 = Coffee()   // coffee1은 새로 만들어진 Coffee() 그 자체입니다.
var coffee2 = coffee1    // coffee2는 coffee1을 복사한 값 자체입니다.
coffee1.name = "아메리카노" // coffee1의 이름을 바꿔도
coffee2.name             // coffee2는 완전히 별개이기 때문에 이름이 바뀌지 않습니다. (nil)

생성자(Initializer)

 클래스와 구조체 모두 생성자를 가지고 있다. 생성자에서 속성의 초깃값을 지정할 수 있다.

class Dog {
  var name: String?
  var age: Int?

  init() {
    self.age = 0
  }
}

struct Coffee {
  var name: String?
  var size: String?

  init() {
    self.size = "Tall"
  }
}

만약 속성(프로퍼티)이 옵셔널이 아니라면 항상 초깃값을 가져야 한다. 옵셔널이 아닌 속성이 초깃값을 가지고 있지 않으면 컴파일 에러가 난다.

class Dog {
  var name: String?
  var age: Int // 컴파일 에러!
}
에러 메세지
stored property 'age' without initial value prevents synthesized initializers

속성을 정의할 떄 초깃값을 지정해주는 방법과, 생성자에서 초깃값을 지정해주는 방법이 있다.

// 속성 초깃값 세팅
class Dog {
  var name: String?
  var age: Int = 0 // 속성을 정의할 때 초깃값 지정
}

// 생성자 초깃값 세팅
class Dog {
  var name: String?
  var age: Int

  init() {
    self.age = 0 // 생성자에서 초깃값 지정
  }
}

생성자도 함수와 마찬가지로 파라미터를 받을 수 있다.

class Dog {
  var name: String?
  var age: Int

  init(name: String?, age: Int) {
    self.name = name
    self.age = age
  }
}

var myDog = Dog(name: "찡코", age: 3)

 만약 상속받은 클래스면 생성자에서 상위 클래스의 생성자를 호출해주어야 한다. 생성자의 파라미터가 상위클래스의 파라미터와 같다면, override 키워드를 붙여주어야 한다. super.init() 은 클래스 속성들의 초깃값을 모두 설정한 후에 사용해야하며 이 후 self를 사용할 수 있게 된다.

 

deinit은 메모리에서 해제된 직후에 호출된다.

 

속성 (Properties)

 속성은 크게 두 가지로 나뉜다. 값을 가지는 속성(Stored Property)과 계산되는 속성(Computed Property)이다.

 

 지금까지 값을 저장했던 속성은 Stored Property였다. 이와달리 Computed Property는 연산하여 어떤 값을 반환하는데 그를 위해 ge과 set을 가진다. 

 

 다음은 10진수를 16진수로 16진수를 10진수로 받을 수 있는 16진수 Computed Property에 대한 예제코드이다.

struct Hex {
  var decimal: Int?
  var hexString: String? {
    get {
      if let decimal = self.decimal {
        return String(decimal, radix: 16)
      } else {
        return nil
      }
    }
    set {
      if let newValue = newValue {
        self.decimal = Int(newValue, radix: 16)
      } else {
        self.decimal = nil
      }
    }
  }
}

var hex = Hex()
hex.decimal = 10
hex.hexString // "a"

hex.hexString = "b"
hex.decimal // 11

 hexString이 호출될 때 get이 호출되어 decimal 속성을 기반으로 16진수 숫자로 변환되어 반환하게 된다. 혹은 저장하게 되면 set이 호출되어 Int 값으로 변경한 후 decimal에 저장된다.

 

 참고로 이처럼 get, set 모두 사용하지 않고 get만 사용하게 되면 get을 생략할 수도 있다.

 

 이외에 willSet, didSet 를 사용하면 속성에 값이 지정되기 전과 후에 원하는 처리를 할 수 있다. 각 호출은 newValue와 oldValue라는 예약어를 가지며 값을 가져올 수 있게 한다.

 

 보통 두 가지는 어떤 속성의 값이 바뀌었을 때 UI를 업데이트하거나 특정 메서드를 호출하는 등의 역할을 할 때 사용된다.

 

튜플(Tuple)

튜플은 어떤 값들의 묶음을 말한다. 간단한 자료형을 만들 때 구조체 대신 사용할 수 있다. 배열과는 다르게 길이가 고정되어 있으며 값에 접근할 때는 배열과 달리 .을 이용해서 접근한다.

var myInfo = (name: "이동욱", birthday: "0525", gender: "Man", hobby: "game")

myInfo.0 // 이동욱
myInfo.hobby = "cooking"

튜플을 응용하면 다음과 같이 여러 변수에 저장을 할 수도 있고 무시하고 싶은 값이 있다면 _ 키워드를 통해서 처리할 수 있다.

var (name, _, gender, hobby) = (name: "이동욱", birthday: "0525", gender: "Man", hobby: "game")

birthday는 무시되게 된다.

 

튜플을 반환하는 함수도 만들 수 있다.

func checkMember(for name: String) -> (name: String, age: Int)? {
  let memberList: [(name: String, price: Int)] = [
    ("이동욱", 20),
    ("홍길동", 22),
  ]
  for memberInfo in memberList {
    if memberInfo.name == name {
      return memberInfo
    }
  }
  return nil
}

coffeeInfo(for: "이동욱")?.age // 20
coffeeInfo(for: "김영수")?.price // nil

let (_, gildongAge) = checkMember(for: "홍길동")!
gildongAge // 22

 

Enum

enum은 Enumeration의 약어로 열거라는 뜻을 가지고 있다. java의 enum과 동일하다.

enum EnumSample: Int {
  case oneEnum = 1
  case twoEnum

  func simpleDescription() -> String {
    switch self {
    case .oneEnum:
      return "첫번째 이넘"
    case .twoEnum:
      return "두번째 이넘"
    }
  }
}

let one = EnumSample.oneEnum
print(one.simpleDescription()) // 첫번째 이넘
print(one.rawValue)            // 1

oneEnum에 할당한 값은 원시값(rawValue)를 의미한다. 원시값으로 호출할수 있으며 만약 없는 원시값을 호출할 경우(예를들어 3) 결과는 nil을 반환한다. nil을 반환한다는 사실을 통해 우리는 원시값을 통한 호출은 반환 타입이 옵셔널임을 유추할 수 있다.

EnumSample(rawValue: 3) // nil

let sample = EnumSample(rawvalue: 1)
print(sample)	// Optional(EnumSample.oneEnum)

 enum에서 반드시 rawValue가 필요한 것은 아니다. 필요없는 경우에는 없어도 된다. 또한, Enum을 예측할 수 있는 경우(타입 어노테이션을 통한 inference) Enum의 이름도 생략할 수 있다. 

let enumSample: EnumSample = .oneEnum // 변수에 타입 어노테이션이 있기 때문에 생략 가능

func doSomething(with enumSample: EnumSample) {
  // ...
}
doSomething(with: .twoEnum) // 함수 정의에 타입 어노테이션이 있기 때문에 생략 가능

추가로 Swift의 enum은 다른 언어들과 달리 원시값이 Int 뿐만아니라 String 타입이어도 가능하다.

 

연관 값(Associated Value)을 가지는 Enum

 Enum은 연관값(Associated Value)을 가질 수 있다. 다음 예시는 API에 대한 에러를 정의한 Enum이며 invalidParameter 케이스는 필드 이름과 메세지를 가지도록 정의되어 있다.

enum NetworkError {
  case invalidParameter(String, String)
  case timeout
}

let error: NetworkError = .invalidParameter("email", "이메일 형식이 올바르지 않습니다.")

이 값을 꺼내오기 위해서는 if-case 또는 switch 를 활용하는 방법이 있다.

if case .invalidParameter(let field, let message) = error {
  print(field) // email
  print(message) // 이메일 형식이 올바르지 않습니다.
}

switch error {
case .invalidParameter(let field, let message):
  print(field) // email
  print(message) // 이메일 형식이 올바르지 않습니다.

default:
  break
}
추가 정보! - 옵셔널은 Enum이다.
public enum Optional<Wrapped> {
  case none
  case some(Wrapped)
}

옵셔널은 Enum이므로 다음과 같은 Switch 구문을 사용할 수 있다.

let age: Int? = 20

switch age {
case .none: // `nil`인 경우
  print("나이 정보가 없습니다.")

case .some(let x) where x < 20:
  print("청소년")

case .some(let x) where x < 65:
  print("성인")

default:
  print("어르신")
}

 

프로토콜(Protocol)

 프로토콜은 자바의 인터페이스 혹은 추상 클래스 같은 역할이라고 이해하면 좋을 것 같다. 즉, 최소한으로 가져야 할 속성이나 메서드를 정의하며 자세한 구현은 하지 않는다.

 당연하게 프로토콜은 클래스와 구조체에 적용(ConForm)할 수 있으며 적용시키면 프로토콜에 상세된 속성과 메서드를 모두 구현해야한다.

/// 전송가능한 인터페이스를 정의합니다.
protocol Sendable {
  var from: String? { get }
  var to: String { get }

  func send()
}

 

Any와 AnyObject

 Any는 모든 타입에 대응한다. AnyObject는 모든 객체(Object)에 대응한다. 이 두가지는 프로토콜이며 Swift에서 사용 가능한 타입은 Any를 따르도록 설계되어있고, 모든 클래스들에는 AnyObject 프로토콜이 적용되어있다.(자바에서 모든 클래스는 Object의 자식인 것과 같은 개념같다.)

 

 따라서 다음과 같이 작성될 수 있다.

let anyNumber: Any = 2023
let anyString: Any = "스위프트"

let anyInstance: AnyObject = Info()

 

타입 캐스팅 (Type Casting)

 anyNumber에 2023이라는 Int값을 넣었다고 Int 타입이 되는 것이 아니라 그대로 Any 타입이다. 따라서 Int타입간 연산을 위해서는 다운캐스팅 과정이 필요하다. 다운 캐스팅이란 Any가 Int보다 더 큰 범위이기 때문에 Int 타입으로 내려주는(변환시켜주는) 것을 의미한다. 

 

 만약 2023(Any)와 1(Int)를 더하는 연산을 하고싶다면 Any 타입을 Int로 다운 캐스팅 해주어야 하기 때문이다. 다운 캐스팅을 위한 예약어는 `as`이다. 다만, Any가 포함하는 많은 타입 중 Int가 가능한 타입인지 아닌지 모르기 때문에 `as?`처럼 ?(물음표)를 붙여 옵셔널을 취해야한다.

let number: Int? = anyNumber as? Int

 

타입 검사

 타입 검사를 위한 예약어는 `is`이다. 다음과 같이 사용할 수 있다.

print(anyNumber is Int)    // true
print(anyNumber is Any)    // true
print(anyNumber is String) // false
print(anyString is String) // true

 

Swift 주요 프로토콜

 개발에 유용하게 사용되는 Swift에서 제공하는 기초적인 프로토콜들이 있다.

 

CustomStringConvertible

 자기 자신을 표현하는 문자열을 정의한다. print(), String(), \() 등에서 사용될 때의 값이다.

public protocol CustomStringConvertible {
  /// A textual representation of `self`.
  public var description: String { get }
}

  만약 이 프로토콜을 사용하고 싶다면 다음과 같이 적용(Conform)시키면 된다.

struct Dog: CustomStringConvertible {
  var name: String
  var description: String {
    return "🐶 \(self.name)"
  }
}

let dog = Dog(name: "찡코")
print(dog) // 🐶 찡코

 

ExpressibleBy

 우리가 Int, String 등을 사용할 때 생성자를 통해서 변수에 선언하지 않는다. 그 이유는 Swift의 컴파일러가 생성자를 사용하지 않더라도 알아서 선언해주기 때문이다. 이런 것들을 리터럴(Literal)이라고 부르는데 만약 우리가 만든 객체를 Literal로 만들고 싶다면 이 프로토콜을 활용하면 된다.

 

 예를들어 Int는 ExpressibleByIntegerLiteral 이라는 프로토콜을 사용하며, String은 ExpressibleByStringLiteral 이라는 프로토콜을 적용하여 사용한다.

 

 만약 우리의 객체에 이 리터럴을 적용한다면 다음과 같이 작성될 수 있다.

struct DollarConverter: ExpressibleByIntegerLiteral {
  typealias IntegerLiteralType = Int

  let price = 1_177
  var dollars: Int

  init(integerLiteral value: IntegerLiteralType) {
    self.dollars = value * self.price
  }
}

let converter: DollarConverter = 100
converter.dollars // 117700
위 코드에서 1_177은 1177과 동일하다. 단순히 가독성을 위해 _를 삽입하여 숫자의 가독성을 높여준다.

 

익스텐션(Extension)

 Swift에서는 이미 정의된 타입에 임의적으로 새로운 속성이나 메서드를 추가할 수 있다. extension이라는 키워드를 사용하면 기존 클래스와 구조체를 확장해서 사용할 수 있다.

extension String {
  var length: Int {
    return self.characters.count
  }

  func reversed() -> String {
    return self.characters.reversed().map { String($0) }.joined(separator: "")
  }
}

let str = "안녕하세요"
str.length // 5
str.reversed() // 요세하녕안

 

'App' 카테고리의 다른 글

ARC  (0) 2023.07.11