DI, 의존성 주입
주니어 개발자가 정말 많이 접하는 단어이며, 개념이 추상적이어서 머리속에 개념이 확립되지 않는 것. - 나무

실제로 개발자로 취업하기 전부터 DI에 대해 많이 찾아보았고, 이해하려 노력하였다.

검색을 하다보면 DI의 장점들이 적혀있다.

  1. 모듈간의 결합도를 낮춘다
  2. 유지보수가 용이하다
  3. 테스트에 용이하다

등등 많은 장점들이 나오고 "이런 장점이 있기 때문에 사용하는구나" 정도로 이해하였다.

 

그러다가 한 영상을 찾게 되었고, 영상을 통해 느낀점을 적어보려 한다.

 

의존성 주입 (Dependency Injection)

객체에게 필요한 의존성을 주입하는 행위

-> 어떤 객체가 필요한 것들을 전달하는 행위

(화살표는 내가 쉽게 이해하기 위해 바꾼 문장이다.)

 

전달한다 = 누군가에게 받는다 = 나에게는 없는 것을 받는다

결국, 필요한 것들을 내가 직접 생성하는 것이 아닌, 다른 누군가에게 전달받는다

 

Example

class Car {
	var handle: Handle?
}
// 1. Car Class 내부에서 직접 생성하는 경우

class Car {
	var handle: Handle? = Handle()
}
// or
class Car {
	var handle: Handle?
    
    init(){
    	self.handle = Handle()
    }
}

// 2. Car Class 내부에서 직접 생성하지 않고 전달받는 경우

class CarFactory {
	var car: Car = Car()
    
	func setHandle() {
		self.car.handle = Handle()
    }
}

이런 식으로 의존성을 주입한다. (끝)

그래서 의존성 주입에 대한 정의는 알겠는데, 그래서 이걸 써서 얻는 장점인

  1. 모듈간의 결합도를 낮춘다
  2. 유지보수가 용이하다
  3. 테스트에 용이하다

이것들은 어떻게 느낄 수 있는가?

 

 

의존성 낮추기

protocol HandleType {
	func honk() // 경적을 울리다
}

final class AHandle: HandleType {
	func honk() {
    	print("뿌뿌")
    }
}

final class CHandle: HandleType {
	func honk() {
    	print("뛰뛰")
    }
}

final class BHandle: HandleType {
	func honk() {
    	print("빵빵")
    }
}

이처럼 핸들의 종류는 수도 없이 많을 것이며, 서로 다른 기능/책임을 갖고 있을 수 있다.

 

하나의 Car 객체는 Handle이 필요하다.

class Car {
	var handle: HandleType?
}

이때, HandleType Protocol 타입을 프로퍼티로 선언하여 내부 handle 프로퍼티를 선언하였다면

handle은 A, B, C Handle 모두 사용이 가능하다.

 

이때, CarFactory에서 car.handle = AHandle() 처럼 프로퍼티 주입을 사용할 수 있는데

이렇게 됐을 때, Car 클래스는 AHandle에 대한 의존성을 갖지 않게 된다.

-> 다시 말해, Car 클래스는 Handle 이라는 protocol에 대한 소스코드 의존성이 생겼을 뿐

-> AHandle Class 에 대한 소스코드 의존성이 생기진 않는다.

-> 또한, 소스코드 의존성은 빌드시간에 영향을 미치기 때문에 

 

-> 사실 이런 protocol을 사용하는 건 DI에 항상 연관되어서 나오는데

-> DI만을 따지자면 위에서 말한 "어떤 객체가 필요한 것들을 전달하는 행위" 뿐이다
-> DI와 protocol을 합쳐서 의존성(필요한 것)을 주입하고 소스 코드 의존성을 줄이는 것을 DI라고 하는 것으로 이해했다.

 

DI = 필요한 것을 주입하는 행위 + Procotol을 이용한 소스코드 의존성 감소

 

CarFactory, AHandle, BHandle, CHandle이 각각 다른 모듈에 있다고 가정해보자.

각각의 핸들을 교체할 때마다 해당 Handle 파일이 있는 모듈을 import해야 할 것이며

이는 빌드시간에 그대로 영향을 미친다.

 

이를 Protocol을 사용하여 사용하는 모듈에서 의존성 주입을 통해 주입하게 되면

이때 발생하는 개념이 "IoC (Inversion of Control)" 즉, 제어의 역전이다.

 

// 1. DI X

// 비즈니스 로직 수행 모듈 A

import B
...
...
let service = ServiceImpl()
let info = service.getInfo()


// 구현체 모듈 B
class ServiceImpl {
	func getInfo() -> Info {
    	...
    }
}


// 2. DI

// 비즈니스 로직 수행 모듈 A
// import B -> 사라짐
....
.....
init(service: Service) {
	self.service = service
}

....

let info = service.getInfo()


protocol Service {
	func getInfo() -> Info
}



// 구현체 모듈 B

import A // -> 생김

class ServiceImpl: Service {
	func getInfo() -> Info {
    	return info
    }
}

예제를 통해서 보면 1번 예제에서는 모듈 A에서 모듈 B를 import하여서 사용하였고

2번 예제에서는 DI를 통해 전달받기 때문에, 모듈 A에서 모듈 B를 import하지 않고

