일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- viewcontroller
- GitLab
- vscode
- currying
- REACT
- nextJS
- npm install
- npm
- rn
- github
- React Native
- HTML
- js
- commit
- ReactNative
- ES6
- styling
- Xcode
- JavaScript
- Docker
- git
- shortcut
- styled-components
- react-native
- ios
- xtring.log
- Android
- Swift
- DevOps
- Branch
- Today
- Total
xtring.dev
[Swift] Swift 기본 문법 정리(2) 👽 - 함수와 클로저, 클래스와 구조체 본문
Swift 기본 문법 정리(2)
전수열님의 GitBook을 기반으로 정리합니다.
함수
Swift에서 함수는 func
키워드를 사용해서 정의합니다. 그리고 ->
를 사용해서 함수의 반환(return) 타입을 지정합니다.
func hello(name: String, time: Int) -> String { // hello 함수는 name(string), time(int) parameter들을 받으며 String 타입을 반환합니다.
var string = ""
for _ in 0..<time {
string += "\(name)님 안녕하세요!\n"
}
return string
}
Swift에서는 함수를 호출할 때 파라미터 이름을 함께 써주어야 합니다.
hello(name: "Mark", time: 3) // 함수 호출 시 파라미터에 각 파라미터 명을 써주어야 함
함수 호출 시 사용하는 파라미터 이름과 함수 내부에서 사용하는 파라미터 이름을 다르게 사용할 수 있습니다.
func hello(to name: String, numberOfTimes time: Int) {
// 함수 내부에서는 `name`, `time`을 사용합니다.
for _ in 0..<time {
print(name)
}
}
hello(to: "Mark", numberOfTimes: 3) // 함수를 호출 할 때는 `to`, `numberOfTimes`를 사용합니다.
파라미터 이름을 _
로 정의하면 함수를 호출할 때 파라미터 이름을 생략할 수 있습니다.
func hello(_ name: String, time: Int) {
// ...
}
hello("Mark", time: 3) // 'name: '이 생략됨
파라미터에 기본값을 지정할 수도 있습니다. 기본값이 지정되어 있다면 함수 호출 시 생략이 가능합니다.
func hello(name: String, time: Int = 1) {
// ...
}
hello("Mark") // time은 기본값이 지정되어 있으므로 생략이 가능
...
을 사용하면 개수가 정해지지 않은 파라미터(Variadic Parameters)를 받을 수 있습니다.
func sum(_ numbers: Int...) -> {
var sum = 0
for number in numbers {
sum += number
}
return sum
}
sum(1, 2)
sum(3, 4, 5)
// * number parameter를 정하지 않고 받을 수 있음
함수 안에 함수를 다시 작성하는 것이 가능합니다.
func hello(name: String, time: Int) {
func message(name: String) {
return "\(name)님 안녕하세요!"
}
for _ in 0..<time {
print(message(name: name))
}
}
함수 안에 정의한 함수를 반환 할 수 있습니다.
func helloGenerator(message: String) -> (String) -> String {
func hello(name: String) -> String {
return name + message
}
return hello
}
let hello = helloGenerator(message: "님 안녕하세요!")
hello("Mark") // Mark님 안녕하세요!
여기서 핵심은 helloGenerator()
함수의 반환 타입이 (String) -> String
라는 것입니다. 즉, helloGenerator()
는 '문자열을 받아서 문자열을 반환하는 함수'를 반환하는 함수라는 것입니다.helloGenerator()
안에 정의한 hello()
함수가 여러개의 파라미터를 받는다면 아래와 같이 사용합니다.
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("Mark", "Hwang")
클로저(Closure)
클로저(Closure)를 사용하면 바로 위에 작성한 코드를 조금 더 간결하게 만들 수 있습니다. 클로저는 중괄호({}
)로 감싸진 '실행 가능한 코드 블럭'입니다.
func helloGenerator(message: String) -> (String, String) -> String {
return { (firstName: String, lastName: String) -> String in
return lastName + firstName + message
}
}
일반적인 함수와 다르게 함수 이름을 정의하지 않습니다. 하지만 파라미터를 받을 수 있고 반환 값이 존재할 수 있습니다. 이렇게 생긴 형태를 클로저(Closure)라고 합니다.
클로저는 중괄호({}
)로 감싸져있습니다. 그리고 파라미터를 괄호로 감싸서 정의합니다. 함수와 마찬가지로 ->
를 사용해서 반환 타입을 명시합니다. 조금 다른 점은 in
키워드를 사용해서 파라미터, 반환 타입 영역과 실제 클로저의 코드를 분리하고 있습니다.
{ (firstName: String, lastName: String) -> in
return lastName + firstName + message
}
클로저의 장점은 간결함과 유연함에 있습니다.
Swift 컴파일러의 타입 추론 덕분에, helloGenerator()
함수에서 반환하는 타입을 가지고 클로저에서 어떤 파라미터를 받고 어떤 타입을 반환하는지를 알 수 있습니다. >> 파라미터 타입을 생략 가능
func helloGenerator(message: String) -> (String, String) -> String {
return { firstName, lastName in
return lastName + firstName + message
}
}
파라미터에 임의의 변수를 통해 접근이 가능합니다.
func helloGenerator(message: String) -> (String, String) -> String {
return {
return $1 + $0 + message
}
}
클로저 내부의 코드가 한 줄이라면, return
까지도 생략할 수 있습니다.
func helloGenerator(message: String) -> (String, String) -> String {
return { $1 + $0 + message }
}
코드를 최대한으로 생략하면 아래의 코드가 나옵니다.
let hello: (String, String) -> String = { $1 + $0 + "님 안녕하세요!" }
hello("Mark", "Hwang")
여기에는 옵셔널과 옵셔널 체이닝도 사용가능합니다.
let hello: ((String, String) -> String)?
hello?("Mark", "Hwang")
클로저를 변수로 정의하고 함수에서 반환할 수도 있는 것처럼, 파라미터로도 받을 수 있습니다.
func manipulate(number: Int, using block: Int -> Int) -> Int {
return block(number)
}
manipulate(number: 10, using: { (number: Int) -> Int in
return number * 2
})
이것도 생략이 가능합니다.
manipulate(number: 10, using: {
$0 * 2
})
만약 함수의 마지막 파라미터가 클로저라면, 괄호와 파라미터 이름마저 생략할 수 있습니다.
manipulate(number: 10) {
$0 * 2
}
이런 구조로 만들어진 예시가 sort()
와 filter()
입니다. 함수가 클로저 하나만을 파라미터로 받는다면, 괄호를 아예 쓰지 않아도 됩니다.
let numbers = [1, 3, 2, 6, 7, 5, 8, 4]
let sortedNumbers = numbers.sort { $0 < $1 }
print(sortedNembers) // [1, 2, 3, 4, 5, 6, 7, 8]
let evens = numbers.filter { $0 % 2 == 0 }
print(evens) // [2, 6, 8, 4]
클로저 활용하기
클로저는 Swift의 많은 곳에서 사용됩니다. sort()
, filter()
와 같은 배열에 많이 사용됩니다. 대표적인 메서드로는 sort()
, filter()
, map()
, reduce()
가 있습니다.
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
Swift에서는 연산자도 함수입니다. 따라서 이와 같은 문법도 가능합니다.
arr1.reduce(0, +) // 28
클래스와 구조체
클래스(Class)는 class
로 정의하고, 구조체(Structure)는 struct
로 정의합니다.
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(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"
}
}
만약 속성(name, age, ...)이 옵셔널이 아니라면 항상 초깃값을 가져야 합니다. 만약 옵셔널이 아닌 속성이 초깃값을 가지고 있지 않으면 컴파일 에러가 발생합니다.
class Dog {
var name: String?
var age: Int // 초기값이 없기 때문에 컴파일 에러!
}
stored property 'age' without initial value prevents synthesized initializers
속성을 정의할 때 (1)초깃값을 지정해 주는 방법과, (2)생성자에게 초깃값을 지정해주는 방법이 있습니다.
(1) 초깃값을 지정하는 방법
class Dog {
var name: String?
var age: Int = 0 // 속성을 정의할 때 초깃값을 지정하는 방법
}
(2) 생성자에게 초깃값을 지정해주는 방법
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
키워드를 사용할 수 있습니다.
class Dog: Animal {
var name: String?
var age: Int
override init() {
self.age = 0 // 초깃값 설정
super.init() // 상위 클래스 생성자 호출
print(self.simpleDescription()) // 여기서부터 `self` 접근 가능
}
func simpleDescription() -> String {
if let name = self.name {
return "\(name)"
} else {
return "No name"
}
}
}
만약, 위 예시 코드를 아래처럼 바꿔서 super.init()
을 하기 전에 self
에 접근한다면 컴파일 에러가 발생합니다.
override init() {
self.age = 0
print(self.simpleDescription()) // 컴파일 에러
super.init()
}
error: use of 'self' in method call 'simpleDescription' before super.init initializes self
deinit
은 메모리에서 해제된 직후에 호출됩니다.
class Dog {
// ...
deinit {
print("메모리에서 해제됨")
}
}
속성
속성은 크게 '값을 가지는 속성(Stored Property)'과 '계산되는 속성(Computed Property)'로 구분됩니다. 쉽게 말하자면 속성이 값 자체를 가지고 있는지, 혹은 어떠한 연산을 수행한 뒤 그 결과를 반환하는지의 차이입니다.
우리가 지금까지 정의하고 사용한 name
, age
와 같은 속성들은 모두 Stored Property입니다. Computed Property는 get
, set
을 사용해서 정의할 수 있습니다. set
에서는 새로 설정될 값을 newValue
라는 예약어를 통해 접근할 수 있습니다.
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
은 실제 값을 가지고 있지 않지만, decimal
로부터 값을 받아와 16진수 문자열로 만들어서 반환합니다. decimal
은 Stored Property, hexString
은 Computed Property입니다.
참고로, get
만 정의할 경우에는 get
키워드를 생략할 수 있습니다. 이러한 속성을 읽기 전용(Read Only)이라고 합니다.
class Hex {
// ...
var hexCode: String? {
if let hex = self.hexString {
return "0x" + hex
}
return nil
}
}
get
, set
과 비슷한 willSet
, didSet
을 이용하면 속성에 값이 지정되기 직전과 원하는 코드를 실행할 수 있습니다.
struct Hex {
var decimal: Int? {
willSet {
print("\(self.decimal)에서 \(newValue)로 바뀔 예정입니다.")
}
didSet {
print("\(oldValue)에서 \(self.decimal)로 값이 바뀌었습니다.")
}
}
}
마찬가지로, willSet
에서 새로운 값을 newValue
로 얻어올 수 있고, didSet
에서는 예전 값을 oldValue
라는 예약어를 통해 얻어올 수 있습니다.willSet
과 didSet
은 일반적으로 어떤 속성의 값이 바뀌었을 때 UI를 업데이트하거나 특정 메서드를 호출하는 등의 역할을 할때 사용됩니다.
'Mobile > iOS' 카테고리의 다른 글
[iOS/Xcode] Xcode Pro 처럼 빌드환경 세팅하기 🖥 (0) | 2021.07.08 |
---|---|
[Swift] Swift 기본 문법 정리(1) 🧾 - Swift의 변수와 상수, 조건문과 반복문, 옵셔널(Optional) (0) | 2021.03.22 |
[iOS] ViewController의 Life Cycle에 대해서 분석해봅시다 🥸 - ViewController Life Cycle (0) | 2021.03.19 |
[iOS] 앱 출시 전엔 꼭 테스트 해야합니다! - TestFlight 사용하여 앱 테스트하기 🚀 (0) | 2021.03.09 |
[iOS] ViewController에 대해서 알아봅시다 ⚙️ (0) | 2021.03.05 |