iOS 공부하는 감자

iOS) App · ViewController의 생명주기 본문

iOS

iOS) App · ViewController의 생명주기

DongTaTo 2022. 7. 13. 09:11
반응형

iOS에서 생명주기란 앱의 최초 실행과 종료 사이에 발생하는 일련의 이벤트로 구성된다.

내부 메커니즘에 의해 각 이벤트 시점마다 함수들이 자동으로 호출되며, 개발자는 각 시점에 필요한 기능을 재정의하여 구현하면 된다.

 

Life Cycle은 크게 두 종류로 구분된다.

  1. 앱 생명주기
  2. ViewController 생명주기

 

 


 

앱 생명주기

앱 생명주기는 앱의 전반적인 실행과 종료에 관련된 생명주기를 말한다.

(앱이 실행되고, 백그라운드도 진입하고, 다시 실행되고, 종료되는 등의 주기..)

 

iOS 13 발표 전까지는 왼쪽의 사진처럼 AppDelegate에서 앱 생명주기(앱의 실행과 종료)와 UI 요소의 생명주기 (백그라운드 상태 로직 등)을 모두 처리했었지만

발표 이후 오른쪽 사진처럼 AppDelegate에 추가로 SceneDelegate가 생겨났다.

 

SceneDelegate의 필요성(?)

iOS 12까지는 하나의 앱에 하나의 window가 있었다.

UIWindow는 앱 실행의 기본 요소로써 여러 화면을 컨트롤(?)해주는 역할을 수행했지만

iPadOS와 여러 화면을 한번에 올려서 실행시키는 스플릿뷰의 등장으로 이를 지원하기 위해 window의 개념이 scene으로 대체되었다.

 

한 번에 하나의 화면만 보여주던 기존과 다르게, 동일한 앱을 스플릿뷰로 나눠서 실행할 수도 있게 되었다.

이런 경우 사용자가 실제로 사용하는 앱 프로세스는 하나지만, 화면에 보이는 Scene은 여러 개가 될 수 있고, 각각의 Scene은 독립적으로 생명주기를 갖기 때문에 이를 전문적으로(?) 관리하기 위해 SceneDelegate가 생긴 것이다.

(ex- 한 Scene은 foreground에 있고, 다른 하나는 background 혹은 suspended 되었을 수 있다.)

 

 

기존 AppDelegate의 역할이었던 UILifeCycle에 대한 부분들이 SceneDelegate로 옮겨졌고

 

 

AppDelegate에 UISceneSession Lifecycle 에 대한 역할이 추가되었다.

아래의 두 메서드는 Scene Session이 생성되거나 삭제될 때 AppDelegate에 알리기 위해 존재한다.

 

새로운 Scene이 필요할 때마다 AppDelegate의 application(_:configurationForConnecting:options:) -> UISceneConfiguration 가 호출되고, Scene이 추가되면 UISceneSession 라이프 사이클에서 scene(_:willConnectTo:options:)가 호출된다.

 

 

iOS 13.0 이후의 AppDelegate 역할

  1. 앱의 중요한 데이터 구조를 초기화한다.
  2. Scene을 환경설정한다.
  3. 앱 밖에서 발생하는 알림에 대응한다. (배터리 부족, 전화 수신)
  4. 특정 Scene, View, ViewController에 한정되지 않고, 앱 자체를 타깃으로 하는 이벤트에 대응한다.
  5. 애플 푸시 알림 서비스처럼 실행 시 요구되는 모든 서비스를 등록한다.

 

 

 

앱 생명주기 흐름 ( iOS 13.0 ~ )

빨간박스 : AppDelegate

검정박스 : Scene Delegate

Unattached : 앱이 실행되지 않은 상태 (메모리에 올라오지 않은 상태)

 

Foregound : 화면을 점유하는 상태로, CPU를 포함한 시스템 리소스를 우선적으로 사용한다.

  • InActive : 임시 비활성화 상태 (전화나 알람 등으로 인해 앱이 잠시 비활성화되는 상태) / 화면을 점유하지만, 앱의 모든 동작을 사용자가 전부 컨트롤할 수 없는 상황이면 InActive상태 / 외부적인 요인(Interrupt)에 의해 InActive가 될 수 있다.
  • Active : 앱이 실행되고, 이벤트를 받을 수 있는 상태

 

Background : 앱이 Background에 있거나, 스플릿뷰 상에서 화면을 점유하지만 포커싱 되지 않은 상태

제한적인 실행만 가능하다. (ex- 음악 재생, 사용자 위치 사용.. 등등)

백그라운드 상태에서 추가적인 작업이 없다면 suspended상태로 이동한다.

 

Suspended : 앱이 일시 중단된 상태로, 다음 실행을 기다리는 대기 상태이다.

앱을 다시 실행할 경우 빠르게 실행하기 위해 메모리에만 올라가 있다.