모듈 B에서 모듈 A를 import하였다.

 

즉, 상위 모듈이 하위 모듈(구현체)을 import 해야 하는 의존방향에서 역전이 된 것이다.

그렇다고 코드 호출 방향이 달라지는 것은 아니다.

여전히 상위 모듈(비즈니스 로직 수행)에서 하위 모듈(구현체)의 기능을 호출한다.

 

 

IoC = 방향이 같던 "코드 호출방향"과 "소스코드 의존성" 에서 "소스코드 의존성"의 방향을 반대로 뒤집어
하위모듈에 대한 상위모듈의 의존성을 역전시킨 것

 

장점

  • 확장과 재사용성
    • 새 기능 개발 용이
    • 기존 기능 수정 수월
    • 모듈별 독립적인 재사용 가능
  • 유지보수성
    • 영향 범위 파악이 쉬움
    • 빌드 시간 단축
  • 테스트 용이성
    • 테스트 대역으로 치환 용이

단점

  • 코드가 복잡해져 이해하기 어려워짐
    • 호출들이 protocol로 연결되어 있어서 호출을 따라가기가 힘들어짐
    • 디버깅이 어려워짐

 

 

 

 

 

 

 

 

 

스위프트 타입 시스템

타입 시스템은 프로그래밍 언어 작성 방식과 프레임워크의 구조를 결정하는 매우 중요한 요소이다.
스위프트를 비롯한 함수 중심 언어 의 타입 시스템은 Objective-C와 코코아 프레임워크에서 사용하는 타입 시스템보다 안전하고 세밀하다.

타입 시스템

스위프트는 JS, Python처럼 Duck Type 시스템은 아니지만 명시적인 시스템이다. Objective-C 처럼 모든 객체가 Dynamic 타입은 아니지만, 프로토콜 타입을 활용하여 Dynamic하게 확장하면서도 Objective-C보다 안전하게 쓸 수 있다.

스위프트 타입

스위프트에는 크게 두 종류의 타입이 있다.

  • 이름있는 타입 : Named Type
  • 이름없는 타입 : Compound Type (int, char 등 Primitive Type)

합쳐진 타입은 Tuple이나 클로저/함수 타입으로 이름이 따로 정해지지 않고, 다른 타입들을 합쳐서 사용하는 타입이다. ex) (Int, (Int) -> (Int))

타입 검사

스위프트는 안전한 타입 언어를 표방한다.
"안전한 타입 언어"

값에 대한 타입을 명확하게 구분해서 사용할 수 있는 언어
컴파일러가 다른 타입으로 선언한 변수에 값을 전달하는 것을 미리 방지

컴파일 동안 안전한 타입 사용을 위해, 타입 검사를 진행한다.

타입 추론

타입 검사는 값에 대한 타입을 다르게 사용할 경우 컴파일 에러를 표시한다.
그렇다고 모든 변수를 선언할 때, 타입을 명시해야하는 것은 아니다. -> 타입 추론 덕분

func foo(x: Double) -> Int {...}
var doubleValue: Double = 3.141592
var unknown = foo(doubleValue)

func bar<T>(x: T) -> T {return x}
var floatValue: Float = -bar(1.414)

코드는 스위프트 타입 추론이 양방향으로 가능하다는 것을 보여준다.

foo() 함수의 타입 정의를 보면 리턴 타입이 Int라는 것을 유추할 수 있다.
따라서 foo() 함수 리턴값을 저장하는 unknown 변수는 Int 타입이다.
bar() 함수는 제네릭 타입으로 타입이 명시되지 않았지만 floatValue 변수의 타입이 Float라서 Float 타입으로 동작한다.

스위프트 타입 검사는 기존의 Objective-C처럼 명시적으로 타입을 선언한 정보를 근거로 타입 정보를 만드는 것도 가능하다.

스위프트 타입 추론은 3단계로 진행된다.

  1. "제약 만들기"
  2. "제약 계산하기"
  3. "제약 판단하기"

-> 스위프트 오픈소스 TypeChecker.rst 문서를 참고

타입 변환(Type Cast)

타입 변환은 종류가 전혀 다른 타입끼리 타입을 바꾸는 것이 아니다.
비슷한 종류끼리만 타입을 바꾸는 것을 의미한다.

"타입의 종류가 같다" -> 수학에서 구조 동일성을 가지는 벡터와 좌표 시스템처럼
데이터 타입의 메모리 구조가 동일하고 다루는 소재가 다른 타입끼리만 타입을 바꿀 수 있다.

ex.1) String 과 Int 는 구조가 다른 타입이기 때문에 타입 변환이 불가능하다.
ex.2) Struct 타입이나 Class 타입에서 상속받은 객체들끼리는 구조 동일성이 유지되기 때문에 타입 변환 가능.
ex.3) 숫자를 표시하는 타입들은 구조가 동일하기 때문에 서로 전환이 가능하다. -> 다만 값에 대한 손실이 발생할 수 있는 경우에는 반드시 타입을 지정해야 한다.

의미 있는 값 vs 의미 있는 레퍼런스

"의미 있는 레퍼런스"

