[iOS] TIL

Zoe·2023년 11월 1일
0

iOS

목록 보기
26/39

✅ defer

defer란 현재 코드 블록을 나가기 전에 꼭 실행해야 되는 코드를 작성하여 코드가 블록을 어떻게 빠져 나가든 꼭 마무리해야 되는 작업을 할 수 있게 도와준다

  • defer는 역순으로 실행된다.
  • defer가 선언된 코드 블록을 빠져나가기 직전에 실행된다.
func deferTest() {
    defer {
        print("call defer")
    }

    print("hello")
}

hello
call defer

🌟 defer는 함수의 가장 마지막에 실행이 된다는 것을 보장하는 것이다.

✅ defer가 호출되는 순서, 호출되지 않는 경우

1️⃣ 호출 순서

defer를 사용하게 되면 역순으로 호출하게 된다. 아래는 그 예시이다.

func deferTest2() {
  defer {
      print("1")
  }

  defer {
      print("2")
  }

  defer {
      print("3")
  }

  defer {
      print("4")
  }

  print("5")
}

5
4
3
2
1

출력된 결과를 보게 되면 선언된 순서의 역순으로 호출되고 있는 것을 확인할 수 있다.
그리고 항상 그 코드 블럭 안에서 제일 마지막으로 호출된다는 것을 기억해야 한다.

2️⃣ 호출되지 않는 경우

defer는 코드 블록을 빠져 나가기 직전에 호출되지만, 무조건 defer를 선언한다고 100% 호출을 보장하지는 않는다. 아래 세 가지의 경우를 유의해서 사용하자

🌟 throw를 이용해서 오류를 던질 경우

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 문을 사용하여 중간에 함수를 종료하는 경우

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가 호출되지 않는다.

🌟 리턴값이 Never인 경우

func test() -> Never {
    defer {
        print("1")
    }

    defer {
        print("2")
    }

    defer {
        print("3")
    }

    abort()
}

에러가 발생하면서 함수를 반환하지 않고 실행을 종료하기 때문에 defer가 호출되지 않는다.

✅ property wrapper

@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"

이렇게 여러 프로퍼티들에 동일한 관리 코드를 작성해줘야하는 경우, 프로퍼티 래퍼를 유용하게 사용할 수 있다. 프로퍼티 래퍼를 정의해서 관리 코드를 한번만 작성해준 후, 여러 프로퍼티들에 프로퍼티 래퍼를 적용하여 코드를 재사용할 수 있기 때문이다.

✅ Generic

제네릭이란 타입에 의존하지 않는 범용 코드를 작성할 때 사용한다. 제네릭을 사용하면 중복을 피하고, 코드를 유연하게 작성할 수 있다. (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()

✅ some 키워드에 대해 설명하시오.

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 등등 구체적인 뷰값을 명시해줘야했을 것이다.

✅ Result타입에 대해 설명하시오.

@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 의 경우에 따라 처리를 해주면 된다.

✅ Codable

대부분의 앱 서비스는 네트워크 통신을 사용해 원격 서버에서 데이터를 가져와 사용하며, 이 데이터는 일반적으로 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"
  }
}

✅ Closure

클로저와 함수는 기능은 완전히 동일한데, 형태만 다르다고 생각하면 된다.

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)

✅ Closure와 함수

✅ Optional Chaining, nil-coalescing operator

옵셔널 체이닝이란 표현식 자체가 옵셔널의 가능성이 있다는 것을 표현하고, 체이닝의 결과는 항상 옵셔널이다.(옵셔널 타입에 대해 접근연산자 사용은 항상 ?를 붙임)

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

  • info? : 함수가 없을 수 있다.
  • info?()? : 함수의 결과값이 없을 수 있다.

nil coalescing은 (삼항 연산자와 유사한 방식으로) 읽기 쉬운 방법으로 옵셔널 타입을 벗기는 방법이다.

optionalName ?? "default name”

만약 optionalName이 nil값을 가지고 있다면 default name으로 값을 추출하는 것이다.

profile
iOS 개발자😺

0개의 댓글