iOS 2주차 백업

은지·2024년 10월 20일

3주차 수업 사전 학습

클로저
Name Closure, UnNamed Closure
이름이 있는 함수는 Named Closure, 이름을 붙이지 않고 사용하는 함수는 Unnamed Closure.
클로저는 둘 다 포함하지만, 보통 Unnamed Closure를 말함.
클로저 또한 자료형을 가지고 있음.

클로저 표현식
클로저 헤드와 클로저 바디를 구분지어주는 in

Parameter와 Return Type이 둘 다 없는 클로저
let noParameterNoReturnTypeclosure = { () -> () in
print("No parameter, No return Type closure")}

클로저는 1급 객체이기 때문에, 상수에 클로저 대입 가능.

Return type이 없으면 생략해도 되고, 없어도 생략할 수 있음.
Parameter도 생략할 수 있음.
-> 클로저 경량 문법

Parameter와 Return Type이 있는 클로저

여기서 쓰이는 name 변수는 오직 Parameter Name임.
클로저는 Argument Label을 사용하지 않음.

클로저의 사용

주로 함수의 전달 인자로 많이 사용됨.
따로 클로저를 상수나 변수에 넣어 전달하지 않고, 함수를 호출할 때 클로저를 작성하여 전달할 수 있음.

후행 클로저(trailing closure)
함수의 매개변수 마지막으로 전달되는 클로저
함수 밖에서 구현할 수 있음

따라서, 클로저가 함수의 마지막 전달인자라면 마지막 매개변수 이름을 생략한 후 함수 소괄호 외부에 클로저를 구현할 수 있음.

반환타입 생략
컴퓨터가 반환 타입을 유추할 수 있다면 클로저에서 반환 타입을 명시적으로 생략해도 됨.
대신 in 키워드는 생략할 수 없음.

단축 인자 이름
클로저의 매개변수 이름이 굳이 불필요하면 단축 인자 이름 활용 가능.
단축 인자 이름은 클로저의 매개변수 순서대로 $0, $1, $2 … 처럼 표현됨.
func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
return method(a, b)
}
var result: Int
result = calculate(a: 10, b: 10) { // 후행 클로저와 사용하는 예시
return $0 + $1
}
print(result)

암시적 반환 표현
클로저가 반환값이 있으면 클로저의 마지막 줄의 결과값은 암시적으로 반환값 취급함.
result = calculate(a: 10, b: 10) { $0 + $1 }

한 줄로 축약한 예시.
$0 + $1가 리턴값.

캡처리스트
주변 환경의 범위에서 참조한 변수들을 얼마나 강하게 캡쳐해야 하는지 명시하는 것.
캡쳐 리스트를 사용하여 메모리 누수를 일으키는 강한 참조 순환을 피할 수 있음.

형식
참조 방식과 참조할 대상을 [대괄호]로 둘러싼 목록 형식으로 작성 -> 캡처리스트 뒤에 in 키워드
캡처리스트에 명시한 요소가 참조 타입이 아니라면 해당 요소들은 클로저가 생성될 때 초기화됨.