레퍼런스 방식으로 참조하는 대상(대부분 객체 인스턴스)이 중요하다는 것

"의미 있는 값"

값 자체가 중요하다는 것

스위프트는 의미 있는 값으로 쏠려있다.

FP에서는 함수에서 다루는 변수가 레퍼런스가 아니고 불변 변수여야만 부작용이 없다.
따라서 값 자체를 다루는 것이 더 의미 있다.
값 방식은 Reference Count를 하지 않기 때문에 그만큼 병렬 처리나 성능 최적화 측면에서 유리하다.

Objective-C 에서는 클래스 객체를 사용하는 경우에만 의미 있는 레퍼런스를 사용하고, C 언어와 호환하기 위한 내장 타입들은 그대로 C 언어 방식(의미 있는 값)을 사용한다.
C 언어 타입은 컬렉션에 넣지 못하기 때문에, 메모리 관리가 안되고, 타입 변환이 불편해서 객체와 함께 사용하기에 불편하다.

타입별 성능 비교

대표적인 의미 있는 값 방식 구조체 타입과 의미 있는 레퍼런스 방식 클래스 타입, 프로토콜 타입에 대해서 메모리 공간, 참조 계산, 메서드 디스패치 동작을 비교하면 표와 같다.

스택 메모리의 경우, 사용할 때 SP(스택 포인터)를 증가시키고 사용하지 않을 때 감소시키면 된다.
반면에 힙 메모리를 사용할 경우, 비어있는 힙 공간을 찾고 처리를 위한 별도의 데이터 구조가 필요다핟.
여러 스레드에 대한 안정성 확보를 위한 동작이 필요할 경우를 비교하면 힙이 상대적으로 느리다.
프로토콜 타입으로 확장하는 경우에도 세 워드(64bit 기준 24bit)보다 작은 크기 값은 스택만 사용하지만, 그보다 크면 힙 공간을 추가로 사용하기 때문에 느려질 수 있다.

정적 디스패치는 컴파일 시점에 함수의 메모리 주소를 찾아두기 때문에, 런타임에는 해당 주소로 바로 이동한다.
특정 조건에서는 컴파일러가 속도 향상을 위해 인라인에 코드를 그대로 복사하기도 한다.
반면에 동적 디스패치는 런타임에 구현 함수 목록에서 함수 메모리 주소를 찾아 이동해야 한다.

요약

  • 어떤 타입을 사용할 지 결정하고, 타입에 적합한 메모리 관리 방식에 대한 고민이 프로그램 구조에 영향을 준다.
  • 스위프트는 다양한 타입을 지원하기 때문에, 선택의 폭이 넓다. 그것은 개발자의 책임이 큰 것이다.

열거 타입

C 언어나 Objective-C 언어에서 열거타입(enumeration)은 단순히 정수 타입 값을 나열하는 편의를 위한 것이다.
스위프트에서는 문자열 타입도 지정가능하고, 실수 타입도 지정할 수 있다.
뿐만 아니라 모든 값이 있을 필요도 없고, 모두 다 같은 타입이 아니어도 된다.
클래스처럼 함수를 만들 수도 있고 확장도 가능하다.

열거 타입과 프로토콜

열거 타입에 정의한 값은 기본적으로 Hashable 프로토콜을 지원해야 한다.

Hashable

public protocol Hashable: Equatable {
    var hashValue: Int {get}
}

Hashable 프로토콜을 Equatable 프로토콜을 상속받아 만들어져서, 추가적으로 Equatable 프로토콜에 있는 == 비교 함수도 구현해야 한다.

Equatable

public protocol Equatable {
    @warn_unused_result
    func == (lhs: Self, rhs: Self) -> Bool
}

열거 타입에서 일반적으로 동일한 타입 값을 사용하는 경우

enum PenModels{
    case BallPen
    case NamePen
}
  • 열거타입은 내부적으로 분기 처리를 하는데, 위에서부터 순서대로 비교하여 값을 할당한다.
    • 자주 사용하는 case를 가장 위에 두는 것도 하나의 방법?

프로토콜 타입과 증거 테이블

클래스 타입에 대한 상속과 다형성은 가상 함수들을 런타임에 찾는 다이내믹 디스패치 방식을 사용한다.
하지만 다른 타입들은 프로토콜 중심 프로그래밍 방식에 맞춰서 프로토콜 증거 테이블을 사용해서 다형성을 구현한다.
어느 모듈의 특정 타입에 대한 프로토콜 구현 함수 이름을 프로토콜 증거 테이블에서 바로 찾아 호출할 수 있다.

ex)

protocol Drawable{
    func draw()
}

struct Point: Drawable{
    var x, y: Double
    func draw(){...}
}

struct Line: Drawable{
    var x1, y1, x2, y2: Double
    func draw(){...}
}

var drawables: [Drawable]
for d in drawables{
    d.draw()
}
// 출처 : https://zeddios.tistory.com/597

d.draw()는 어떤 draw 메소드를 호출해야 하는가?
PWT를 사용하여 맞는 draw() 메소드 호출하게 함

