IBOutlet, Weak, Strong, IUO

Choong Won, Seo·2024년 5월 31일

TIL

목록 보기
3/7
post-thumbnail

스토리보드 기반 Layout을 오랜만에 배우면서, 새롭게 배우는 느낌으로 이전에 별 의심없이 그냥 지나갔던 것들, 헷갈리는 내용들을 조금 다시 정리해보려 한다.

@IBOutlet weak var button: UIButton!

Strong & Weak

예전에 혼자 @IBOutlet을 생성할 때에는 모든 아웃렛을 weak로 생성했다. → 왜?? 그냥 default가 weak이니까…

그렇다면 strong(default)weak는 어떤 차이가 있고, 애플에서는 어떤 방식을 권장을 하는 것일까?

일단 불변하는 사실들에 대해서 먼저 알아보자.

  • 모든 ViewController는 자신이 관리하는 View(생성할 때 자동으로있는)에 대한 강한 참조를 유지한다.

open var view: UIView!를 보면 weak로 선언된게 아닌 것을 알 수 있다.

ViewController가 존재하는 한 View는 없어지면 안되기 때문이다.

  • 마찬가지로 View는 또한 자신의 하위 View객체(Subviews)에 대한 강한 참조를 유지한다.

기본적으로 View에 올린 View객체들(label, button…)은 View의 강한 참조로 인해 RC가 0이 아닌 1이다.

이런 배경 지식들을 알고있는 상태에서 위에 사진 ViewController에서 StrongWeak로 각각 View객체를 연결할 때 Reference Count를 살펴보자.

  1. Strong View객체 참조일 때
  • ViewController는 View를 강하게 참조 → View의 RC: 1
  • View는 View객체를 강하게 참조 → View객체의 RC: 1
  • ViewController는 View객체를 강하게 참조 → View객체의 RC: 2
  1. Weak View객체 참조일 때
  • ViewController는 View를 강하게 참조 → View의 RC: 1
  • View는 View객체를 강하게 참조 → View객체의 RC: 1
  • ViewController는 View객체를 약하게 참조 → View객체의 RC: 1 (변동 X)

그럼 언제 Weak를 쓰고 String을 써야하지?

Weak

Weak, Strong이 언제나 그렇듯 정상적인 메모리 Dealloc에서는 메모리 누수가 일어나지 않는다.

하지만 메모리가 부족해지면 ViewController는 didReceiveMemoryWarning()메소드를 호출하는데, 이 때 View가 nil처리 되면서 참조하던 View객체들의 RC는 줄어들지만, Strong참조일 때 ViewController가 View객체를 강하게 참조하고 있던 Count가 떨어지지 않아 메모리 누수가 일어난다.

didReceiveMemoryWarning() - VC가 없어지는 것이 아닌 view가 nil처리 된다. 리소스 관리를 위해 없어진 view는 loadView, viewDidLoad등의 생명주기 함수들을 통해 다시 나타날 수 있다.

Strong

메모리가 해제되는 시점을 직접 제어하고 싶을 때 활용하면 좋을 것 같다. ViewController가 살아있는 한 Strong View객체는 메모리에서 해제되지 않으므로 제어가 용이하다.

또한, 복잡한 계층구조를 갖고 있는 View라면, 중간 계층의 View객체가 어떠한 사정으로 인해 갑자기 메모리에서 해제되면, 그 View객체의 하위 요소들도 모두 메모리에서 해제될 수 있다. 이런 경우 nil에러가 발생할 확률이 높기 때문에 Strong을 쓰면 좋을 것 같다.

→ 결론적으로 말하면, 메모리 해제가 될 가능성이 있거나, 해제되면 Error가 발생할 확률이 높은 View객체들은 Strong으로 작업하고, 나머지 View객체들은 Weak로 작성해서 메모리 누수가 일어나지 않게 하는 것이 좋을 것 같다.

(여기서 궁금증이 생겼다. Hierachy가 있는 View객체들끼리는 Reference Count가 어떻게 될까?)

이런 경우… 찾아보니 유의미한 결론이 나오지 않아서 직접 RC를 체크를 해보기로 했다.

@IBOutlet var viewObject: UIView!
@IBOutlet var viewObjectLabel: UILabel!

print(CFGetRetainCount(viewObject)) //4
print(CFGetRetainCount(viewObjectLabel)) //4

3이 아니라 4가 나온거는 사실 VC어디선가의 inital 증가값 (3)이어서 그런 것 같은데 일단 무시한다.

viewObject - VC에서 강한 참조 inital 증가값(3) + View에서 강한 참조(1)

viewObjectLabel - VC에서 강한 참조 inital 증가값(3) + View에서 강한 참조(1)

똑같이 값이 4가 나오는 것을 보니 Hierachy가 있는 View객체들 사이에서는 참조가 일어나지 않는 모습을 볼 수 있다.

→ 왜그런지는 뒤의 IUO에서 정확히 알아보도록 하자.

IBOutletCollection