메모리가 부족한 상황이 왔을 때 iOS는 suspended상태의 앱을 메모리에서 해제시켜서 부족한 메모리를 확보한다.

 

Document
When the user dismisses your app's UI, UIKit moves the associated scene to the background state and eventually to the suspended state. UIKit can disconnect a background or suspended scene at any time to reclaim its resources, returning that scene to the unattached state.

유저가 사용 중이던 앱의 UI를 dismiss 하면(내리면) UIKit이 scene을 백그라운드로 이동시키고, 결국은 일시정지 상태로 보낸다.
UIKit은 리소스를 회수하기 위해 언제든지 백그라운드 또는 일시 중단된 scene의 연결을 끊고 해당 scene을 연결되지 않은 상태로 되돌릴 수 있다.

 

 

 

 


 

ViewController 생명주기

 

ViewController의 생명주기는 위 그림과 같은 주기를 가지며

영어 직독직해를 하면 쉽게 이해할 수 있다.

 

 

loadView()

View를 로드한다. ==>> View를 메모리에 올리는 함수이다.

ViewController는 메모리에 올라온 후, 인스턴스 메서드인 loadView()를 통해 화면(view)을 메모리에 올리는 것이다.

 

ViewController의 IUO 저장 프로퍼티로 선언되어 있는 view의 요청으로 이 메서드가 호출되면

view를 로드하거나 생성하여 해당 저장 프로퍼티에 할당한다.

 

ViewController와 연결된 스토리보드(nib)가 있다면 해당 스토리보드에서 View를 로드하며,

스토리보드(Interface Builder)를 통해 View를 생성 및 초기화하는 경우 loadView() 메서드를 재정의해서는 안된다.

"If you use Interface Builder to create your views and initialize the view controller, you must not override this method."

 

만약 view를 수동으로(코드로) 생성했다면 loadView()를 재정의해서 ViewController의 view에 직접 생성한 view를 할당하면 된다. ( super.loadView() 불필요 )

 

 

 

viewDidLoad()

view did load .. 즉, view가 메모리에 올라온 후 호출된다.

스토리보드와 IBOutlet을 사용하여 view를 구성했다면, 스토리보드의 뷰객체와 ViewController의 코드가 연결된 후 호출된다.

 

뷰가 메모리에 올라온 후, 한 번만 호출된다.

뷰가 메모리에서 해제되었다면 viewDIdLoad()는 다시 호출된다. (VC가 메모리에서 해제되었는지 여부를 기준으로 함 -> deinit)

 

공식문서에서 view의 추가 초기화를 진행하려면 이 viewDidLoad()를 재정의하여 사용하라고 말해주고 있다.

view가 메모리에 올라올 때 한 번만 실행되므로, 처음 한 번만 실행하면 되는 코드를 이곳에 작성한다.

(보통 뷰객체의 추가 초기화 내용을 담은 함수를 따로 생성하고, viewDidLoad에서 해당 함수를 호출하는 방식을 많이 사용함)

 

 

 

viewWillAppear

view가 화면에 나타나기 전에 호출된다.

 

ViewController가 메모리에 올라올 때, 한 번만 실행되는 viewDIdLoad()와 다르게

화면을 띄워줄 때마다 호출된다. (다른 VC를 보다가 다시 돌아오는 등..)

 

뷰를 띄울 때마다 특정 처리가 필요하거나, 다른 뷰에서 처리한 작업 결과로 인해 뷰를 업데이트해야 하는 경우에 사용된다.

(== 화면 갱신이 필요한 상황)

 

 

 

viewDidAppear

스크린에 뷰가 나타난 후 호출된다.

뷰의 애니메이션을 시작하거나, 타이머를 시작하는 등 화면이 나타난 후 필요한 작업을 처리한다.

 

 

 

viewWillDisappear

스크린에서 뷰가 사라지기 전에 호출된다.

진행 중인 애니메이션을 멈추거나, 타이머를 종료시키는 등의 작업을 처리한다.

 

 

 

viewDidDisappear

스크린에서 뷰가 사라진 후 호출된다.

단지 화면이 스크린에서 내려갔을 뿐, 메모리에서 해제된 것은 아니다.

 

 

 

 

 


 

 

 

ViewController 생명주기 함수의 호출 시점 파악

 

TabBarController 하위에 2개의 ViewController를 구성하고,

그중 하나의 ViewController에서 push와 modal로 이동할 수 있는 2개의 ViewController를 연결했다.

 

그리고 각 ViewController는 생명주기 함수가 호출될 때, 호출하는 함수를 print 하도록 설정했다.

 

 

 

1. 앱을 처음 실행시켰을 때

처음 보이는 화면의 ViewController만 메모리에 올라왔음을 확인할 수 있다.

 

 

 

2. Tab Bar를 사용한 화면 전환

(좌) 두번째 TabBar VC로 화면 전환          (우) 다시 첫번째 TabBar VC로 화면 전환