변수를 포함하는 프로토콜을 컴파일하면 PWT(Protocol Withness Table)와 함께 VWT(Value Withness Table)도 만들어진다.
VWT는 의미있는 값을 가지는 타입에 대한 기본적인 동작을 다루는 생성, 복사, 파괴, 해제 함수들에 대한 참조 테이블이다.
VWT와 PWT 증거 테이블은 그림과 같이 값을 저장하는 저장소 데이터 구조를 참조한다.

값 크기가 버퍼크기보다 작으면 좌측 첫번째 구조처럼 스택공간을 그대로 저장한다.
만약 값 크기가 버퍼 크기보다 크면 좌측 두번째 구조처럼 힙에 큰 데이터 구조를 생성하고, 버퍼에는 힙 공간의 주소를 저장한다.
따라서 프로토콜 타입에서 스택만 사용하는 의미 있는 값을 사용하려면 버퍼보다 작은 데이터 구조를 사용해야 한다.

Equatable 프로토콜

Hashable 프로토콜과 마찬가지로 Hashable 프로토콜이 상속받은 Equatable 프로토콜에 대한 == 비교 함수도 동일하게 만들어진다.
== 비교 함수는 좌우에서 각각 .PenModels 파라미터를 받아서, 좌측 값에 대한 case 비교문 Int 값과 우측 값에 대한 case 비교문 Int 값을 구한다.
그리고 Int 타입의 == 비교함수를 통해서 최종적으로 같은 값인지 판단한다.
-> == 를 사용해서 enum 타입들을 비교할 때, lhs와 rhs 모두 Enum 분기처리를 통해 Int 값을 받아온 후, 가져온 Int 값을 통해 비교한다.

연관 값을 가지는 열거 타입

열거 타입에는 다른 언어에 있는 variants나 unions 형태로 여러 타입에 대한 값이 있을 수 있다.
이런 값을 열거 타입 연관 값이라고 한다.

enum PatientId{
    case socialNumber(String)
    case registeredNumber(Int)
}

var temporaryPatient = PatientId.registeredNumber(1550)

이런 경우는 열거 타입이지만, Hashable이나 Equatable 프로토콜을 구현하는 내부 함수는 만들어지지 않는다.
왜냐하면 case 구문으로 값이 같은지 비교하지 않더라도, 특정한 값을 바로 적용하기 때문이다.

가공 없는 값을 가지는 열거 타입

열거 타입에 특정 타입을 지정해서 가공 없는 값(Raw Value)을 할당하는 방식도 흔히 사용한다.

가공 없는 값을 갖는 열거 타입의 경우는 Grade 타입처럼 열거 타입 생성자가 만들어진다.
가공 없는 값을 전달하면 열거 타입 값들과 비교한다.
열거 타입과 매칭이 되면 값이 들어가고, 매칭이 되지 않으면 null을 할당하기 떄문에 enum.Grade? 타입을 리턴한다.
특이한 점은 스택에 만든 로컬 변수를 비교할 때, == 연산 함수를 사용하지 않고 ~= 연산 함수를 사용한다는 것이다.

** ~= vs == **

~=는 범위 지정이 가능, 패턴 매칭 가능
case문에서 사용되는 것 같음

ex)

switch point{
    case (0, 0): // ~= 연산자를 사용해서 패턴 매칭
        return true
    default: 
        return false
}

요약

다른 언어에서 열거타입은 편의를 위해 상수를 선언하는 타입이었지만, 스위프트에서 열거타입은 패턴 매칭과 함께 확장 가능한 데이터 구조 타입이다.
열거 타입은 구조체 타입과 같이 의미 있는 값타입이다.
Optional, Process, Bit 타입등이 열거 타입의 예 이다.


구조체 타입

스위프트 표준 라이브러리는 대부분 구조체 타입을 기반으로 만들어졌다.
그만큼 구조체 타입은 스위프트에서 가장 핵심적인 타입 중 하나다.
스위프트로 프로그래밍을 한다면 클래스보다 구조체를 사용하는 것이 더 효율적이다.

구조체 타입

Q)구조체는 C언어의 구조체와 Objective-C의 클래스 중 어디에 가까울까?

struct Car {
    let model = "apple"
}

스위프트 구조체 타입은 클래스와 비슷하게 LifeCycle을 가지는 타입이다.
생성자인 init() 초기화 함수가 만들어진다.
그리고 model 변수 속성이 불변이기 때문에, getter 내부적으로 함수가 만들어진다

init() 함수는 구조체를 위한 메모리 박스를 할당한 다음, 내부 변수 타입인 String 타입 초기화 함수를 사용해 "apple"를 지정한다.
그리고 이 값을 model 변수 위치에 저장한다.

가변 변수가 포함된 경우

struct Car{
    var driver = "tree"
}

앞서 살벼본 init() 함수와 별도로 init(driver: String) 함수가 추가된다. -> 자동 생성되는 init에서 let, var인 내부 변수의 차이

driver 변수에 대한 초기값을 지정해서 객체를 초기화 할 수 있는 추가 초기화 함수를 추가해준다.

따라서

let myCar = Car(driver: "tree") 처럼 초기화 값을 넘겨 초기화 가능하다.

