defer란 현재 코드 블록을 나가기 전에 꼭 실행해야 되는 코드를 작성하여 코드가 블록을 어떻게 빠져 나가든 꼭 마무리해야 되는 작업을 할 수 있게 도와준다
- defer는 역순으로 실행된다.
- defer가 선언된 코드 블록을 빠져나가기 직전에 실행된다.
func deferTest() {
defer {
print("call defer")
}
print("hello")
}
hello
call defer
🌟 defer는 함수의 가장 마지막에 실행이 된다는 것을 보장하는 것이다.
defer를 사용하게 되면 역순으로 호출하게 된다. 아래는 그 예시이다.
func deferTest2() {
defer {
print("1")
}
defer {
print("2")
}
defer {
print("3")
}
defer {
print("4")
}
print("5")
}
5
4
3
2
1
출력된 결과를 보게 되면 선언된 순서의 역순으로 호출되고 있는 것을 확인할 수 있다.
그리고 항상 그 코드 블럭 안에서 제일 마지막으로 호출된다는 것을 기억해야 한다.
defer는 코드 블록을 빠져 나가기 직전에 호출되지만, 무조건 defer를 선언한다고 100% 호출을 보장하지는 않는다. 아래 세 가지의 경우를 유의해서 사용하자
func deferWithError(receiveError: Bool) throws -> Void {
defer {
print("1")
}
if receiveError {
enum TestError: Error {
case error
}
throw TestError.error
}
defer {
print("2")
}
print("3")
}
print("when we have error")
try? deferWithError(receiveError: true)
print("when we don't have error")
try? deferWithError(receiveError: false)
when we have error
1
when we don't have error
3
2
1
중간에 throw가 발생해서 함수가 종료될 경우 아래 선언된 defer에 도달하지 못해 defer가 호출되지 않는다.
guard 문을 사용해서 중간에 함수를 종료하는 경우에도 defer가 호출되지 않는다.
func deferWithGuard(request: request?){
defer {
print("1")
}
guard let r = request else {
return
}
defer {
print("2")
}
print("3")
}
print("when request is nil")
deferWithGuard(nil)
print("when request isn't nil)
deferWithGuard(someRequest)
when request is nil
1
when request isn't nil
3
2
1
guard문 이후에 나오는 defer에 도달하기 전에 함수가 종료되어 defer가 호출되지 않는다.
func test() -> Never {
defer {
print("1")
}
defer {
print("2")
}
defer {
print("3")
}
abort()
}
에러가 발생하면서 함수를 반환하지 않고 실행을 종료하기 때문에 defer가 호출되지 않는다.
@State, @Binding, @StateObject, @ObservedObject, @Published 등 SwiftUI에서 사용하고 있는 property wrapper를 확인할 수 있는데, 이를 직접 만들어서 사용할 수도 있다.
Property Wrapper를 정의하려면 wrappedValue property를 정의한 struct, enum, class를 만들면 된다.
@propertyWrapper
struct TwelveOrLess {
private var number = 0
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
}
}
위 코드를 통해 @TwelveOrLess로 정의된 변수는 항상 12 이하의 number로 설정되는 걸 보장할 수 있다.
struct SmallRectangle {
@TwelveOrLess var height: Int
@TwelveOrLess var width: Int
}
var rectangle = SmallRectangle()
print(rectangle.height)
// Prints "0"
rectangle.height = 10
print(rectangle.height)
// Prints "10"
rectangle.height = 24
print(rectangle.height)
// Prints "12"
이렇게 여러 프로퍼티들에 동일한 관리 코드를 작성해줘야하는 경우, 프로퍼티 래퍼를 유용하게 사용할 수 있다. 프로퍼티 래퍼를 정의해서 관리 코드를 한번만 작성해준 후, 여러 프로퍼티들에 프로퍼티 래퍼를 적용하여 코드를 재사용할 수 있기 때문이다.
제네릭이란 타입에 의존하지 않는 범용 코드를 작성할 때 사용한다. 제네릭을 사용하면 중복을 피하고, 코드를 유연하게 작성할 수 있다. (Array와 Dictoinary 또한 제네릭 타입)
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let tempA = a
a = b
b = tempA
}
func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
let tempA = a
a = b
b = tempA
}
func swapTwoStrings(_ a: inout String, _ b: inout String) {
let tempA = a
a = b
b = tempA
}
//🌟🌟🌟
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let tempA = a
a = b
b = tempA
}
여러 타입에 대한 함수를 하나하나 만들지 않고 모든 타입을 위해 사용하는 것이 바로 제네릭이다. 타입에 제한을 두지 않는 코드를 사용하고 싶을 때 쓰는 것이다.
T를 Type Parameter라고 부르는데, T라는 새로운 형식이 생성되는 것이 아니라, 실제 함수가 호출될 때 해당 매개변수의 타입으로 대체되는 Placeholder 이다.
var someInt = 1
var aotherInt = 2
swapTwoValues(&someInt, &aotherInt)// 함수 호출 시 T는 Int 타입으로 결정된다.
var someString = "Hi"
var aotherString = "Bye"
swapTwoValues(&someString, &aotherString)// 함수 호출 시 T는 String 타입으로 결정된다.
이렇게 실제 함수를 호출할 때, Type Parameter인 T의 타입이 결정된다.
func swapTwoValues<One, Two> { ... }
타입 파라미터는 T가 아니라 원하는 이름으로 만들 수 있고, 1개 말고 여러 개를 comma(,)를 이용해서 선언할 수 있다. 위는 제네릭 함수(Generic Function)이다.
구조체, 클래스, 열거형 타입에도 선언할 수 있는데, 이것을 "제네릭 타입(Generic Type)" 이라고 한다. 아래는 예시이다.
struct Stack<T> {
let items: [T] = []
mutating func push(_ item: T) { ... }
mutating func pop() -> T { ... }
}
let stack1: Stack<Int> = .init()
let stack2 = Stack<Int>.init()
struct ContentView: View {
var body: some View
}
some 키워드는 computed property인 body안에 불투명한 타입이 있음을 나타낸다.
func makeShape() -> Shape {
return Circle()
}
Swift는 불투명한 타입을 리턴하는 것을 허용하지 않는다.
위 코드는 "Protocol 'Shape' can only be used as a generic constraint because it has Self or associated type requirements" 에러를 발생시킨다.
func makeShape() -> some Shape {
return Circle()
}
some을 사용해주면 에러를 해결할 수 있다. 타입을 미리 지정해서 함수의 내부를 변화시키는 제네릭 타입과는 정 반대로 함수 내부의 코드에 따라 구체적인 리턴 타입이 달라지게 된다.
만약 body에서 some을 안 쓴다면 매번 함수 리턴값을 VStack, HStack, Button, Text 등등 구체적인 뷰값을 명시해줘야했을 것이다.
@frozen enum Result<Success, Failure> where Failure : Error
기존의 에러처리 방식을 개선하고 결과값을 명확히 받기 위해 Result Type이 나왔다.
(Swift5의 Result Type을 사용하면 비동기 API 호출 코드를 간단하고 명확하게 만들 수 있다.)
// 기존의 에러 처리 방법
enum McDonaldOrderError: Error {
case invalidSelection
case LackOfMoney
case outOfStock
}
struct Hamburger {
var name: String
var price: Int
var count: Int
}
let bigMac = Hamburger(name: "BigMac", price: 4600, count: 3)
let myMoney = 4000
func OrderMcDonaldMenu(orderedMeun: Hamburger) throws {
if orderedMeun.name != "BigMac" {
throw McDonaldOrderError.invalidSelection
}
if orderedMeun.price > myMoney {
throw McDonaldOrderError.LackOfMoney
}
if orderedMeun.count == 0 {
throw McDonaldOrderError.outOfStock
}
}
do {
try OrderMcDonaldMenu(orderedMeun: bigMac)
} catch McDonaldOrderError.invalidSelection {
print("저희 매장에 주문한 메뉴가 없습니다. 메뉴이름을 다시 확인해주세요.")
} catch McDonaldOrderError.LackOfMoney {
print("메뉴를 주문하기에 고객님의 잔액이 부족합니다.")
} catch McDonaldOrderError.outOfStock {
print("현재 재고가 없어 주문이 불가능합니다.")
}
위 코드를 보면서 에러 처리시 보이는 문제점들이 몇 가지 있다.
- 에러를 받는 입장에서는 어떤 에러인지 확인 후, 타입 캐스팅을 통해 정의한 에러를 사용할 수 있다.
- 정의한 에러 타입에서 새로운 타입이 추가되어도 컴파일러는 알 수 없어 런타임 시(앱 실행중에) 해당 에러를 처리 안했을 때 생기는 문제점들이 있을 수 있다.
// Result Type을 적용한 에러 처리
enum McDonaldOrderError: Error {
case invalidSelection
case LackOfMoney
case outOfStock
}
struct Hamburger {
var name: String
var price: Int
var count: Int
}
let bigMac = Hamburger(name: "BigMac", price: 4600, count: 3)
let myMoney = 4000
func orderMcDonaldMenu(orderedMenu: Hamburger) -> Result<Bool, McDonaldOrderError> {
if orderedMenu.name != "BigMac" {
return .failure(.invalidSelection)
}
if orderedMenu.price > myMoney {
return .failure(.LackOfMoney)
}
if orderedMenu.count == 0 {
return .failure(.outOfStock)
}
return .success(true)
}
let isOrderable = orderMcDonaldMenu(orderedMenu: bigMac)
switch isOrderable {
case .success(let data):
print(data)
case .failure(let error):
print(error)
}
Result Type은 Result<Success, Failure> 으로 이루어졌기 때문에 switch문을 통해 success, failure 의 경우에 따라 처리를 해주면 된다.
대부분의 앱 서비스는 네트워크 통신을 사용해 원격 서버에서 데이터를 가져와 사용하며, 이 데이터는 일반적으로 JSON 형식이다.
Swift는 강력한 타입 시스템이기 때문에 기존에는 JSON 형식의 데이터를 다루기가 쉽지 않았다. Swift4부터는 Codable 프로토콜을 이용해 단 한줄로 간편하게 JSON 데이터 파싱을 할 수 있게 되었다.
Codable은 프로토콜 이기 때문에 채택하여 사용하는데, class, struct, enum에서 채택 가능하다.
//Encoding
struct User: Codable {
let email: String
let password: String
let name: Bool
}
let request = User(email: "zoe@naver.com", password: "password", name : "zoe")
do {
let encoder = JSONEncoder()
let data = try encoder.encode(request) //인스턴스를 JSONEncoder를 이용하여 Data로 인코딩
print(data) // bytes
if let jsonString = String(data: data, encoding: .utf8) {
print(jsonString) // {"email": "zoe@naver.com", "password": "password", "name" : "zoe"}
}
} catch {
print(error)
}
디코딩하는 방법은 아래와 같다.
let jsonData = """
{
"email": "zoe@naver.com",
"password": "password",
"name" : "zoe"
}
""".data(using: .utf8)!
do {
let decoder = JSONDecoder()
let data = try decoder.decode(User.self, from: jsonData)
print(data) //User(email: "zoe@naver.com", password: "password", name : "zoe")
print(data.name) // zoe
} catch {
print(error)
}
api 데이터가 스네이크 케이스(snake_case)를 사용하는 경우 Swift 이름 정의 규칙에 부합하지 않는다. 이와 같이 key를 커스텀하고 싶을 때 CodingKeys를 이용한다.
struct User: Codable {
let email: String
let password: String
let name: Bool
enum CodingKeys: String, CodingKey {
case email = "user_email"
case password = "user_password"
case name = "user_name"
}
}
클로저와 함수는 기능은 완전히 동일한데, 형태만 다르다고 생각하면 된다.
func test( ) -> Int {
return ...
}
{ () -> Int in
return ...
}
함수는 타입이다!
함수는...
변수에 할당할 수 있음
파라미터로 전달할 수 있음
return 값이 될 수 있음
func add(a: Int, b: Int) -> Int {
let result = a + b
return result
}
{ a,b in
let result = a + b
return result
}
클로저는 참조 형식이다. 필요 시에 항상 메모리의 주소를 전달하고, 값의 저장은 Heap (주소를 Stack에 저장)
클로저는 왜 참조형식으로 저장될까?
var stored = 0
let closure = { (number: Int) -> Int in
stored += number // 클로저 외부에 존재하는 stored 변수를 계속 사용할 수 있어야 한다. 그래서 stored를 캡처함
return stored
}
closure(0)
closure(1)
옵셔널 체이닝이란 표현식 자체가 옵셔널의 가능성이 있다는 것을 표현하고, 체이닝의 결과는 항상 옵셔널이다.(옵셔널 타입에 대해 접근연산자 사용은 항상 ?를 붙임)
class User {
var name: String?
var weight: Int?
func run( ) {
print(“달린다.”)
}
}
var user: User? = User() 1) 옵셔널 Dog타입으로 선언
user?.name = “zoe”
print(user?.name) // zoe
user?.run() //달린다.
옵셔널 타입으로 선언된 값에 접근해서, 속성, 메서드를 사용할때 접근연산자(.)앞에 ?(물음표) 붙여야한다. 이는 앞의 값이 옵셔널의 가능성을 내포한다는 의미이다.
결과는 항상 옵셔널타입으로 리턴
옵셔널 체이닝 과정에서 그 값 중 하나라도 nil을 리턴한다면, 이어지는 표현식을 평가하지 않고 nil 리턴
user?.info?()?.name
nil coalescing은 (삼항 연산자와 유사한 방식으로) 읽기 쉬운 방법으로 옵셔널 타입을 벗기는 방법이다.
optionalName ?? "default name”
만약 optionalName이 nil값을 가지고 있다면 default name으로 값을 추출하는 것이다.