두 번째 TabBar VC로 이동할 때, 해당 VC의 viewDidLoad()가 호출되었고,

다시 첫 번째 TabBar VC로 돌아와도 두 번째 VC는 메모리에서 해제되지 않았다. (deinit 호출되지 않음)

 

TabBarController 하위의 ViewController들 생명주기

1) 다른 화면으로 전환되더라도, 메모리에서 해제되지 않는다. (TabBarController가 하위의 VC들을 강하게 참조하여 RC를 유지시킨다.)

2) lazy 하게 동작하는 것 같다. (화면을 띄워야 할 상황에 TabBarController에 의해 강하게 참조되며 메모리에 올라온다.)

 

 

 

 

3. (NavigationController) push를 사용한 화면 전환

(좌) Push를 사용하여 화면 전환          (우) pop을 사용하여 원래 화면으로 돌아옴

push를 사용하여 화면을 전환할 때, 생명주기 함수 호출은 TabBar를 사용한 화면 전환과 동일하지만

 

pop을 통해 원래의 VC로 돌아올 때, ViewController가 메모리에서 해제되어 deinit이 호출된다.

그리고, 다시 push 하여 VC를 전환하면 viewDidLoad()가 호출된다. (메모리에서 해제되었으니깐)

 

NavigationController는 Stack처럼 동작한다.

push를 통한 뷰 전환을 하면 기존 뷰 위에 하나씩 차곡차곡(?) 쌓이는 개념이다. (그래서 메서드 이름이 push랑 pop...)

 

Stack에서 pop 되는 데이터는 메모리에서 사라진다.

그래서 pop 된 ViewController도 메모리에서 사라진 것!!

 

아마 내부적으로는 NavigationController경로 상에서 상위 ViewController가 하위 ViewController를 강하게 참조하여 RC를 유지시키다가 pop을 통해 화면이 돌아오면, 참조를 잃도록 설계된 것 같다.

 

 

 

4. modal을 사용한 화면 전환

modal의 경우 Presentation의 설정값에 따라서 루트 뷰 컨트롤러의 생명주기 함수 호출에 차이가 있다.

 

1. Presentation : 기본값, Page Sheet, Form Sheet

(좌) modal을 사용하여 화면 전환          (우) dismiss()를 사용하여 원래 화면으로 돌아옴

기존 ViewController를 완전히 덮지 않는 Presentation 설정의 경우,

modal로 띄우는 ViewController에 대한 생명주기 함수만 호출되었다.

 

위 사진처럼 루트 뷰 컨트롤러가 화면에서 사라지지 않고, 뒤에 겹쳐서 보이기 때문에 viewWillDisappear, viewDidDisappear 같은 생명주기 함수가 호출되지 않은 것..!

 

기본 스타일의 modal을 사용하여 뷰 전환을 구현한다면, 루트 뷰 컨트롤러의 생명주기 함수가 호출되지 않음을 인지하고 주의해서 로직을 구현해야 한다.

 

그리고, Modal을 사용한 뷰 전환도 push를 사용한 뷰전환에서와 마찬가지로 뷰전환 과정이 Stack처럼 쌓인다.

원래의 VC로 뷰가 돌아가면 modal로 띄운 VC는 메모리에서 해제된다.

 

 

 

2. Presentation : Full Screen

(좌) modal(full screen)을 사용하여 화면 전환          (우) dismiss()를 사용하여 원래 화면으로 돌아옴

 

Full Screen 설정의 경우, 루트 뷰 컨트롤러를 완전히 덮기 때문에 push를 사용한 뷰 전환과 마찬가지로 이전 ViewController의 생명주기 함수도 같이 호출된다.

 

 

 

 

 

3. Presentation : Over Full Screen, Over Current Context

(좌) modal을 사용하여 화면 전환          (우) dismiss()를 사용하여 원래 화면으로 돌아옴

 

두 modal 스타일은 분명 루트 뷰 컨트롤러를 완전히 덮는데도 modal 기본값 설정처럼 루트 뷰 컨트롤러의 생명주기 함수가 호출되지 않는다.

 

이유는 이름에서 알 수 있듯이 루트 뷰 컨트롤러의 화면 위에 Over해서 화면을 쌓기 때문이다.

 

그냥 Full Screen과 push를 사용한 뷰 전환의 경우, 루트 뷰 컨트롤러의 화면은 사라진다.

메모리에서 해제된다는 의미는 아니고.. 화면에서 사라진다.

 

하지만 modal의 기본 스타일과 Over Full Screen, Over Current Context는 루트 뷰 컨트롤러의 화면을 그대로 유지하고, 위에 새로운 화면을 쌓는 개념으로 이해하면 될 것 같다..

 

그래서 Over 스타일을 적용하면 이동하는 ViewController에 배경 Opacity를 조절하여 팝업창을 띄우는 것 같은 효과를 낼 수 있다.

 

 

 

 

 

 

 

 

Hola

반응형