그런데 init(driver: String) 구현이 독특하다.
구조체 타입을 초기화하기 위해 내부에서 init() 함수를 부르는 게 아니라, driver 초기 값과 함께 ㄴtruct $Car 명령을 실행하고 반환받은 값을 그대로 리턴한다.
-> 이 부분은 마치 C++ 구조체 초기화 함수처럼 구조체 내부 변수에 대한 초기값을 순서대로 전달해서 구조체 메모리를 초기화하는 방식과 비슷하다.
-> init() 함수를 부르지 않는다는 점을 기억하자

우선 driver 변수에 대한 문자열을 받아 설정하는 setter
첫번째 파라미터 변수는 자체 소유권을 갖는 문자열이고, 두번째 마라미터 변수는 inout으로 선언한 Car 구조체 변수다.
기존 Car 구조체 값이 그대로 전달되지만, 내부에서는 임시로 Car 구조체를 복사하기 위해 박스가 하나 더 만들어진다.
새로 만들어진 박스에 기존 Car 구조체를 복사하고, driver 변수에 첫 번째 파라미터 값을 할당한다.
새 박스의 값을을 두번째 파라미터 구조체인 Car에 복사하고 만들었던 박스를 메모리에서 해제한다.

함께 만들어지는 meterializeForSet() 함수는 var 변수에 대한 초기값을 바로 할당하는 경우가 아닌 경우에 사용한다.(lazy/Computed Property)
-> 개발자가 직접 호출할 수 있는 함수가 아니기에 신경 안써도 됨

구조체 타입 기반의 스위프트 타입

Int, Bool, Set, Ditionary, Array 모두 구조체로 구현됐다.
따라서 Objective-C와 다르게 스위프트는 의미 있는 값 방식으로 동작한다.
Swift 1.x 버전에는 초기 호환성을 위해 Objective-C 런타임 기반으로 동작하는 클래스 타입이 많았다.
지금은 스위프트 런타임으로 코코아 라이브러리의 상당수를 구조체 타입 기반으로 다시 작성했다.
따라서 Objective-C 기반보다 빠르다.

요약

구조체 타입은 성능 향상을 위해 대부분의 경우는 스택에 값을 할당하고 사용한다.
구조체 구조가 동적으로 변하거나 크기가 너무 크다면, 힙 공간을 예외적으로 사용하기도 한다.
힙 공간에 있는 구조체거나 글로벌 구조체의 경우 함수 범위가 벗어나도 해당 구조체를 참조할 수 있다. (참조 방식의 특징)
이런 경우 구조체는 객체에 대한 레퍼런스 방식과 비슷하게 동작하지만, 참조 계산을 사용하지 않아 순환 참조 문제가 발생하지 않는다.

iOS에서 View의 Layout과 Content Update

iOS를 개발하다보면 View의 Layout과 Content와 관련된 이슈를 자주 접하게 된다.

실제로 UIView가 언제 Update되는지 모르기 때문에 발생한다.

View가 언제 Update 되는지 알기 위해서는 iOS App's Run Loop 를 이해하고 그것이 UIView가 제공하는 메서드들과 어떤 관계를 갖고 있는지 파악해야 한다.

 

iOS App's Run Loop

iOS App의 Main Run Loop는 유저로부터 모든 input Event를 받고, 응답을 담당한다.

유저가 발생한 모든 상호작용은 Event Queue에 추가된다.

아래의 사진처럼 App 객체는 Event Queue로부터 Event를 하나씩 꺼내서 App의 다른 객체들에게 전달한다.

App 객체는 유저로부터 input Event를 해석하고 그에 상응되는 App의 Core 객체들 안에 있는 Handler를 호출해준다.

또한 이러한 Handler는 개발자들이 만들어놓은 코드를 실행한다.

이러한 메서드들이 반환되면 다시 Main Run Loop로 돌아가 Update Cycle이 다시 시작된다.

Update Cycle은 View들을 배치하고 다시 그리는 역할을 한다.

 

Update Cycle

Update Cycle은 App이 유저로부터의 모든 Event Handling Code를 수행하고 다시 Main Run Loop로 컨트롤을 반환하는 지점

바로 이 지점에서 시스템은 우리의 View들을 배치하고(layout), 보여주고(display), 제약(contraints)한다.

만약 우리가 Event Handler들을 처리하는 과정에서 어떤 UIView에 변화를 준다면, 해당 UIView는 다시 그려져야 한다고 표시됨

다음 Update Cycle에서 시스템은 이 UIView의 모든 변화를 수행한다.

유저가 상호작용하는 것과 Layout이 변하는 시간의 Gap은 유저가 인지하지 못한다.

iOS App은 60 FPS이고, Update Cycle은 이 중 1 프레임에 해당된다.

이렇게 빠르게 Update되기 때문에, 유저는 UI와 상호작용 간의 차이를 느끼지 못한다.

그러나 이벤트가 처리되는 시점과 실제로 View가 다시 그려지는 시점의 차이가 존재하기 때문에, View는 우리가 View를 Update하기 원하는 Run Loop의 특정 시점에 Update되지 않을 수도 있다.

 

이는 다음과 같은 위험을 초래한다.

만약 우리가 View의 마지막 Layout이나 Content에 대해 계산을 해야하는 시점이라면, 예전 정보를 갖고 View를 조작할 가능성이 생긴다.