우리는 중복되는 UI의 IBOutlet을 모두 생성하기 귀찮을 때 IBOutletCollection을 사용한다.

하지만 IBOutletCollection는 weak를 사용할 수 없고, 명시적으로 weak 키워드를 넣었을 때는 에러가 뜬다.

@IBOutlet var viewCollection: [UIView]!

'weak' may only be applied to class and class-bound protocol types, not '[UIView]’

왜 그럴지 생각해봤는데, 이는 IBOutletCollection이 View객체들의 참조를 저장한 ‘배열’이기 때문이다.

배열은 Struct로 이루어져있기 때문에, 참조라는 개념과는 전혀 무관하다. 그래서 weak를 사용할 수 없다.

IUO (Implicitly Unwrapping Optional)

이제 Weak와 Strong이 왜 있고 어떤 차이점이 있는지 알았다. 하지만 여기서 끝이 아니다. 또 궁금한 점이 생겼다. → 왜 Var을 쓰고 IUO(Implicitly Unwrapping Optional)를 통해서 초기화를 하는 것일까?

일단 IUOnil coalescing, force unwrapping, optional chainning, optional binding과 마찬가지로 Optional type을 선언하는 방법중 하나이다.

가장 큰 장점은 Optional type을 Non-Optional type에 대입할 때 따로 처리 없이 바로 할당(unwrapping)이 가능하다는 것이다.

IUO - container that will automatically perform a force unwrap each time we read it.

물론, IUO도 그냥 print를 할 때에는 Optional type이다.

let iuo: String! = "IUO"
print(iuo) // Optional("IUO")

단, 아래와 같이 Non-Optional type에 대입할 때는 Error없이 바로 할당이 가능하다. 또한, 연산도 가능하다.

var nonOptionalIuo: String = iuo 
print(nonOptionalIuo) // IUO

이러한 이점들 때문에, IBOutlet에서 Property 지연초기화를 할 때 사용을 한다.

지연초기화

왜 지연초기화를 하는데?? ViewController에는 viewDidLoad(), viewWillApear()등 여러 생명주기가 있다는 사실을 알 것이다. 이러한 생명주기가 있는 이유는 ViewController가 바로 나타나는 것이 아니라 어떠한 시점에 Instant가 생성되고, 초기화가 된다는 것이다.

IBOutlet PropertyUI 요소 값을 넘겨주는 시점은 viewDidLoad()인데, 그렇다면 그 시점 전, 초기화가 되는 시점에서는 저장 Property에 값이 없으면 안되지 않겠는가?!

그래서 지연초기화(IUO방식)를 사용하는 것이다…!!

왜 IUO방식?

다른 지연초기화 방법도 많은데 왜 IUO를 사용하는건데!! 라고 묻는다면 이미 답을 알고있다. 앞에서 말했듯 IUO방식은 Non-Optional type에 대입할 때는 Error없이 바로 할당이 가능하다, 또 연산도 가능하다. 만약 IUO방식을 사용하지 않고 옵셔널 타입으로 지정만 한다면, 아래처럼 IBOutlet을 사용할 때마다 항상 옵셔널 바인딩이나 언래핑을 항~상 사용해야 할 것이다.

@IBOutlet weak var button: UIButton?
//1
guard let button = button else { reutrn }
//2
button?.currentTitle ?? ""

하지만 편한 만큼 단점도 존재한다.

viewDidLoad() 메소드가 불리기 전 시점에서 IBOutlet Property에 접근하게 된다면 무조건 런타임 에러가 발생할 것이다.

이에 대한 해결 방법은 1. 옵셔널 타입을 사용하던가 2. 런타임 에러가 걱정되는 시점에는 옵셔널 체이닝을 통해 안전하게 접근하면 된다.

imageView?.image = UIImage(named: "someImage")

후 드디어 마지막…. 아까 뒤에서 알아본다고 했던 View객체들 사이에는 왜 Reference Count가 증가하지 않는지 알아보고 끝내도록 하자.

앞서 알아본 지연초기화 때문에, IBOutlet으로 연결된 View객체들은 VC Instance 내부에 저장되는 것이 아니라는 것은 대략 짐작이 왔을 것이다. 이 View객체들은 Class로써 Heap 영역에 저장이 되는데, 그로인해 Outlet변수로 연결되는 시점에 VC에 ‘참조’형식으로 저장되게 된다.

이를 통해 유추해보면, Hierachy가 있는 View객체와 View객체는 계층구조만 있고, 같은 시점에 VC에 참조될 뿐, 서로에 대한 참조는 갖고 있지 않다고 유추해볼 수 있다.

여러모로 너무 배운게 많은 정리였다… 생명주기까지 연결이 돼서 공부할줄은 몰랐는데, 역시 기본적인 레벨부분이 알면 알수록 더 보이는게 많은 것 같다 ㅜㅜ

참조

iOS) IBOutlet연결 Strong VS Weak

[Swift] IUO(옵셔널 암시적 추출)

[UIKit] Dicee 앱 만들기

profile
UXUI Design Based IOS Developer

0개의 댓글