til's People
til's Issues
[Swift] didSet / willSet
didSet
은 프로퍼티의 값이 변경된 후에 호출되는 프로퍼티 옵저버입니다.
예를 들어, 아래의 코드에서 isAllFieldsFilled 프로퍼티에 didSet 옵저버가 추가되어 있습니다.
private var isAllFieldsFilled: Bool = false {
didSet {
nextButton.backgroundColor = isAllFieldsFilled ? .customBlue : .lightGray
nextButton.isEnabled = isAllFieldsFilled
}
}
이 코드에서 isAllFieldsFilled의 값이 변경될 때마다 didSet 블록이 실행됩니다.
didSet 블록 내에서는 프로퍼티의 변경 후 실행되는 로직을 구현할 수 있습니다.
위의 예시에서는 nextButton의 배경색과 활성화 상태를 isAllFieldsFilled의 값에 따라 변경하고 있습니다.
만약 isAllFieldsFilled이 true로 변경되면 nextButton의 배경색을 .customBlue로, 활성화 상태를 true로 설정합니다.
그렇지 않으면 배경색을 .lightGray로, 활성화 상태를 false로 설정합니다.
이렇게 didSet을 사용하면 프로퍼티의 값이 변경될 때마다 특정 동작을 수행할 수 있습니다.
willSet
은 프로퍼티의 값이 변경되기 직전에 호출되는 프로퍼티 옵저버입니다.
예를 들어, 아래의 코드에서 count
라는 프로퍼티에 willSet
옵저버가 추가되어 있습니다.
var count: Int = 0 {
willSet(newCount) {
print("현재 count 값은 \(count)이고, 새로운 값은 \(newCount)입니다.")
}
}
이 코드에서 count
프로퍼티의 값이 변경되기 직전에 willSet
블록이 호출됩니다.
willSet
블록 내에서는 새로운 값인 newCount
와 현재 프로퍼티의 값인 count
에 접근할 수 있습니다.
예를 들어, count
의 값을 변경하는 코드가 다음과 같이 있다고 가정해봅시다.
count = 10
이 코드를 실행하면 다음과 같은 출력이 나타납니다.
현재 count 값은 0이고, 새로운 값은 10입니다.
즉, willSet
옵저버가 호출되어 현재 count
의 값과 새로운 값이 출력됩니다.
이후 count
프로퍼티의 값이 변경됩니다.
willSet
을 사용하면 프로퍼티의 값이 변경되기 직전에 원하는 로직을 실행할 수 있습니다.
Push vs Present
🔗[HIG링크](https://developer.apple.com/design/human-interface-guidelines/modality)
화면전환 구현하기
🔴 UITabBarController
다른 탭을 선택함으로써 화면을 전환.
🔴 modal (present - dismiss)
다른 뷰 컨트롤러 위에 올라가는 형태로 표시되며,
사용이 끝나면 해당 뷰 컨트롤러를 닫고 원래의 뷰 컨트롤러로 돌아감.
(사용자에게 주의를 끌고 중요한 작업을 수행하도록 유도하는 데에 유용하며,
일시적으로 화면을 차단하고 사용자의 주의를 확보할 수 있는 효과적인 방법 중 하나임)
🔴 UINavigationController (push - pop) 스택
뷰 컨트롤러를 스택에 push하여 화면을 전환.
![스크린샷 2024-04-30 오후 2 55 11](https://private-user-images.githubusercontent.com/108393988/326686260-f66d59f1-4935-4407-86ca-d91904ba5522.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTkzOTAzODIsIm5iZiI6MTcxOTM5MDA4MiwicGF0aCI6Ii8xMDgzOTM5ODgvMzI2Njg2MjYwLWY2NmQ1OWYxLTQ5MzUtNDQwNy04NmNhLWQ5MTkwNGJhNTUyMi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQwNjI2JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MDYyNlQwODIxMjJaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT05MTE3MTVmOTZjMWMyNmRiZDZkYmM5MDJkZDk1ZThjNzc4MzA0MTYwZjJkZjA2NDYyYjBiMDQ3YzE4MjkxY2FhJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.Doixq3_51uYWc63T2BgnvZx1OaEZz80sORE_cMEgnCo)
present
- 세로 방향으로 전개되는 View
- 화면을 다른 화면 위로 띄워 표현하는 방식
- 흐름을 이어지는 컨텐츠를 담기 보다는 흐름을 끊고 눈에 보여줘야하는 콘텐츠를 담는데 사용
UIViewController
의 메소드- 실행시키면 기존의
UIViewController
위에 새로운 뷰를 띄운다. - 이전 뷰로 돌아가기 위해서는
dismiss
dismiss
present된 뷰를 메모리에서 삭제하고 걷어냄
계층적 구조의 뷰 워크플로우를 구현하기 위해 사용
present방식과 달리 자식 뷰는 stack으로 관리
push
- 가로 방향으로 전개되는 View (왼→오른)
UINavigationController
의 메소드.- 실행시키면 기존의
UIViewController
위에 새로운 뷰를 띄운다.
navigation에 새로운 VC를 쌓는 개념. - 스택에 뷰를 쌓는 형태로 화면을 업데이트
- 이전 뷰로 돌아가기 위해서는
pop
pop
스택에 쌓인 뷰를 1개 pop하고 돌아가기
[iOS] Moya
- Moya는 Alamofire를 기반으로 한 네트워킹 추상화 라이브러리입니다.
- 네트워킹 계층을 추상화하여 각 요청과 응답을 쉽게 정의하고 관리할 수 있도록 도와줍니다.
- 서비스 API와 관련된 코드를 추상화하여 모델 기반의 스타일로 작성할 수 있습니다.
이는 유지보수성과 코드의 재사용성을 높여줍니다. - Alamofire의 기능을 사용하면서, 네트워킹 코드를 보다 간결하게 유지할 수 있도록 해줍니다.
- 일반적으로 형거형
enum
을 사용해서 네트워크 요청 타입을 안전한 방식으로 캡슐화합니다. urlSession
,Alamofire
를 한번 더 감싼 통신 API 입니다.
![image](https://private-user-images.githubusercontent.com/108393988/325175877-89368f7e-1e86-4e65-9fbf-1996cff03492.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTQ2MjM5MjMsIm5iZiI6MTcxNDYyMzYyMywicGF0aCI6Ii8xMDgzOTM5ODgvMzI1MTc1ODc3LTg5MzY4ZjdlLTFlODYtNGU2NS05ZmJmLTE5OTZjZmYwMzQ5Mi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQwNTAyJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MDUwMlQwNDIwMjNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0zMTMyMzliZWM3YTdlOTc2MTVjZmE3ZTYyZDIwNjZjMDc4MjEzN2U2ZDYxNTAyZDZkZmQzZGEzZjY4OTE1Y2NlJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.Nx_0dqJ8liwi0WOUbnNlEG6WBN2cpEL6k5Ul__2FSUA)
- Compile-time checking for correct API endpoint accesses.
컴파일시 API 엔드 포인트가 올바른지 체크
- Lets you define a clear usage of different endpoints with associated enum values.
Enum을 이용해서 언제, 어디에 사용될지 안전하게 (type-safe) 정의
- Treats test stubs as first-class citizens so unit testing is super-easy.
유닛 테스트를 용이하게 만듦.
- API 목록 작성 with
Enum
import Moya
enum JokeAPI {
case randomJokes(_ firstName: String? = nil, _ lastName: String? = nil)
}
TargetType
작성 (네트워크에 필요한 속성들 제공)
enum으로 작성한 API 목록을 extension 할건데, TargetType 프로토콜을 Conform 하여
API 별로 리퀘스트에 필요한 것들을 지정할 수 있음!
**baseURL**
: Server base URL 지정**path**
: API Path 지정**method**
: HTTP Method 지정**sampleData**
: Mock Data for Test**task**
: Parameters for request 지정**validationType**
: 허용할 response 정의 - [validationType 참고](https://github.com/Moya/Moya/blob/development/Sources/Moya/ValidationType.swift) 기존
**Alamofire 의 .validate()**
처럼**리스폰스의 StatusCode**
에 따라 성공 유무를 판단**headers**
: HTTP headers 적용: 기존의**인터셉터의** **Adapter**
역할을 할 수 있습니다. (ex. 헤더에 JWT 값 넣기)
Switch 문을 활용해서 간편하게 여러 타입의 API에 대한 값들을 지정할 수 있고,
필요없는 파라미터는 wild card 구문 _ 을 사용해서 표현하면 됩니다!
extension JokeAPI: TargetType {
var **baseURL**: URL {
return URL(string: "https://api.icndb.com")!
}
var **path**: String {
switch self {
case .randomJokes(_, _):
return "/jokes/random"
}
}
var **method**: Moya.Method {
switch self {
case .randomJokes(_, _):
return .get
}
}
var **sampleData**: Data {
switch self {
case .randomJokes(let firstName, let lastName):
let firstName = firstName
let lastName = lastName
return Data(
"""
{
"type": "success",
"value": {
"id": 107,
"joke": "\(firstName)\(lastName) can retrieve anything from /dev/null."
}
}
""".utf8
)
}
}
var **task**: Task {
switch self {
case .randomJokes(let firstName, let lastName):
let params: [String: Any] = [
"firstName": firstName,
"lastName": lastName
]
return .requestParameters(parameters: params, encoding: URLEncoding.queryString)
}
}
var **headers**: [String : String]? {
return ["Content-type": "application/json"]
}
}
- Response Struct 생성
받아온 **Response를 저장할 구조체
**를 선언
Alamofire를 사용할 때와 동일한 방식으로 생성하면 됩니다. (Codable 프로토콜 준수)
struct Joke: Decodable {
var type: String
var value: Value
struct Value: Decodable {
var id: Int
var joke: String
}
}
response 받은 JSON의 형태
{
"type": "success",
"value": {
"id": 268,
"joke": "Time waits for no man. Unless that man is John Doe."
}
}
- Request with Moya Provider
**Moay Provier**
생성
private let provider = MoyaProvider<JokeAPI>()
네트워크 요청을 수행할 Moya Provider 인스턴스를 생성 해줍니다.
Moya Provider는 제네릭 타입으로 TargetType 프로토콜을 준수하는 Enum을 받고 있습니다.
즉, API의 동작 범위에 따라 인스턴스를 구분하여 생성할 수 있고,
이 인스턴스들을 이용해 동작을 하는 메소드를 사용할 때 재사용할 수 있습니다.
제네릭은 코드를 작성할 때 타입을 명확히 지정하지 않고 일반화하여 사용할 수 있도록 도와주는 기능입니다. 이를 통해 코드의 재사용성과 유연성을 높일 수 있습니다.
제네릭 타입을 사용하는 Moya의 Provider는 TargetType 프로토콜을 준수하는 Enum을 받습니다.
이 말은 Moya의 Provider가 어떤 타입의 네트워크 요청을 수행할 것인지를 명시적으로 지정하지 않고,
여러 종류의 네트워크 요청을 일반화하여 처리할 수 있도록 도와준다는 뜻입니다.
예를 들어, 여러 종류의 API를 다루는 경우 각각의 API에 대한 네트워크 요청을 별도로 작성하는 대신,
제네릭 타입을 사용하여 API의 동작 범위를 일반화하여 한 번에 다룰 수 있습니다.
따라서 한 개의 Moya Provider 인스턴스를 생성하여 여러 종류의 API 요청을 처리할 수 있습니다.
이렇게 하면 코드의 재사용성이 높아지고 유지보수가 쉬워집니다.
- 네트워크 요청/처리 - Moya
provider.request(.randomJokes("GilDong", "Hong")) { (result) in
switch result {
case let .success(response):
let result = try? response.map(Joke.self)
self.jokeTextView.text = result?.value.joke
case let .failure(error):
print(error.localizedDescription)
}
생성한 Provider를 통해 **request
**를 요청할 수 있습니다.
provider.request의 파라미터로 아까 생성한 TargetType을 전달해주세요.
이때, 반환되는 response는 Result<Response, MoyaError>
형태입니다.
[iOS] Core Graphics
Core Graphics란?
2차원 그래픽을 그릴 수 있도록 제공하는 그래픽 라이브러리
Quartz
코어 그래픽스는 애플이 제공하는 ‘쿼츠’라는 그래픽 라이브러리 안에 포함됨
(하나의 라이브러리가 아니라 ‘코어 그래픽 + 코어 애니메이션’으로 구성됨)
drawRect : 그리기를 해줌
점/좌표/픽셀 : CGFloat 형태로 왼쪽 상단 0,0 부터 위치를 나타내줌
CGPoint: 2D 공간에서 위치를 표현함
CGSize : 두개의 CGFloat 값으로 넓이 / 높이 나타내줌
Graphic Context : 쿼츠 API 함수 호출로 인자 넘겨받은 그래픽 콘텍스트 필요 (그리기 정보)
UIGraphicsGetCurrentContext() 함수 호출로 그래픽 콘텍스트 정보를 얻어 그릴 준비를 함
선 그리기
@IBAction func btnDrawLine(_ sender: UIButton) {
// 그림을 그리기 위한 콘텍스트 생성
UIGraphicsBeginImageContext(imgView.frame.size)
// 생성된 콘텍스트 정보 획득
let context = UIGraphicsGetCurrentContext()!
// Draw Line
// 선 굵기를 2.0으로 설정
context.setLineWidth(2.0)
// 선 색상을 빨간색으로 설정
context.setStrokeColor(UIColor.red.cgColor)
// 커서의 위치를 (50,50)으로 이동
context.move(to: CGPoint(x: 50, y: 50))
// 시작 위치에서 (250,250)까지 선 추가
context.addLine(to: CGPoint(x: 250, y: 250))
// 추가하 선을 콘텍스트에 그림
context.strokePath()
// Draw Triangle
// 선 굵기를 4.0으로 설정
context.setLineWidth(4.0)
// 선 색상을 파란색으로 설정
context.setStrokeColor(UIColor.blue.cgColor)
// 커서의 위치를 (150,200)으로 이동
context.move(to: CGPoint(x: 150, y: 200))
// 시작 위치에서 (250,350)까지 선 추가
context.addLine(to: CGPoint(x: 250, y: 350))
// 이전 위치에서 (50,350)까지 선 추가
context.addLine(to: CGPoint(x: 50, y: 350))
// 이전 위치에서 (150,200)까지 선 추가
context.addLine(to: CGPoint(x: 150, y: 200))
// 추가한 선을 콘텍스트에 그림
context.strokePath()
// 현재 콘텍스트에 그려진 이미지를 가지고 와서 이미지 뷰에 할당
imgView.image = UIGraphicsGetImageFromCurrentImageContext()
// 그림 그리기를 끝냄
UIGraphicsEndImageContext()
}
사각형 그리기
@IBAction func btnDrawRectangle(_ sender: UIButton) {
UIGraphicsBeginImageContext(imgView.frame.size)
let context = UIGraphicsGetCurrentContext()!
// Draw Rectangle
context.setLineWidth(2.0)
context.setStrokeColor(UIColor.red.cgColor)
// (50,100) 위치에서 가로 200, 세로 200 크기의 사각형 추가
context.addRect(CGRect(x: 50, y: 100, width: 200, height: 200))
context.strokePath()
imgView.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
원 그리기
@IBAction func btnDrawCircle(_ sender: UIButton) {
UIGraphicsBeginImageContext(imgView.frame.size)
let context = UIGraphicsGetCurrentContext()!
// Draw Ellipse
context.setLineWidth(2.0)
context.setStrokeColor(UIColor.red.cgColor)
// (50,50) 위치에서 가로 200, 세로 100 크기의 타원 추가.
context.addEllipse(in: CGRect(x: 50, y: 50, width: 200, height: 100))
context.strokePath()
// Draw Circle
context.setLineWidth(5.0)
context.setStrokeColor(UIColor.green.cgColor)
// (50,200) 위치에서 가로 200, 세로 200 크기의 타원 추가. 즉 원을 추가
context.addEllipse(in: CGRect(x: 50, y: 200, width: 200, height: 200))
context.strokePath()
imgView.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
호 그리기
@IBAction func btnDrawArc(_ sender: UIButton) {
UIGraphicsBeginImageContext(imgView.frame.size)
let context = UIGraphicsGetCurrentContext()!
// Draw Arc
context.setLineWidth(5.0)
context.setStrokeColor(UIColor.red.cgColor)
context.move(to: CGPoint(x: 50, y: 50))
// 호 그리기
context.addArc(tangent1End: CGPoint(x: 200, y: 50), tangent2End: CGPoint(x: 200, y: 200), radius: CGFloat(50))
context.addLine(to: CGPoint(x: 200, y: 200))
context.move(to: CGPoint(x: 100, y: 250))
// 호 그리기
context.addArc(tangent1End: CGPoint(x: 250, y: 250), tangent2End: CGPoint(x: 100, y: 400), radius: CGFloat(20))
context.addLine(to: CGPoint(x: 100, y: 400))
context.strokePath()
imgView.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
색상 채우기
@IBAction func btnDrawFill(_ sender: UIButton) {
UIGraphicsBeginImageContext(imgView.frame.size)
let context = UIGraphicsGetCurrentContext()!
// Draw Rectangle
context.setLineWidth(1.0)
context.setStrokeColor(UIColor.red.cgColor)
// 채우기 색상 설정
context.setFillColor(UIColor.red.cgColor)
let rectangle = CGRect(x: 50, y: 50, width: 200, height: 100)
context.addRect(rectangle)
// 사각형 채우기
context.fill(rectangle)
context.strokePath()
// Draw Circle
context.setLineWidth(1.0)
context.setStrokeColor(UIColor.blue.cgColor)
context.setFillColor(UIColor.blue.cgColor)
let circle = CGRect(x: 50, y: 200, width: 200, height: 100)
context.addEllipse(in: circle)
// 타원 채우기
context.fillEllipse(in: circle)
context.strokePath()
// Draw Triangle
context.setLineWidth(1.0)
context.setStrokeColor(UIColor.green.cgColor)
context.setFillColor(UIColor.green.cgColor)
context.move(to: CGPoint(x: 150, y: 350))
context.addLine(to: CGPoint(x: 250, y: 450))
context.addLine(to: CGPoint(x: 50, y: 450))
context.addLine(to: CGPoint(x: 150, y: 350))
// 선 채우기
context.fillPath()
// 선으로 그려진 삼각형의 내부 채우기
context.strokePath()
imgView.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
[iOS] GCD 사용시 주의사항
1️⃣ 반드시 메인큐에서 처리해야 하는 작업
메인 Thread 화면을 다시 그리는 역할 → UI관련 일들은 다시 메인쓰레드로 보낼 필요
DispatchQueue.main
-
에러발생
UI와 관련된 작업들은 메인쓰레드에서 처리하지 않으면 에러 발생(메인쓰레드가 아닌 쓰레드는 그림을 다시 그리지 못함)
DispatchQueue.global().async { ... self.textLabel.text = "New posts updated!" // ❌ Error }
-
메인쓰레드
UI와 관련된 작업들을 메인쓰레드에서 처리할 수 있도록 메인큐를 통해서, 작업을 다시 메인쓰레드로 보냄
DispatchQueue.global().async { ... DispatchQueue.main.async { self.textLabel.text = "New posts updated!" } }
UI관련 일이기 때문에 그림을 다시 그리는 작업은 메인큐에서!
2️⃣ completionHandler의 존재이유 - 올바른 콜백함수의 사용
데이터를 return으로 전달하면 안되고, closure로 전달해야함!
@escaping(데이터) -> Void
-
잘못된 함수설계
비동기적인 작업을 해야하는 함수를 설계할때, return을 통해서 데이터를 전달하면 항상 nil이 반환
func getImage(image: string) -> UIImage? { URLSession.shared.dataTask(...) { .. }.resume() ... return photoImage // ⭐️ 함수 내부의 일이 끝나기전에 return 하므로 무조건 nil을 반환! }
-
제대로된 함수 설계
비동기적인 작업을 해야하는 함수는 항상 클로저를 호출할 수 있도록 함수를 설계해야함
func getImage(image: @escaping (UIImage?) -> Void) { ... URLSession.shared.dataTask(..) { ... completion(photoImage) // ⭐️ 함수 내부의 일이 끝나면 completion 클로저 호출! }.resume() }
위 예제에서 URLSession은 내부적으로 비동기적으로 실행되기 때문에 내부 실행을 기다리지 않고 return 으로 넘어감 → 무조건 nil로 리턴됨..
즉, return이 아닌 콜백함수를 통해 끝나는 시점을 알려줘야함!
3️⃣ weak, strong 캡처 주의 - 객체 내에서 비동기코드 사용 시
-
강한참조
캡처리스트 안에서 weak self로 선언하지 않으면 강한 참조(strong)
-
서로를 가리키는 경우 메모리누수 발생 가능
-
클로저의 수명주기가 길어지는 현상이 발생
-
-
약한참조
대부분의 경우, 캡처리스트 안에서 weak self로 선언하는 것을 권장
DispatchQueue.global().async { [weak self] in guard let self = self else { return } ... DispatchQueue.main.aysnc { self.textLabel.text = "New posts updated!" } }
4️⃣ 동기함수를 비동기적으로 동작하는 함수로 변형하는 방법
-
일반(동기)함수
오래걸리는 일반적인 함수를 단순히 동기함수로 만들면 메인쓰레드에 부하가 걸림
func doSomething() { print("시작") sleep(3) print("종료") } print("1") doSomething() print("2") // 1 // 시작 // 종료 // 2
-
비동기함수
오래걸리는 일반적인 함수를 내부에 비동기적 처리를 하면 비동기로 동작하는 함수로 변형가능
func doSomething(com: @escaping (Void) -> Void) { DispatchQueue.global().async { print("시작") sleep(3) print("종료") com() } } print("1") doSomething() print("2") // 1 // 2 // 시작 // 종료 // 등등..
5️⃣ 비동기 함수/메서드의 이해
일반적으로 대부분의 네트워킹 등 오래걸리는 API들은 따로 비동기처리를 하지 않아도 내부가 비동기적으로 구현되어 있음
URLSession(configuration: .default).dataTask(with: url) { (data, response, error) in
self.image = UIImage(data: data!)
}
내부가 비동적으로 처리 되어있지 않아, 디스패치큐로 클로저를 보내서 명시적으로 비동기적 처리가 필요한 API들도 있음
DispatchQueue.global().async {
...
let imageData = try? Data(contentsof: url)!
let someImage = UIImage(data: imageData)!
DispatchQueue.main.async {
self.imageView.image = someImage
}
}
[Swift] GCD
동기/비동기 그리고 Blocking/Non-blocking
동기/비동기
: 스레드의 수와는 무관하게 작업이 끝나기를 기다리냐, 기다리지 않느냐의 구분 ⭐️
동기/비동기는 작업의 진행 방식을 나타내고, (결과값과 순서에 관련된 개념, retrun값)
Block/Non-block은 대기여부를 나타냄. (CPU 제어권과 관련된 개념, 제어권 여부)
동기(sync): 결과값을 기다리고, 순차적 실행
비동기(async): 결과값을 기다리지 않고, 비순차적 실행
Blocking: 제어권 바로 반환하지 않음
Non-blocking: 제어권 바로 반환
Swift에서는 동기는 Blocking 개념으로 비동기는 Non-blocking의 개념으로만 사용
직렬과 동시
동시성
: Serial이냐, Concurrent인가는 스레드가 단일 스레드인가, 다중 스레드인가의 구분 ⭐️
직렬(Serial)
(보통 메인에서) 분산처리 시킨 작업을 “다른 한개의 쓰레드에서” 처리하는 큐
순서가 중요한 작업을 처리할때 사용
동시(Concurrent)
(보통 메인에서) 분산처리 시킨 작업을 “다른 여러개의 쓰레드에서” 처리하는 큐
각자 독립적이지만 유사한 여러개의 작업(중요도나 작업의 성격등)을 처리할때 사용
동시성 프로그래밍 vs 병렬 프로그래밍
1️⃣ 동시성 프로그래밍은 병렬 프로그래밍과 반대되는 개념은 아니다.
병렬 프로그래밍은 다중 코어를,
동시성 프로그래밍은 다중 스레드를 활용하는 것
이 둘은 동시에 일어날 수 있다.
2️⃣ 여러 가지 일을 여러 개의 코어에서 담당하는 것은 병렬 프로그래밍이 아니다.
정확히는 병렬 프로그래밍일 수도 있고, 아닐 수도 있다.
시스템에서 어떻게 일을 처리하게 하는지는 정확히 알 수 없으니까.
하지만 4가지 일을 4개의 코어가 하나씩 담당해서 한다고 생각했을 때 이것은 사실 병렬 프로그래밍이 아니다.
병렬 프로그래밍은 한 가지의 일을 여러 코어가 분담해서 하는 것이다.
이 상황은 각 코어가 일을 하나씩 맡아서 처리하는 상황일 뿐.
별개의 이야기지만 일을 처리하는 내부적인 환경은 단일 스레드 환경일 수도 있고, 다중 스레드 환경일 수도 있다.
동시성 프로그래밍
동시성 프로그래밍은 하나의 CPU가 여러 작업을 동시에 처리하는 것
동시성 프로그래밍은 병렬 프로그래밍과 달리 싱글 코어에서도 가능한 논리적인 개념
물리적으로는 하나의 작업만 처리하는 환경(코어가 하나)에서 논리적으로 여러 일을 동시에 처리하기 때문(물론 여러개의 코어로도 동시성 프로그래밍이 가능)
여러 개의 스레드를 이용하여 동시에 여러 작업을 처리
실제로는 여러 가지 작업을 나열해두고 하나씩, 그리고 조금씩 번갈아가면서 작업을 처리하는 것인데 그 속도가 매우 빨라서 동시에 작업을 처리하는 것처럼 보여짐.
병렬 프로그래밍
병렬 프로그래밍은 **여러 개의 CPU(코어)**가 유사하거나 동일한 작업을 분담해서 처리하는 것
많은 연산이 필요한 그래픽 처리나 머신 러닝에서는 이 병렬 프로그래밍이 적극적으로 사용됨.
병렬 프로그래밍은 물리적인 개념
으로 CPU(코어)가 여러 개 있을 때에 가능함.
싱글 코어에서는 병렬 프로그래밍을 할 수가 없다. 왜냐하면 병렬 프로그래밍은 실제로 동시에
작업을 처리하는 것이기 때문.
병렬 프로그래밍은 iOS를 개발하면서는 직접 구현할 일은 없다.
그러니 동시성 프로그래밍과 비교하며 개념만 알아두면 됨.
운영체제에서 알아서 처리해주는 영역이기 때문.
DispatchQueue
class DispatchQueue : DispatchObject
- Disaptch: 보내다(파견하다)
- Queue: 대기열
GCD는 개발자가 작업만 정해주면 시스템이 알아서 스레드를 관리
DispatchQueue에 작업을 넘겨주기만 하면 알아서 동작
DispatchQueue는 GCD를 사용하기 위한 대기열
로, GCD 기술의 일부
대기열 들에 작업을 추가해주기만 하면 시스템은 알아서 스레드를 관리하여 작업을 처리하도록 도와줄 것
DispatchQueue는 FIFO으로 작업을 처리
단, DispatchQueue에 작업을 넘길 때 지켜야할 두 가지
단일 스레드를 사용할 것인가, 다중 스레드를 사용할 것인가 **(Serial/Concurrent)**
그리고 동기로 작업을 처리할 것인가, 비동기로 작업을 처리할 것인가**(sync/async)**
Serial / Concurrent
DispatchQueue는 크게 Serial, Concurrent 두가지 큐로 구분할 수 있음
Serial은 단일 스레드에서만, Concurrent는 다중 스레드에서 작업을 처리함
DispatchQueue를 초기화할 때 attributes를 따로 설정하지 않으면 기본 값인 Serial로 설정됨.
// Serial Queue
DispatchQueue(label: "Serial")
DispatchQueue.main
// Concurrent Queue
DispatchQueue(label: "Concurrent", attributes: .concurrent)
DispatchQueue.global()
main / global
DispatchQueue.**main**
과 DispatchQueue.**global**()
은 이미 만들어져 있는 큐로 각각 Serial, Concurrent큐임.
특히 main은 일반적인 Serial큐와는 달리 앱이 실행되는 동안에는 늘 메모리에 올라와 있으며, 또 전역적으로 사용 가능한 큐라는 특수한 성질을 가짐.
main에 작업을 추가하면 Serial큐인 main스레드에서 작업을 처리
하나의 스레드에 작업이 쌓여서 처리가 되는 것임.
따라서 main 스레드에만 작업을 쌓을 경우 동시에 여러 작업을 처리할 수 없음
반면 global에 작업을 추가하면 새로운 스레드를 만들어 그 위에서 작업을 처리함.
global스레드는 main 스레드가 아닌, 작업을 처리하기 위해 발생한 스레드들을 말함.
global()이 호출되면 작업을 처리하기 위해 메모리에 올라왔다가, 작업이 끝나고 나면 메모리에서 제거됨.
시스템이 관리해주기 때문! 덕분에 손 쉽게 비동기 프로그래밍 구현이 가능함
DispatchQueue로 GCD를 구현하기 위해서는 2가지를 정해주어야한다.
Serial큐에서 작업할건지, Concurrent큐에서 작업할것인지 정해주었다면,
다음으로는 동기/비동기 처리를 해주면 됨
// 동기, sync
DispatchQueue.main.sync {}
DispatchQueue.global().sync {}
// 비동기, async
DispatchQueue.main.async {}
DispatchQueue.global().async {}
Queue의 종류
DispatchQueue
-
.main: DispatchQueue.main
메인큐 = 메인쓰레드 (UI업데이트 내용 처리하는 큐)
Serial 직렬
-
.global: DispatchQueue.global(qos: )
6가지 Qos (작업에 따라 Qos 상승가능), (시스템이 우선순위에 따라 더 많은 쓰레드를 배치하고, 배터리를 더 집중해서 사용하도록 함)
Concurrent 동시
-
.custom: DispatchQueue(label: “…”)
Qos추론 / Qos설정가능
디폴트: Serial, 직렬/동시 둘다 가능(attributes로 설정)
OperationQueue
… 공부필요
메인 쓰레드?
메인스레드는 앱의 기본이 되는 스레드임.
메인 스레드는 앱의 생명주기를 가지는, 앱이 실행되는 동안에는 늘 메모리에 올라와있는 기본 스레드임
메인 스레드가 멈추는 것은 앱이 멈추는 것이며 메인스레드가 존재하지 않으면 앱은 동작할 수 없음.
동시성 프로그래밍에서는 여러개의 스레드를 사용함
이 경우에도 메인 스레드는 늘 메모리에 올라와있는 상태로 존재하며 메인 스레드에서부터 필요한 만큼의 스레드가 파생되는 것
이때 파생되는 스레드들은 자신이 담당하는 작업이 처리되면 메모리에서 사라짐.
main 스레드의 특징
- 전역적으로 사용이 가능
- global 스레드들과는 다르게 Run Loop가 자동으로 설정되고 실행됨
- UI작업은 메인 스레드에서만 작업가능
… 추가
[Swift] static
타입 프로퍼티/함수?
인스턴스 메서드 (Instance Method)
class A {
func functionA() {
print("functionA in class A")
}
let a: A = .init()
a.functionA()
A란 타입을 갖고 있는 a라는 인스턴스를 생성
a의 functionA에 접근해서 그걸 갖다 씀
즉, functionA는 인스턴스를 생성해야만 접근할 수 있는 함수 → 인스턴스 메서드
A.functionA() // ❌ 에러남 ❌
에러내용 → "instance member 'functionA' cannot be used on type 'A';"
= 인스턴스 멤버인 'functionA'는 타입 'A'에 걸어서 쓸 수 없음
타입 메서드 (Type Method)
인스턴스 메서드가 ‘인스턴스에 걸려있는’ 메서드라면
타입 메서드는 ‘타입에 걸려있는’ 메서드
타입 메서드로 선언하고 싶을 때, 앞에 쓰는게 ‘static’임..
class A {
static func isStatic() {
print("static function")
}
class func isClass() {
print("class function")
}
}
A.isStatic() // static function
A.isClass() // class function
static 프로퍼티/메서드 사용이유..?
- 클래스/구조체의 인스턴스들 간에 동일한 값을 공유하거나 동일한 동작을 수행하는데 유용함
- 클래스 자체에 연결되어 있으므로 클래스 이름을 통해 직접 호출이 가능함. 클래스의 인스턴스를 생성하지 않고도 프로퍼티나 메서드에 엑세스 할수 있음. (전역 상태나 동작을 모델링)
- 인스턴스 별로 값을 저장하는 대신 클래스/구조체 수준에서 값을 저장하므로 메모리 사용량을 줄임.
→ 전역 상태 또는 동작을 모델링 하거나 클래스/구조체의 인스턴스 간에 공유되는 상태 또는 동작을 구현하는데 유용함.
class와 static 둘 다 타입 프로퍼티로 만드는 키워드인데.. 차이점은?
class func은 오버라이딩이 가능한데,
static func은 오버라이딩이 불가능하다.
static var도 씀?
보통 값이 바뀔일 없는 프로퍼티에 static을 선언함..(ex. 테이블뷰 셀 아이디)
그래서 static let 을 세트처럼 사용함..
그럼 도대체 static var는 언제쓰냐…
class Animal {
static var nums = 0
init() {
Animal.nums += 1
}
}
let dog = Animal()
Animal.nums // 1
let cat = Animal()
Animal.nums // 2
이럴때 쓴다고함…
static을 사용하는 예시
class Student {
static let section: String = "A" // static constat
static var day: String = "Monday" // static variable
var name: String = "Aksh" // instance variable
var rollNum: Int = 1 // instance variable
}
let student1 = Student() // object 1
print(student1.name) // Akash
print(student1.rollNum) // 1
student1.name = "Aman" // Setting object1 value to Aman
print(student1.name) // Aman
let student2 = Student() // Object 2
print(student2.name) // Akash
print(Student.section) // A
print(Student.day) // Monday
[iOS] frame과 bounds의 차이
CGRect는 View의 위치(origin)와 크기(size)를 나타내기 위해 사용됨..
frame과 bounds는 뷰의 위치와 크기 정의하는 속성임..
frame
뷰의 위치와 크기를 슈퍼뷰의 좌표 시스템에서 정의
슈퍼뷰의 좌표 시스템을 기준으로 절대적인 위치를 정의
bounds
(0,0)을 기준으로 상대적인 위치를 정의
뷰의 위치와 크기를 뷰 자체의 좌표 시스템에서 정의
어떤 뷰의 frame과 bounds를 각각 출력해보면 size(width, height)는 동일하지만 origin(x,y) 값이 다름을 볼 수 있다?
→ 뷰의 위치를 해석하는 기준이 다르기 때문임..
frame은 부모 뷰의 좌표시스템,
bounds는 뷰 자체의 좌표시스템이 기준임..
frame
Super View좌표계에서 View의 위치와 크기를 나타낸다.
Super View..?
각 뷰는 다른 뷰를 포함할 수 있다.. 이러한 뷰들은 부모-자식 관계를 형성함.
슈퍼뷰는 특정 뷰의 부모뷰를 나타냄. (뷰 계층에서 한칸 윗 계층 뷰를 의미)
frame의 origin(x,y)
orgin은 해당 뷰의 좌상단 모서리의 위치를 나타내며, CGPoint로 표시됨..
x와 y 좌표값으로 표현되며, 슈퍼뷰의 좌상단 모서리를 기준으로 함 (슈퍼뷰의 원점에서 얼만큼 떨어져 있느냐)
frame의 size(width, height)
size는 해당 뷰의 너비와 높이를 나타내며, CGSize로 표시됨…
width와 height로 표현되며, 픽셀 단위로 뷰의 크기를 정의함
뷰의 영역을 모두 ⭐️ 감싸는 ⭐️ 사각형으로 나타냄 (뷰 자체의 크기가 아니라.. 뷰가 차지하는 영역을 감싸서 만든 사각형)
frame의 size가 변경되면 origin 값도 변경될 수 있다!
bounds
자신의 좌표계에서 View의 위치와 크기를 나타낸다.
bounds의 origin(x,y)
frame에서와는 달리 bounds는 슈퍼뷰와 아무 상관이 없고 기준이 자기 자신임
→ 뷰를 처음 생성하면 **bounds의 origin은 무조건 (0,0)**으로 초기화
-
항상 (0,0) 일거면 bounds의 origin은 무슨 의미가 있을까?
ScrollView의 ContentOffset과 같이 bounds를 바꿔줘야 하는 경우…
추가 공부필요..
-
뷰를 회전 시키면 bounds의 origind은 변할까?
bounds는 시작점이 자신의 원점이기 때문에 뷰가 회전하든 안하든 자신의 원점이 곧 좌표의 시작점임.
→ 변하지 않는다.
bounds의 size(width, height)
뷰 자체의 영역을 나타냄
뷰를 회전 시키면 frame의 size는 바뀜.. 하지만
bounds에서는 뷰를 회전 시켜도 size가 바뀌지 않음.
왜냐, bounds의 size는 뷰 자체의 영역을 나타내기 때문임.
(frame의 size는 뷰 영역을 모두 감싼 사각형)
요약
frame | bounds | |
---|---|---|
origin(x,y) 기준 | super view의 좌표계 | 자신의 좌표계 |
size(width, height) 기준 | view 영역을 모두 감싸는 사각형 | view 영역 자체 |
[iOS] 동시성프로그래밍
코어(Core):
- 코어는 중앙 처리 장치(CPU)의 물리적인 부분을 나타냅니다. 하나의 코어는 하나의 명령어 집합을 실행할 수 있는 독립적인 처리 유닛입니다.
- 다중 코어 시스템에서는 여러 개의 코어가 병렬로 작업을 수행할 수 있습니다.
- 코어의 개수는 시스템의 성능에 직접적인 영향을 미치며, 병렬 처리 능력을 결정합니다.
스레드(Thread):
- 스레드는 프로세스 내에서 실행되는 실행 흐름의 단위입니다. 하나의 프로세스에는 여러 개의 스레드가 있을 수 있습니다.
- 스레드는 독립적인 실행 흐름을 가지며, 서로 다른 스레드 간에는 메모리를 공유할 수 있습니다.
- 다중 스레드 프로그래밍을 통해 여러 작업을 동시에 수행하거나, 하나의 작업을 여러 개의 스레드로 분할하여 병렬로 처리할 수 있습니다.
병렬 프로그래밍
여러개의 CPU(코어)가 유사하거나 동일한 작업을 분담해서 처리하는 것 (다중코어)
Concurrent 동시성 프로그래밍
GCD, Operation, async/sync
여러 스레드를 이용하여 여러 작업을 동시에 처리하는 것
.global()
Serial 직렬 프로그래밍
하나의 스레드
순서대로 작업
기본값
.main
동기 sync /비동기 async
작업이 끝나기를 기다리냐 안기다리냐..
실행 종료시점을 알 수 있는가?
동기 → 기다림, 완전히 끝나야 다음 코드로..
비동기 → 안기다림, 다음 코드블럭 바로 실행
// 동기
DispatchQueue.main.sync {} // 직렬 동기
DispatchQueue.global().sync {} // 동시 동기
// 비동기
DispatchQueue.main.async {} // 직렬 비동기
DispatchQueue.global().async {} // 동시 비동기
GCD
코어와 스레드 관리하지 않아도 시스템에서 알아서 관리..
DispatchQueue 클래스
→ GCD를 사용하기 위한 대기열, 대기열들에 작업을 추가해주기만 하면 시스템은 알아서 스레드를 관리해 작업을 처리함
DispatchQueue. . { }
단일 스레드 Serial → 기본값, .main
다중 스레드 Concurrent → .global()
동기 → sync
비동기 → async
main 쓰레드
앱의 생명주기를 가지고, 앱이 실행되는 동안에는 늘 메모리에 올라와 있음
전역적으로 사용 가능한 큐
동시에 여러작업 불가능
UI 작업은 메인쓰레드에서만 작업
…
[Practice] TIL 연습
제인짱
[iOS] UIWindow, makeKeyAndVisible()
UIWindow란?
View들을 담는 컨테이너
사용자 인터페이스에 배경을 제공하여 이벤트 처리 행동을 제공하는 객체
시각적인 화면을 가지고 있지 않고 기능적인 면을 담당
자동으로 Xcode가 앱의 기본 window를 제공함
(최초 iOS 프로젝트에는 스토리보드를 사용하여 앱의 View들을 정의, 스토리보드는 Xcode에서 자동으로 제공하는 AppDelegate(또는 SceneDelegate)에 window속성이 존재해야 가능)
최초 프로젝트 생성 시 스토리보드와 연결된 View를 표현하기 위해 Strong으로 UIWindow 객체가 참조되고 있는것을 확일할 수 있음.
UIWindow는 UIView의 subclass임
스토리보드가 아닌 코드로 View를 구성할 때, Window rootViewController 수정이 필요함
let codeVC = UIViewController()
window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = codeVC
window?.makeKeyAndVisible()
window.makeKeyAndVisible() 의미
keyWindow 로 설정
keyWindow가 뭐임? → window 가 여러개 존재할 때, 가장 앞쪽에 배치된 window
window의 rootViewController를 위에서 세팅하고
makeKeyAndVisivle()을 부르면 지정한 rootViewController가 상호작용을 받는 현재 화면으로 세팅 완료
[iOS] View Drawing Cycle
- View Drawing Cycle이란
뷰가 로드되거나 변경이 생겼을 때, 이를 화면에 적용시켜 그리는 과정임
이들은 즉각적으로 처리되지 않고, Main Run loop에 의존적인 형태를 가짐
![ㅇㅇㅇㅇ](https://private-user-images.githubusercontent.com/108393988/312373489-e81527b7-4097-4755-861a-a10a5d390433.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTQ2MjM5MjMsIm5iZiI6MTcxNDYyMzYyMywicGF0aCI6Ii8xMDgzOTM5ODgvMzEyMzczNDg5LWU4MTUyN2I3LTQwOTctNDc1NS04NjFhLWExMGE1ZDM5MDQzMy5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQwNTAyJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MDUwMlQwNDIwMjNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT05MjFiM2UzZmU2YzJjZWY0ODU3NWI4MWZjYTg0YjI0ZTFkY2IxNWRjNTQyMGNjYTNiMjY1Mzk5OGI3MTU4Y2UxJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.wbUWEuTK4MoRCOG2-1wneMAXsjvEcQ70UBQ4dIjBbeg)
- Main Run loop
이벤트는 각 스레드마다 존재하는 Run loop를 통해 처리됨.
발생하는 이벤트들이 모두 시스템상에 이벤트 큐에 들어가고,
Run loop가 돌아가면서 이들을 하나씩 가져와 처리하는 방식
그 중에서도 Main Run loop는 사용자의 입력 이벤트를 처리하고 적절한 응답을 트리거하는 역할을 수행함.
- Update Cycle
메인런루프의 마지막 단계에서는 Update Cycle이라는 것이 발생함.
이 Update Cycle에서는 Layout(size, Position), Display(Color, text, image…), Constraints(AutoLayout)를 업데이트하는 작업을 수행함.
이벤트들을 처리하는 과정에서 View의 변경이 요청되면 시스템은 이를 바로 처리하는 것이 아니라,
이를 체크(마킹)해 놓음.
그리고 다음에 발생하는 Update Cycle에서 체크된 요청들을 확인해서 실질적 변화를 수행함.
- draw, layoutSubviews, updateConstraints
Update Cycle 과정에서 실질적으로 변화에 대한 처리를 수행하는 것은 View 내부의 메서드임.
Display의 경우 draw
Layout의 경우 layoutSubviews
Constraints의 경우 updateConstraints
가 그 처리를 담당함.
이들은 최초로 View가 load 되어 화면에 나타날 때도 불려짐.
(최초로 load되어 그려질때도 당연히 이들 과정이 필요하기 때문)
순서
- Constrains 값을 기반으로 초기 설정된 Layout(Size, Position)을 변경 혹은 유지
- Layout 값을 기반으로 Size 및 Position 설정
- 최종 Drawing 처리
![ㅇ](https://private-user-images.githubusercontent.com/108393988/312373817-09d549fc-c04b-4d98-b563-94d7f1ec4c82.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTQ2MjM5MjMsIm5iZiI6MTcxNDYyMzYyMywicGF0aCI6Ii8xMDgzOTM5ODgvMzEyMzczODE3LTA5ZDU0OWZjLWMwNGItNGQ5OC1iNTYzLTk0ZDdmMWVjNGM4Mi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQwNTAyJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MDUwMlQwNDIwMjNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0wYWQ0NWNiNmM5ODc3NzA3YTYzZmZkY2ZiNDhiZTNjMDg5ZjVhMzEwMzYzZTZjY2UwM2RjMDM5MzA5NTFmMTU5JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.yZQ_fsx0BN0t49IYYgV48iA1vi8uzd73iq0YPbovpoU)
- setNeedsUpdateConstraints, setNeedsLayout, setNeedsDisplay
애플은 updateConstraints
, layoutSubviews
, draw
는 Update Cycle에서 수행되는 것이 더 바람직하니까 직접 사용하지 말라고 말함.
(아마 Update Cycle에서 한 번에 처리하는 것이 아니라 사용자가 직접 부르도록 하면 동시에 발생하는 다른 이벤트 등으로 인해 Update 처리가 제대로 수행되지 않을 것을 우려해서인 것 같음. 또 동시에 적용하지 않으면 고비용의 작업을 여러번 실행하면서 효율성이 떨어지는 문제)
그래서 존재하는 것이 setNeedsUpdateConstraints
, setNeedsLayout
, setNeedsDisplay
같은 업데이트 요청 메서드들임.
이들은 각각 Constraints, Layout, Display에 대한 업데이트를 명시적으로 예약하는 역할을 함
이들이 호출되면 시스템은 이를 체크해뒀다가
다음 Update Cycle 단계에서 updateConstraints
, layoutSubviews
, draw
를 호출.
직접적으로 setNeedsUpdateConstraints
, setNeedsLayout
, setNeedsDisplay
와 같은 메서드를 호출하지 않아도
- View의 크기 변경
- SubView 추가
- ScrollView에서 Scroll 발생
- 디바이스 회전으로 인한 화면표시 모드변경 (가로, 세로)
- View의 Constraints 변경
과 같은 상황에서는 setNeedsLayout
, setNeedsDisplay
가 자동으로 트리거됨.
그래서 지금까지 setNeedsLayout
, setNeedsDisplay
를 직접적으로 호출하지 않아도 UI 업데이트가 수행되었던 것임.
- updateConstraintsIfNeeded, layoutIfNeeded
사실 Update Cycle을 기다리지 않고 바로 View를 업데이트할 수 있는 방법이 있음.
바로 updateConstraintsIfNeeded
혹은 layoutIfNeeded
메서드를 호출하는 것.
해당 메서드를 사용하면 Update Cycle와 관계없이 각각 곧바로
updateConstraints
, layoutSubviews
를 실행시킴.
특히 layoutIfNeeded
는 Contraints를 통해 애니메이션을 구성하고 싶을 때 유용하게 사용할 수 있음.
단순히 Contsraint를 변경하는 애니메이션을 만들 경우 결국 이에 대한 처리는 Update Cycle에서 처리되기 때문에 애니메이션 효과를 줄 수 없습니다.
하지만 Constraints 변경후 layoutIfNeeded
메서드를 애니메이션 코드에 추가하면 layoutIfNeeded
가 순차적으로 처리되면서 애니메이션을 확인할 수 있게 됩니다.
[Swift] 클로저와 메모리관리
클로저와 값 캡쳐
- 클로저는 내부 함수와 주변 환경의 값(변수, 상수)을 포함하는 객체.
- 클로저 내부에서 외부 변수를 사용하면, 해당 변수의 값을 캡쳐함. 이것을 "값 캡쳐"라고함.
- 예를 들어, 클로저 내부에서 외부 변수
num
을 사용하면, 클로저에 의해num
의 값이 캡쳐됨.
클로저의 값 캡쳐 방식
- 클로저는 값을 캡쳐할 때 Value/Reference 타입에 관계 없이 Reference Capture(참조 캡쳐)를 함.
- 예를 들어, Int 타입의 구조체(값 타입)를 사용하는 경우에도 클로저는 Reference Capture를 함.
- 따라서 클로저에서 외부 변수의 값을 변경하면 외부 변수도 변경됨.
클로저의 캡쳐 리스트 (Capture Lists)
- 클로저의 시작
{
바로 옆에[]
를 사용하여 캡쳐할 멤버(변수 또는 상수)를 명시하고,in
키워드를 작성함. - 캡쳐 리스트를 사용하면 Value Type의 값을 복사해서 Capture(캡쳐)할 수 있음.
- 예를 들어, Value Type 변수를 캡쳐하고 싶다면 Capture Lists를 사용하고
weak
또는unowned
를 이용하여 캡쳐함.
let closure = { [num] in
클로저와 ARC (Automatic Reference Counting)
- 클로저와 인스턴스 간의 관계에서 강한 순환 참조(Strong Retain Cycle)가 발생할 수 있음.
- 이를 해결하기 위해 Capture Lists를 사용하여
weak
또는unowned
를 이용하여 강한 순환 참조를 방지할 수 있음.
클로저(Closure)는 Swift 언어에서 기능적인 코드 블록을 의미하며, 함수와 유사한 역할을 함.
클로저는 자신이 정의된 환경을 캡처하고 저장할 수 있는 특징이 있음.
이 때문에 클로저를 사용할 때 주의가 필요하며, 특히 Automatic Reference Counting (ARC)와 함께 사용될 때 메모리 관리에 대한 이해가 중요함.
1. 클로저와 메모리 관리
클로저는 자체적인 독립적인 블록이기 때문에 주변 환경의 변수나 상수를 캡처하여 사용할 수 있음. 이때, 클로저가 주변 환경의 값을 캡처하면서 생기는 순환 참조 문제를 이해해야 함.
순환 참조는 두 객체가 서로를 강하게 참조하고 있어 더 이상 사용되지 않아도 메모리에서 해제되지 않는 상황을 의미함.
클로저에서는 이런 순환 참조 문제가 발생할 수 있음.
2. 예시 코드
아래는 간단한 클로저와 클래스를 사용한 예시 코드
class Person {
var name: String
var greetingClosure: (() -> Void)?
init(name: String) {
self.name = name
// 순환 참조 발생 가능한 클로저
self.greetingClosure = {
print("Hello, \(self.name)!")
}
}
deinit {
print("\(name) is being deinitialized")
}
}
var person: Person? = Person(name: "John")
// 순환 참조 발생
person?.greetingClosure?()
person = nil
이 코드에서 주목해야 할 부분은 greetingClosure
클로저임.
이 클로저는 self
(즉, Person
인스턴스)를 캡처하고 있음.
그런데 이 클로저가 Person
인스턴스를 강하게 참조하고 있기 때문에, Person
인스턴스를 nil로 설정해도 deinit 메시지가 출력되지 않음.
3. 순환 참조 해결 방법
3.1. [weak self]를 사용한 해결
class Person {
var name: String
var greetingClosure: (() -> Void)?
init(name: String) {
self.name = name
self.greetingClosure = { [weak self] in
print("Hello, \(self?.name ?? "")!")
}
}
deinit {
print("\(name) is being deinitialized")
}
}
var person: Person? = Person(name: "John")
// 순환 참조 해결
person?.greetingClosure?()
person = nil
[weak self]
를 사용하여 클로저 내에서 self
를 약한 참조로 캡처함으로써 순환 참조 문제를 해결할 수 있음.
3.2. [unowned self]를 사용한 해결
class Person {
var name: String
var greetingClosure: (() -> Void)?
init(name: String) {
self.name = name
self.greetingClosure = { [unowned self] in
print("Hello, \(self.name)!")
}
}
deinit {
print("\(name) is being deinitialized")
}
}
var person: Person? = Person(name: "John")
// 순환 참조 해결
person?.greetingClosure?()
person = nil
[unowned self]
를 사용하여 클로저 내에서 self
를 약한 참조가 아닌 미소유(unowned) 참조로 캡처함으로써 순환 참조 문제를 해결할 수 있음.
📖
클로저와 ARC는 함께 사용될 때 메모리 관리에 신경을 써야 하는 중요한 주제임.
순환 참조 문제는 특히 클로저에서 주의해야 할 부분 중 하나이며,
[weak self]
나 [unowned self]
와 같은 캡처 리스트를 통해 이를 효과적으로 관리할 수 있음.
[Swift] Any/Anyobject
Any와 AnyObject
타입 캐스팅을 수행할 때 일반적으로는 상속 관계에 있는 클래스끼리만 캐스팅 가능.
업캐스팅(as), 다운캐스팅(as? as!)
범용타입인 Any와 AnyObject 타입을 사용할 경우, 상속관계에 있지 않아도 타입 캐스팅을 할 수 있음
Swift는 Type에 민감한 언어임
배열을 Int형 배열로 선언했다면..
var nums: [Int] = []
nums.append(1)
nums.append(1.0) // Cannot convert value of type 'Double' to expected argument type 'Int'
nums.append("�hong") // Cannot convert value of type 'String' to expected argument type 'Int'
nums.append(false) // Cannot convert value of type 'Bool' to expected argument type 'Int
Int형 외의 타입은 절대 저장못함
하지만 Any를 사용한다면 모든 타입을 저장할 수 있도록 한다
var things: [Any] = []
things.append(1)
things.append(1.0)
things.append("Hong")
things.append(false)
things.append(Human.init()))
things.append({ print("I am Hong!") })
이렇게 Any 타입으로 설정하면 Value 타입, Reference 타입 모두 상관없이 사용가능!
Any는 구조체, 열거형, 클래스 어떤 타입이든 모두 허용함..
하지만 AnyObject는 한층 더 까다로워짐!
var things: [AnyObject] = []
things.append(1) // Argument type 'Int' expected to be an instance of a class
things.append(1.0) // Argument type 'Double' expected to be an instance of a class
things.append("Hong") // Argument type 'String' expected to be an instance of a class
things.append(false) // Argument type 'Bool' expected to be an instance of a class
things.append(Teacher.init()))
things.append({ print("I am Hong!") }) // Argument type '()->()' expected to be an instance of a class
타입이 AnyObject로 선언될 경우, 클래스 타입만 저장 가능함
클래스 타입인 Teacher 인스턴스를 제외하고는 모두 에러뜨는걸 볼 수 있음
Any와 AnyObject의 타입 캐스팅
as는 업캐스팅 혹은 패턴 매칭에 쓰임
for thing in things {
switch thing {
case _ as Int:
print("Int Type!")
case _ as Double:
print("Double Type!")
case _ as String:
print("String Type!")
case _ as Human:
print("Human Type")
case _ as () -> ():
print("Closure Type")
default:
print("something else")
}
}
이렇게 switch문을 활용해서,
해당 타입일 경우(캐스팅에 성공한 경우), case문이 실행됨
Any와 AnyObject가 모든 타입을 저장할 수 있어서 편해보이지만,
불편한 점도 많다!
var name: Any = "Hong"
이건 누가봐도 String 타입인데 name에 String 메서드나 프로퍼티에 접근하려고 하면
아무 자동완성도 나오지 않음..
왜냐,
Any와 AnyObject는 런타입 시점에 타입이 결정되기 때문
따라서, Any 타입인 name을 String으로 사용하고 싶다면
if var name = name as? String {
name.append("gnorm")
}
as 혹은 as? 를 통해, 다운캐스팅 한 후에 사용해야함
[Swift] Override
오버라이딩
메서드 오버라이딩?
상속받은 인스턴스&타입 메서드를 재정의하여, 하위클래스 내에서 해당 메서드를 원하는 대로 구현할수있다.
class Human {
func description() {
print("사람입니다.")
}
}
class Student: Human {
}
let hong: Student = .init()
hong.description() // 사람입니다.
Student 클래스 인스턴스로 description이란 메서드에 접근에 접근하면 “사람입니다.” 가 실행됨
“학생입니다.”를 출력되게 하려면
class Student: Human {
override func description() {
print("학생입니다.")
}
}
이렇게 override 키워드를 붙여주면 오버라이드, 즉 ‘재정의’가 가능함.
let hong: Student = .init()
hong.description() // 학생입니다.
오버라이딩(재정의)된 메서드는 슈퍼클래스에서 작성된 메서드가 실행되지 않고 재정의 한 메서드가 실행됨.
→ 오버라이딩(재정의)이란 슈퍼 클래스의 메서드를 재정의해서 사용할 수 있다.
오버라이딩 메서드를 작성한 서브 클래스에서 슈퍼 클래스의 메서드를 사용하고싶으면?
class Student: Human {
override func description() {
super.description() // 사람입니다.
}
}
super를 사용하면, 슈퍼 클래스에 접근할 수 있다.
이렇게 슈퍼 클래스의 메서드도 오버라이딩 된 메서드도 모두 실행 가능함.
주의할점? (추가필요…)
동기(sync)한 환경에서 코드의 실행순서는 LIFO이기 때문에,
class Student: Human {
override func description() {
print("학생입니다.")
super.description() // 사람입니다.
}
}
이렇게 super를 마지막에 불러주면 당연히 마지막에 실행된다.
→ 슈퍼클래스의 메서드를 부르는 작업을 꼭 맨처음에 해야하는 것은 아니다.
override란 키워드를 붙이면 컴파일러는 해당 정의가 슈퍼 클래스에 있는지 확인하는 작업을 함.
(오버라이딩이 맞는지 아닌지)
존재하지 않는 메서드에 override란 키워드를 붙이면 에러발생함.
프로퍼티 오버라이딩?
상속받은 프로퍼티를 오버라이딩하여 해당 속성에 대한 getter, setter를 제공하거나,
상속받은 프로퍼티 값의 변경을 추적할 수 있도록 옵저버를 추가함.
→ 프로퍼티를 오버라이딩 한다는 것은 getter, setter를 추가할 수 있다.
즉, ‘연산 속성’을 추가할 수 있고 프로퍼티 옵저버를 추가할 수 있다..
연산 속성 추가하기
프로퍼티를 오버라이딩 할려면 ‘프로퍼티 이름’과 ‘타입’을 반드시 명시해야함.
저장 프로퍼티
프로퍼티의 경우 ‘연산’ 속성을 추가하는 것만 가능함..
저장 프로퍼티에 ‘저장’ 속성을 추가하는 오버라이딩이 가능할까?
class Human {
var name = "Hong"
}
class Student: Human {
override var name: String = "Hong2" // ❌ 에러
}
안됨!
그렇다면 상속받은 저장 프로퍼티에 ‘연산’ 속성인 get, set을 추가하는 오버라이딩은 가능할까?
class Student: Human {
var alias = "HHong"
override var name: String { // ❌ 에러
return self.alias
}
}
read-only 프로퍼티로 오버라이딩 할 수 없음.
무슨말이냐..
슈퍼클래스 Humane클래스에서 name이란 프로퍼티는 ‘저장 프로퍼티’여서
기본적으로 getter와 setter가 모두 제공되는 읽기&쓰기가 가능한 프로터티임.
그런데 Student란 서브 클래스에서 name이란 프로퍼티는 getter만 가능하게 오버라이딩 해버리니까 ‘읽기/쓰기 모두 가능한 애인데 왜 서브 클래스 너가 읽기만 가능하게 제한을 둠?’ 하고 에러가 나는거.
따라서, 저장 프로퍼티를 오버라이딩 하고 싶다면,
class Student: Human {
var alias = "HHong"
override var name: String {
get {
return self.alias
}
set {
self.alias = newValue
}
}
}
이렇게 getter / setter 를 모두 구현해주면 오버라이딩이 된다.
let student: Student = .init()
student.name // HHong
실행해보면 오버라이딩 된 alias의 값인 “HHong”이 나오는걸 볼 수 있다.
연산 프로퍼티
저장 프로퍼티의 경우 getter / setter가 이미 구현되어 있는데..
오버라이딩 해서 getter만 구현하는 것은 안된다고 했는데
연산 프로퍼티 또한 마찬가지
class Human {
var name = "Hong"
var alias: String {
return self.name + "노움"
}
}
이렇게 Human 클래스에 alias라는 연산 프로퍼트가 getter로만 구현된 경우
class Student: Human {
override var alias: String {
return self.name + "안녕"
}
}
class Student: Human {
override var alias: String {
get {
return self.name + " 안녕"
}
set {
self.name = newValue
}
}
서브클래스에서 위와 같이 getter만 구현 하거나 setter를 추가 구현하는 오버라이딩이 가능
하지만, 연산 프로퍼티가 getter / setter 로 구현된 경우, 서브클래스에서 getter만 구현하는 오버라이딩은 안됨.
class Student: Human {
override var alias: String { // Cannot override mutable property
return self.name + " 안녕" // ❌
}
}
프로퍼티 옵저버 추가하기
저장 프로퍼티의 경우, var로 선언된 프로퍼티만 오버라이딩으로 옵저버를 추가할 수 있음.
연산 프로퍼티의 경우, getter / setter 가 모두 구현된 경우만 오버라이딩으로 옵저버를 추가할 수 있음.
프로퍼티 옵저버 TIL 작성 후 링크 달기..
저장 프로퍼티
var로 선언된 경우만 가능
프로퍼티 옵저버의 경우 “값이 변경”될 때를 알려주는 것..
상수(let)이면 값이 바뀔 일 없기 때문에 쓸 이유가 없음.. → 변수(var)일 때만 가능
class Human {
var name = "Hong"
}
class Student: Human {
override func name: **String** {
willSet {
print("name 값 변경될꺼임. \(newValue)")
}
didSet {
print("name 값 변경됐다. \(oldValue)")
}
}
}
이렇게 슈퍼 클래스의 변수 저장 프로퍼티를 서브클래스에서 오버라이딩 해서 프로퍼티 옵저버를 추가할수있음.
let student: Student = .init()
student.name = "노움"
// name 값 변경될꺼임. 노움
// name 값 변경됐다. Hong
오버라이딩해서 추가한 프로퍼티 옵저버가 잘 동작함.
주의!
컴파일러는 이 alias가 오버라이딩 되는 게 맞는지 확인하기 위해서
프로퍼티를 오버라이딩시에는 “프로퍼티 이름”, “타입”이 반드시 명시되어야함!
override func name: **String** { ... }
슈퍼클래스의 프로퍼티가 타입 추론에 의해 타입이 명시 되어 있지않아도
프로퍼티를 오버라이딩 할 경우엔 타입을 반드시 명시해야함
안하면 타입 명시하라고 에러남.
연산 프로퍼티
getter만 구현된 경우 setter(값변경)이 호출될일 없음 → 옵저버를 왜 붙임?
→ getter / setter 가 모두 구현된 연산 프로퍼티만 프로퍼티 옵저버를 붙일 수 있음..
class Human {
var name = "Hong"
var alias: String {
get {
return name + " 안녕"
}
set {
self.name = newValue
}
}
}
class Student: Human {
override var alias: String {
willSet {
print("연산 프로퍼티 실행될거임")
}
didSet {
print("연산 프로퍼티 실행됐다")
}
}
}
실행해보면..
let student: Student = .init()
student.alias = "노움"
// 연산 프로퍼티 실행될거임
// 연산 프로퍼티 실행됐다
잘 동작함.
오버라이딩 금지! final
오버라이딩이 가능한 프로퍼티, 메서드, 서브스크립트 등에 final 키워드를 붙이면 더이상 오버라이딩이 불가능함.
class Human {
**final** var name = "Hong"
**final** func description() {
print("홍홍홍")
}
}
이렇게 final 키워드를 붙이면..
class Student: Human {
override func name: String { ... // ❌ Property overrides a 'final' property
}
class Student: Human {
override func description() { ... // ❌ Instance method overrides a 'final instance..
}
서브 클래스에서 오버라이딩 불가능함..
final을 붙인다고 오버라이딩이 금지된 것이지 접근자체가 금지된거는 아님!!!
let student: Student = .init()
student.name
student.description()
final이 붙어도 서브클래스에서 접근가능
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.