Layout

  • UIView의 Layout은 화면에서 UIView의 크기와 위치를 의미한다.
  • 모든 View의 frame을 갖고 있고, 이는 SuperView의 좌표계에서의 위치와 크기를 나타낸다.
  • UIView는 시스템에게 Layout이 변했다고 알려줄 수 있는 메서드
  • View의 Layout이 다시 계산되는 시점에 특정 작업을 실행할 수 있게 오버라이드 가능한 콜백 메서드를 제공

layoutSubviews()

  • View와 SubView들의 위치와 크기를 재조정한다.
  • 현재 View와 모든 SubView들의 위치와 크기를 제공한다.
  • 이 메서드는 재귀적으로 모든 SubView들의 layoutSubviews까지 호출되기 때문에, 부하가 크다.
  • 이 메소드를 직접 호출하면 안된다
    • 대신, layoutSubviews를 시스템이 호출하도록 유도하는 방식 존재
      • 이 방식은 모두 run loop가 돌아가는 동안 layoutSubviews가 실행되는 시점이 모두 다르다. 
    • setNeedsLayout() 호출 or 즉시 업데이트를 원하면 layoutIfNeeded() 메소드 호출
  • layoutSubviews가 완료될 때, View를 소유한 VC의 viewDidLayoutSubviews가 호출된다.
    • layoutSUbviews는 View의 layout이 변화했다는 callback이기 때문에, layout 관련 로직은 오래된 layout을 사용하는 것을 방지하기 위해 viewDidLoad/viewWillAppear가 아닌 viewDidLayoutSubviews에 호출해야 한다.

자동 refresh triggers

다음과 같은 이벤트들은 자동으로 View가 그것들의 layout에 변화가 생겼다는 것을 인지하여, 시스템에서 layoutSubviews가 다음 update Cycle에서 호출된다.

 

  • View를 Resizing
  • SubView 추가
  • UIScrollView 스크롤 시, UIScrollView와 그것의 부모뷰에 layoutSubviews가 호출됨
  • Device를 회전
  • View의 Constraint를 변경

위와 같은 방법들은 자동으로 시스템이 알아채어 layoutSubviews를 호출해준다.

그러나 layoutSubviews를 직접 호출할 수 있는 방법들도 존재한다.

setNeedsLayout()

layoutSubviews를 가장 적은 부하로 호출할 수 있는 메서드이다.

setNeedsLayouts는 시스템에게 이 View의 Layout이 재계산되어야 한다고 알린다.

setNeedsLayouts는 즉시 반환되고, 실제로 View Update를 해주는 것은 아니다.

대신, 시스템이 다음 Update Cycle에서 layoutSubviews를 View와 SubView들에게 호출하게 한다.

실제로 setNeedsLayouts가 호출되는 시점과 View가 다시 그려지는 시점은 정확하지는 않지만, 유저가 인지할 수는 없다.

layoutIfNeeded()

layoutIfNeeded는 UIView가 layoutSubviews를 호출하도록하는 명시적인 메서드이다.

layoutSubviews가 다음 UpdateCycle에서 호출되는 것이 아니라, View의 Layout의 변경사항이 있다면 즉시 호출한다.

만약 layoutIfNeeded를 setNeedsLayout를 호출한 직후나 자동으로 layoutSubviews를 호출하는 방법 직후에 호출한다면,

layoutSubviews는 뷰에 즉시 호출된다.

그러나 우리가 layoutIfNeeded를 호출했는데 View에 변경사항이 없다면 호출되지 않는다.

 

layoutIfNeeded는 주로 COnstraints를 애니메이션하는 상황에 유용하다.

애니메이션 시작전에 새로운 Constraints를 설정하고, 애니메이션 클로저안에 layoutIfNeeded를 호출한다.

 

 layoutIfNeeded()과 setNeedsLayout()의 차이를 보여준다.

 

출처 : ZeddiOS 블로그

 

Display

Layout이란 것이 View의 위치와 크기를 의미한다면, Display는 View의 속성 중 크기/위치나 SubView들에 대한 정보를 갖지 않는 속성을 포함한다.

ex) 색, 텍스트, 이미지, Core Graphics 등이 있다.

Display는 Layout 과정과 유사한데, 시스템이 자동으로 업데이트가 되게 하는 방식과 우리가 명시적으로 업데이트를 하는 방법이 존재

draw(_ rect:)

뷰에서 CGRect 직사각형으로 특정된 영역에 대해 다시 뷰를 그리는 등의 업데이트를 할 때, 호출되는 메서드

UIView의 draw 메서드는 Layout Update 과정에서의 layoutSubviews와 같은 역할을 한다.

차이점은 draw 메서드는 SubView들의 draw까지 호출하지 않는다는 것이다.

직접 draw 메서드를 호출하면 안된다.

내부 인자

rect는 업데이트되어야 하는 영역을 가지고 있따.

해당 rect의 범위는 업데이트 되는 뷰의 bounds 비율이 된다

ex) 뷰가 그려지는 첫 시점에는 직사각형은 그 자체가 전형적으로 현재 뷰로부터 보이는 전체 영역이 된다.

그러나, 부분적인 그리기 연산이 수행될 때, rect는 해당 뷰의 특정부분이 될 수도 있다

역할