let closure { [num1, num2] in
클로저의 시작인 { 의 바로 옆에 []를 이용해 캡쳐할 멤버를 나열함. in 키워드는 필수!

가벼운 예시
func doSomething() {
var message = "Hi i am eunji!"

//클로저 범위 시작
var num = 10
let closure = { print(num) }
//클로저 범위 끝

print(message)

}

closure란 익명함수는 클로저 내부에서 외부 변수인 num을 사용(print)하기 때문에 num의 값을 클로저 내부적으로 저장하고 있음.
이를 클로저에 의해 num의 값이 캡쳐되었다고 표현함.

message 변수는 클로저 외부에서만 사용하기 때문에 클로저에 의해 값이 캡쳐되지 않음.

언제 사용할까요??
값 타입 : 클로저 내부에서 클로저 외부의 값을 참조할 때, 참조하는 값이 변경되면 클로저 내부에서도 참조하는 값 또한 바뀌게 되므로 이를 방지하고자 사용
참조 타입 : 클로저의 강한 참조 순환 문제를 해결

값 캡쳐 방식
closure는 값을 캡쳐할 때 Value/Reference 타입에 관계 없이 Reference Capture한다.

‘가벼운 예시'에서 사용했던 코드를 예시로 들었을 때
num 변수를 클로저 내부적으로 저장하지만 num은 Int 타입의 구조체 형식이고 Value 타입이기 때문에 값을 복사해서 저장해야 하는 것이 일반적임.

클로저는 Value/Reference 타입에 관계 없이 캡쳐하는 값들을 참조함
Reference Capture

클로저 외부에서 num 값을 변경하는 경우
func doSomething() {
var num: Int = 0
print("num check #1 = (num)") // num = 0

let closure = {
    print("num check #3 = \(num)")
}

num = 20
print("num check #2 = \(num)")      // num = 20
closure()                           // num = 20

}

클로저는 num 외부 변수를 클로저 내부에서 사용하기 때문에 num을 캡쳐할 것
Reference Capture, num을 참조함

클로저를 실행하기 전에 num이란 값을 외부에서 변경하면 클로저 내부에서 사용하는 num 값 또한 변경됨.

클로저 내부에서 num 값을 변경하는 경우
func doSomething() {
var num: Int = 0
print("num check #1 = (num)") // num = 0

let closure = {
    num = 20
    print("num check #3 = \(num)")
}

closure()                          // num = 20
print("num check #2 = \(num)")     // num = 20

}
doSomething()

클로저 내부에서 사용하는 num의 값을 바꾸면 클로저 외부에 있는 num의 값도 변경됨.

값 캡쳐 방식 결론!
클로저는 값의 타입이 Value이거나 Reference여도 모두 Reference Capture를 함.

Value Type의 값을 복사해서 캡처하기
Value Type의 경우, Value Capture 하고 싶은 변수를 리스트로 명시해주는 것

func doSomething() {
var num: Int = 0
print("num check #1 = (num)") // num = 0

let closure = { [num] in
    print("num check #3 = \(num)")
}

num = 20
print("num check #2 = \(num)")      // num = 20
closure()                           // num = 0

}

외부 변수 num의 값을 변경해도 클로저의 num에는 영향을 주지 않음.

클로저를 선언할 당시 num의 값을 Const Value Type으로 캡쳐함.
Const Value Type, 즉 상수로 캡쳐된다는 것.

위처럼 클로저 내부에서 Value Capture된 값을 변경할 수 없음.

정리
클로저는 기본적으로 Value Type의 값도 Reference Capture를 함.
하지만 클로저 캡처 리스트를 이용하면 Const Value Type으로 캡쳐가가능함.

Reference Type의 값을 복사해서 캡처하기
캡처리스트를 작성한다고 해도 Reference Type은 Reference Capture를 함.
class Human {
var name: String = "Eunji"
}

var human1: Human = .init()

let closure = { [human1] in
print(human1.name)
}

human1.name = "Unknown"
closure() // 결과 값은 Unknown

클로저와 ARC
ARC : 인스턴스의 Reference Count를 자동으로 계산하여 메모리를 관리하는 방법

클로저의 강한 순환 참조
강한 참조 순환이 발생하는 이유는 클로저가 클래스와 같은 참조 타입이기 때문임.
클로저는 힙 영역에서 살고 있음.

class Human {
var name = ""
lazy var getName: () -> String = {
return self.name
}

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

deinit {
    print("Human Deinit!")
}

}

Human 클래스 생성
name을 얻을 수 있는 lazy 프로퍼티를 클로저를 통해 초기화
lazy 키워드를 붙여서 프로퍼티를 선언하면 다른 프로퍼티보다 지연이 됨.
즉, 해당 프로퍼티가 처음 사용되기 전까지는 메모리에 올라가지 않음.
lazy는 반드시 var 키워드를 사용해서 변수로 선언해야 함!!
프로퍼티가 초기화되기 전에 항상 값을 가지고 있어야 하는데, 값이 없을 수도 있기 때문이다. (값이 변경될 수 있기 때문?) 그래서 lazy let으로 선언하면 에러가 발생함.

var sodeul: Human? = .init(name: "Kim-Sodeul")
print(sodeul!.getName())

sodeul 인스턴스 생성
클로저로 작성되어 있는 getName이란 지연 저장 프로퍼티 호출

sodeul = nil

sodeul 인스턴스 필요 없어져서 nil 할당함

인스턴스에 nil 할당, 인스턴스 다른 변수에 대입한 적 XX
-> 인스턴스의 RC가 0이 돼서 deinit함수가 호출되지 않고 Kim-Sodeul이 출력이 됨.

human이란 인스턴스는
print(sodeul!.getName())
getName을 호출하는 순간 getName이란 클로저가 힙 영역에 할당되면 클로저를 참조할 것.

lazy var getName: () -> String = {
return self.name
}
getName이란 클로저를 보면 self를 통해 Human이란 인스턴스의 프로퍼티에 접근하고 있음.
클로저는 Reference 값을 캡쳐할 때 기본적으로 strong하게 캡처함.
이때 Human이란 인스턴스의 Reference Count가 증가해버림

따라서 Human 인스턴스는 클로저를 참조하고 클로저는 Human 인스턴스(의 변수)를 참조하기 때문에
서로가 서로를 참조하고 있음.
Human 인스턴스 -> 클로저 참조
Human 인스턴스(의 변수) 참조 <- 클로저
둘다 메모리에서 해제되지 않는 강한 순환 참조 발생!

클로저의 강한 순환 참조 해결법
weak & unowned + Capture Lists
self에 대한 참조를 Closure Captur Lists를 이용해 weak, unowned로 캡쳐하는 것.

예시
class Human {
lazy var getName: () -> String? = { [weak self] in
return self?.name
}
}

class Human {
lazy var getName: () -> String = { [unowned self] in
return self.name
}
}

[대괄호] 안에 weak, unowned로 Reference Capture하면 강환 순환 참조를 해결할 수 있음.

weak의 경우 nil을 할당받을 가능성이 있어 Optional-Type으로 self에 대한 Optional Binding을 해야 함.
unowned의 경우 Non-Optional Type으로 self에 대한 Optional Binding 없이 사용할 수 있음.

참고 문헌
소들이
https://babbab2.tistory.com/81
https://babbab2.tistory.com/83
야곰
https://yagom.github.io/swift_basic/contents/12_closure/

https://velog.io/@delmasong/Closure-Capture-List
https://velog.io/@minji0801/Swift-%EA%B8%B0%EC%B4%88-%EB%AC%B8%EB%B2%95-lazy-%ED%82%A4%EC%9B%8C%EB%93%9C

기본 UI 컴포넌트

UIKit의 Storyboard로 프로젝트 생성
UILabel, UIButton 추가
Button을 누르면 UILabel의 텍스트의 숫자가 1씩 증가하도록 한다
위의 내용의 소스코드 파일을 과제란에 복사 붙여넣기 한다.

import UIKit
class ViewController: UIViewController {

@IBOutlet weak var labelNumber: UILabel!
var number: Int = 0

override func viewDidLoad() {
    super.viewDidLoad()
    labelNumber.text = String(number)
}

@IBAction func buttonTapped(_ sender: Any) {
    number += 1
    labelNumber.text = String(number)
}

}

수행 인증 화면!!

프로토콜
프로토콜이란?
어떤 기능에 적합한 특정 메서드, 프로퍼티 및 기타 요구 사항의 청사진을 정의함.
구조체, 클래스, 열거형은 프로토콜을 채택(Adopted)해서 특정 기능을 수행하기 위한 프로토콜의 요구사항을 실제로 구현할 수 있음.
어떤 프로토콜의 요구사항을 모두 따르는 타입은 그 프로토콜을 준수한다(Conform)라고 표현함.

프로토콜 정의 문법
protocol 키워드 사용하여 정의함
protocol 프로토콜 이름 {
/ 정의부 /
}

프로토콜 구현
protocol Band {
var drum: String { get set }
var vocal: String { get set }
var piano: String { get set }
var guitar: String { get set }

func play()

}

이 프로토콜을 따르려면 이러한 것들이 필요하다는 약속을 정의해두는 것.
프로토콜은 어떤 기능을 수행할 때 필요한 어떠한 프로퍼티 or 메서드에 대해 구현하는 것이 아니라 선언만 해두는 것이라고 이해함.
위 코드를 예시로 들어보면 “Band 프로토콜을 따르기 위해서 drum, vocal, piano, guitar라는 프로퍼티와 play라는 메서드가 정의되어야 한다.”라는 것.

프로토콜 채택 및 준수
Band 프로토콜을 선언하여 클래스, 구조체, 열거형이 채택하여 사용할 수 있게 만들 수 있다.
만약 ABand라는 새로운 밴드를 구조체 형식으로 만듦.
struct ABand {}

ABand를 생성할 때 필요한 요소들을 미리 정의해둔 Band라는 프로토콜을 따르고 싶다면 다음처럼 코드를 작성할 수 있음.
struct ABand: Band {}

상속처럼 Band 프로토콜을 이름 옆에 선언해준다면 해당 프로토콜을 채택하게 됨.

하지만 위처럼 프로토콜을 단지 이름 옆에 선언하면 오류가 뜸.
Band 프로토콜 안에 선언만 되어 있던 것들을 채택한 곳인 ABand 구조체 안에서 실제로 구현해줘야 함!

알 수 있는 것은
프로토콜은 프로퍼티/메서드에 대한 껍데기만 제공하고 실제 구현은 채택한 곳에서 하는 것.

프로토콜 Optional 선언
있을 수도 없을 수도 있는 희귀한 프로퍼티는 optional로 선언할 수 있음.
아래 예시에서는 bass가 optional 프로퍼티로 선언됨.
@objc protocol Band {
var drum: String { get set }
var vocal: String { get set }
var piano: String { get set }
var guitar: String { get set }
@objc optional var bass: String { get set }

func play()

}

bass 프로퍼티 한해서는 required가 아니란 optional.
채택해주는 ABand에서는 꼭 선언해주지 않아도 에러가 발생하지 않음.
하지만 @objc가 붙는 순간 호환성을 위해서 클래스 전용일 때 사용하는 AnyObject가 아래처럼 자동으로 채택됨.

@objc protocol Band: AnyObject {
var drum: String { get set }
var vocal: String { get set }
var piano: String { get set }
var guitar: String { get set }
@objc optional var bass: String { get set }

func play()

}

Optional은 채택하는 곳에서 선언 했을지 안 했을지 여부를 알 수 없기 때문에
Band라는 프로토콜 타입을 통해 bass에 접근할 경우
String? 즉 Optional(String) 타입으로 접근해야 함.

야곰
https://yagom.github.io/swift_basic/contents/19_protocol/

소들이
https://babbab2.tistory.com/174
https://babbab2.tistory.com/175

0개의 댓글