전달된 사각형(rect) 내에서 receiver(수신자)의 이미지를 그립니다.

Core Graphics 및 UIKit 과 같은 기술을 사용하여 뷰의 내용을 그리는 하위 클래스는 draw 메서드를 재정의해야한다.

하지만, 이외의 방법으로 직접 컨텐츠를 설정하는 경우에는 재정의할 필요가 없다.

 

UIView를 직접 하위클래스로 만들면, super를 호출할 필요가 없다.

다른 View 클래스를 하위 클래스화 하는 경우, 적당한 위치에서 super를 호출해야 한다

이 메소드를 직접 호출하면 안된다 -> setNeedsDisplay() or setNeedsDisplay(_:) 를 호출하라

 

setNeedsDisplay()

setNeedsLayout과 유사하다.

View의 Content가 Update되게 하는 flag를 활성화시키고, 실제로 View가 다시 그리기 전에 메서드는 반환한다.

그러면 다음 Update Cycle에 시스템은 flag가 활성화된 View들의 draw를 호출한다.

우리가 만약 View의 일부분만 다시 그려지길 원한다면, setNeedsDisplay(_ rect:)를 통해 rect 범위만 그릴 수 있다.

대부분 View의 UI Component를 Update하는 것은 View의 dirty flag를 활성화시켜서 우리가 명시적으로 setNeedsDisplay를 호출하지 않아도 다음 Update Cycle에 View가 다시 그려지도록 유도한다.

그러나, 만약 UI Component와 직접적으로 연관되어 있지 않지만, 매 Update Cycle마다 다시 View를 그려줘야 하는 속성이 있다면,

didSet 감시자를 설정하여 setNeedsDisplay()를 명시적으로 호출할 수 있다.

 

 

Constraints

Auto Layout 세계에서는 Layout하고 Draw하는 것에 대한 3단계 과정이 존재한다.

  1. Constraints를 Update한다
    1. 시스템이 View에 필요한 COnstraints들을 계산하고 설정한다.
  2. Layout 단계
    1. Layout 엔진이 View들의 Frame과 SubView들의 Frame을 계산하고 배치한다.
  3. Display 단계
    1. View의 Content를 다시 그릴 필요가 있다면, draw 메서드를 호출한다.

 

updateConstraints

이 메서드는 Auto Layout을 이용하는 View의 COnstraints를 동적으로 변경할 때 사용된다.

Layout 단계에서 layoutSubviews나 Display단계에서 draw처럼, updateConstraints는 오직 오버라이딩되어야 하며 직접 호출해서는 안된다.

우리는 보통 updateConstraints에서 동적으로 변하는 Constraints들을 구현한다.

정적인 Constraints들은 IB나 View의 생성자나 viewDidLoad에서 정의되어야 한다.

 

일반적으로, Constraints를 활성화/비활성화하거나 Constraint의 우선순위나 constant를 변경하거나, View를 계층에서 삭제하는 것은 updateConstraints를 다음 Update Cycle에서 호출하게 한다.

그러나 이 역시 명시적으로 호출하는 방법이 존재한다.

 

setNeedsUpdateConstraints

setNeedsUpdateConstraints를 호출하는 것은 다음 Update Cycle에서 Constraints가 Update되는 것을 보장한다.

setNeedsLayout과 setNeedsDisplay와 비슷하다.

 

updateConstraintsIfNeeded

updateConstraintsIfNeeded는 layoutIfNeeded와 유사하다.

그러나 AutoLayout을 사용하는 View에서만 유효하다.

Constraint Update Flag를 검사하여서, Update가 필요하다면 updateConstraints를 즉시 호출한다.

 

invalidateIntrinsicContentSize

Auto Layout을 사용하는 몇몇 View들은 intrinsicContentSize 속성을 갖고 있다.

View가 갖고 있는 Content의 크기를 의미한다.

intrinsicContentSize는 전형적으로 View가 갖고 있는 요소들의 Constaints로 결정되지만,  Override하여 Custom이 가능하다.

 

 

그렇다면 이것들을 어떻게 연결시키는가?

View의 Layout과 Display 그리고 Constaints는 Run Loop에서 다른 시점에 어떻게 Update되고, 명시적으로 Update할 수 있는 지에 대해 유사한 패턴을 갖는다.

각 Component들은 layoutSubviews, draw, updateConstraints와 같은 실제로 Update를 하는 메서드를 갖는다.

이 메서드들은 직접 호출되면 안되기 때문에, 유도할 수 있는 방법이 존재한다.

이러한 메서드들은 Run Loop의 마지막에 View의 해당 Flag가 활성화되어 있으면, 시스템이 호출해주는 방식이다.

Layout과 Constraints의 Update는 즉시 Update할 수 있는 메서드가 존재한다.

다음 표는 Update Cycle과 Event Loop 그리고 위의 메서드들이 Cycle 동안 어떻게 호출되는지를 설명하는 표이다.

우리는 layoutIfNeeded / updateConstraintsIfNeeded를 Run Loop의 어디서든 즉시 호출가능하다.

Loop의 끝은 Update Cycle이다. Update Cycle은 Constraints, Layout, Display를 Flag가 활성화된 View를 Update한다.

 

 

 


 

Q) 근데 항상 View들의 속성을 변경해줬을 때, setNeedsDisplay() 호출 없이도 잘만 업데이트되던데...?

A) 시스템 자체적으로 호출되고 있다, 거의 모든 표준 UI 구성요소 View의 프로퍼티가 수정될 때마다, 내부적으로 setNeedsDisplay()가 트리거된다.

예)

  1. View를 부분적으로 가리고 있던 다른 View이동 및 제거
  2. hidden 프로퍼티를 No로 설정하여, 이전에 숨겨진 View를 다시 볼 수 있게 만들기
  3. View를 화면 밖으로 스크롤한 다음, 화면으로 다시 이동하기

이 3가지 상황에서는 내부적으로 setNeedsDisplay()가 호출되고 있다

 

Q) 그럼 언제 사용해?

A) 나 자신만의 View를 만들고, 자체 draw메소드를 구현하고, 무언가가 변경되면 호출!

 


'iOS' 카테고리의 다른 글

[iOS] Dynamic/Static Framework (1)  (0) 2021.06.01
[iOS] Framework vs Library / 모듈화  (0) 2021.06.01
[iOS] View LifeCycle  (0) 2021.05.24
[iOS] UIWindowScene / UIWindow / UIView  (0) 2021.05.23
[iOS] HitTest  (0) 2021.05.21

HitTest

이전 포스팅 Responder에 대해서 공부할 때, 잠깐 나왔던 개념이다.

 

정의는

Returns the farthest descendant of the receiver in the view hierarchy (including itself) that contains a specified point
지정된 점(point)을 포함하는 View 계층에서 리시버의 가장 먼 자손을 리턴한다.

라고 한다.

 

메소드이다.

 

point = 리시버의 bounds로 지점된 점

event = 메소드에 대한 호출을 보증하는 이벤트이다. 이벤트 처리코드 외부에서 이 메소드를 호출하는 경우에는 nil을 지정할 수 있다.

 

리턴 타입이 UIView?로 옵셔널이다.. nil이 반환될 수도 있다는 건데 -> 잠시 후에 알아보자

 

Discussion

이 메소드는 SubView의 point(inside:with:)를 호출하여 View 계층을 탐색하고, 어떤 하위 VIew가 터치 이벤트를 받아야 하는지 결정한다.

point 메소드의 파라미터는 hitTest와 같고, 반환타입만 다르다.

  • hitTest = 해당 point를 통해 리시버로부터 가장 먼 SubView를 반환
  • point = 해당 point가 리시버의 Bounds 내에 존재하는가?

point가 true를 반환하면, 하위 View 계층구조는 지정된 point를 포함하는 가장 앞에 있는 View를 찾을 때까지 재귀적으로 찾는다.

point가 false를 반환하면, View 계층 구조의 해당 분기가 무시된다.

(분기? -> hitTest는 Reverse DFS 방식으로 찾는다.)

ex) 

해당 View 계층일 때,

MainView -> View C -> View C.2 -> View C.1 -> View B ... 

순서로 찾게 된다.

 

왜? DFS면 DFS지 Reverse가 붙어?

-> iOS View 체계에서는 SubView가 SuperView보다 위(사용자에게 가까이)에 놓여진다.

-> 같은 SuperView를 가진 형제 View들의 경우에는 index가 늦은 View가 위에 놓여진다.

따라서, Reverse로 탐색할 때, hitTest가 성공한 최하단 SubView = 가장 위에 놓여진 View 가 된다.

 

 

hitTest 메소드의 동작 조건

  • hidden = false
  • alpha > 0.01
  • userInterationEnabled = true

리시버 Bounds 외부에 있는 point는 실제로 리시버 하위 View중 하나에 속하더라도, hit이 아니다.

-> Responder Chain에서의 GreenView와 같은 현상

 

hitTest 구현부

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (self.hidden || !self.userInteractionEnabled || self.alpha < 0.01 || ![self pointInside:point withEvent:event] || ![self _isAnimatedUserInteractionEnabled]) {
        return nil;
    } else {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            UIView *hitView = [subview hitTest:[subview convertPoint:point fromView:self] withEvent:event];
            if (hitView) {
                return hitView;
            }
        }
        return self;
    }
}

조건문에 있듯이 hitTest의 동작 조건에 반하는 조건들이 if문에 걸려있다.

 

Why?

왜 hitTest를 알아야 하나?

Responder Chain 포스팅에서도 적었 듯

 

HitTest 테스트 포스팅

http://smnh.me/hit-testing-in-ios/

 

Hit-Testing in iOS

Hit-testing is the process of determining whether a point, such as touch-point, intersects a given graphical object, such as UIView, drawn on the screen. iOS uses hit-testing to determine which UIView is the frontmost view under the user’s finger that sh

smnh.me

 

 

 

 

 

'iOS' 카테고리의 다른 글

[iOS] View LifeCycle  (0) 2021.05.24
[iOS] UIWindowScene / UIWindow / UIView  (0) 2021.05.23
[iOS] Responder / Responder Chain  (0) 2021.05.21
[iOS] State Preserving/Restoring ViewControlle  (0) 2021.05.20
[iOS] App Life Cycle  (0) 2021.05.18

+ Recent posts