먹고 기도하고 코딩하라

[UIKit] SceneDelegate와 앱의 라이프 사이클 본문

앱/Swift

[UIKit] SceneDelegate와 앱의 라이프 사이클

사과먹는사람 2023. 7. 6. 21:25
728x90
728x90

 

요즘 iOS 앱들은 최저 배포 버전이 낮아봐야 13, 14 정도다. 당연히 SceneDelegate가 있는 프로젝트들이고, 토이 프로젝트 만들 때도 13 미만을 고려하는 경우는 거의 없다. 그래서 AppDelegate만 있는 앱에서 SceneDelegate를 새로 도입해야 할 때 뭘 고려해야 하는지, 같은 생각 자체를 해본 적이 없다.

하지만..

회사 서비스는 iOS 11부터 지원하고 있었고, 지금까지는 AppDelegate만 사용하고 있었다. 올해 1월에 카플레이를 도입하면서 SceneDelegate를 사용할 일이 있어 이 문제를 고민하게 됐다.

애플 공식 문서를 박박 긁어 AppDelegate, SceneDelegate와 함께 끝까지 가보기로 했다.

 

 

(?) iOS 13부터는 왜 AppDelegate가 아니라 SceneDelegate를 쓰는 걸까?

iOS 13 이상부터는 앱 UI의 카피를 여러 개 만들고 앱 스위처에서 변환해서 사용하는 게 가능하다. 이게 아마 13 미만 버전과의 가장 큰 차이일 텐데, 앱 하나에 하나의 창이 있는 게 아니라 두 개 이상의 창을 쓸 수 있다. 

카플레이 프로젝트를 하느라 SceneDelegate를 도입했는데, 폰에 떠 있는 씬과 카플레이 스크린에 떠 있는 씬 이렇게 2개가 있는 걸 보니까 멀티씬 개념이 한 번에 이해된다.. 사실 개발하는 앱이 폰만 지원한다면 멀티씬 개념을 이해하기가 살짝 어려운데, 아이패드 앱을 만들거나 카플레이 앱을 만들거나 하면 이해할 수 있다.

아무튼 앱의 상태 변화(foreground/background에 들어가는 등)에 따라 적절한 행위를 하도록 조치해야 하는데, UIKit은 앱 상태 변화가 일어날 때마다 적절한 delegate 메소드를 호출한다.

 

 

앱의 라이프 사이클 관리하기

iOS 12 이하에서는 라이프 사이클 이벤트에 반응하는 UIApplicationDelegate 객체를 사용하고, iOS 13 이상에서는 scene 베이스 앱에서 UISceneDelegate 객체를 사용한다.

앱에서 scene을 지원한다면 iOS 13 이상에서는 항상 sceneDelegate를 사용하고, 그 이하에서는 appDelegate를 사용하게 된다.

scene은 기기에서 동작하는 앱의 UI 중 한 객체를 나타낸다. 사용자는 앱마다 여러 scene을 생성할 수 있고, 보이고 숨기고 하는 것을 각각 처리할 수 있다. 앱이 아니라 scene마다 고유 라이프 사이클이 있기 때문에 각자가 다른 상태에 놓일 수 있는 것이다. 예를 들면, 다른 scene은 background거나 suspended일 때 다른 한 scene은 foreground에 있을 수 있다. 

 

Scene 베이스 앱에서는 다음과 같은 작업을 한다.

  • UIKit이 앱에 scene을 연결할 때, scene의 최초 UI와 scene에 필요한 데이터를 로드한다.
  • 포어그라운드 활성화 상태로 변할 때, UI를 설정하고 사용자와 상호작용할 수 있도록 준비한다.
  • 포어그라운드 활성화 상태를 뜰 때, 데이터를 저장한다.
  • 백그라운드 상태로 들어갈 때, 중요한 작업은 끝내고 최대한 많은 메모리를 풀어놓고 앱 스냅샷을 준비한다.
  • scene 연결이 끊어질 때, scene과 연관된 공유 자원들을 정리한다.
  • scene 관련 이벤트에 덧붙여, UIApplicationDelegate 객체를 사용해 앱 구동에 대응을 해야 한다.

iOS 12와 그 이전 버전은 scene을 지원하지 않는다. UIKit은 UIApplicationDelegate 객체에 라이프 사이클 이벤트를 전부 전달한다. App 베이스 앱에서는 다음과 같은 작업을 한다.

  • 구동할 때 UI와 앱의 데이터 구조를 초기화한다.
  • 앱이 활성화될 때, UI 설정을 마치고 사용자와 상호작용할 준비를 한다.
  • 앱이 비활성화될 때, 데이터를 저장한다.
  • 백그라운드 상태로 들어갈 때, 중요한 작업은 끝내고 최대한 많은 메모리를 풀어놓고 앱 스냅샷을 준비한다.
  • 앱이 종료될 때, 모든 작업을 즉시 중단하고 공유 자원들을 풀어놓는다. (applicationWillTerminate(_:))

 

 

앱 구동에 대응하기

시스템은 사용자가 홈 스크린에서 앱 아이콘을 탭했을 때 앱을 구동한다. 앱이 특정 이벤트를 요청했다면, 시스템은 그 이벤트들을 처리하기 위해 백그라운드로 앱을 구동할 수 있다.

모든 앱은 UIApplication 객체와 UIApplicationDelegate 프로토콜을 준수하는 객체를 갖고, 여러 중요한 이벤트들에 대응한다. Scene 베이스 앱이더라도 앱 구동과 종료 등 중요 작업 관리를 위해 AppDelegate를 사용한다. 앱이 구동될 때 UIKit은 자동으로 UIApplication 객체와 델리게이트를 생성한다.

 

앱 데이터 구조 초기화

앱이 구동될 때 초기화하는 코드를 여기 넣는다. 

func UIApplicationDelegate.application(_:willFinishLaunchingWithOptions:)
func UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:)

UIKit은 앱 구동 사이클 시작할 때 이 메소드들을 호출한다. 이 메소드에는

  • 앱 데이터 구조를 초기화
  • 앱 동작에 필요한 자원들을 잘 갖고 있는지 확인 
  • 앱이 처음 시작할 때 한 번만 셋업해도 되는 작업
    • 서버에서 데이터를 다운로드
    • 사용자에 대한 기본적인 preference 설정
    • 사용자 계정이나 필요한 다른 데이터들 모아놓기
  • 앱 사용에 필요한 중요 서비스들을 연결. 예를 들어 앱이 remote notification을 지원한다면 APNs을 연결하는 등의 작업

등의 작업을 넣으면 된다. 특히 didFinish 같은 경우에는 UI가 스크린에 나타나기 전에 추가로 변경해야 할 작업들을 추가하면 된다. didFinish가 return되기 전에는 앱의 인터페이스를 나타내지 않기 때문이다. 위 메소드들은 가능한 일찍 return되는 게 좋긴 하다.

 

앱 구동 순서

  1. 사용자나 시스템이 앱을 구동하거나 prewarm
  2. 시스템이 Xcode가 제공하는 main() 함수를 실행
  3. main() 함수는 UIApplicationMain(_:_:_:_:)을 호출하는데, 여기서 UIApplication 객체와 델리게이트를 생성
  4. UIKit이 Info.plist이나 Target 속성의 커스텀 스토리보드를 로드하는데, 기본 스토리보드를 쓰지 않는다면 스킵
  5. UIKit이 AppDelegate의 application(_:willFinishLaunchingWithOptions:) 호출
  6. UIKit이 상태 복원 수행 -> AppDelegate와 뷰컨트롤러의 메소드 수행
  7. UIKit은 AppDelegate의 application(_:didFinishLaunchingWithOptions:) 호출

prewarm에 대해서는 설명이 좀 필요한데 iOS 15와 그 이상부터 기기 컨디션에 따라 시스템이 앱을 미리 데펴(...)놓는다. 

 

 

UIApplicationDelegate

간단하게만 살펴보자.

  • 앱을 시작할 때
// 구동 프로세스가 시작되고 메인 스토리보드나 nib 파일이 로드됐지만 아직 상태 복원이 일어나지 않았을 때 호출
// 외부 URL 등으로 앱이 켜졌을 때는 시스템이 application(_:open:options:) 메소드를 추가로 호출하기도 함
// 앱을 초기화하고 구동 준비를 하는 데 필요한 코드를 추가할 것
// 홈스크린 퀵 액션으로 앱을 구동할 수도 있는데, 대비하기 위해 application(_:performActionFor:completionHandler:) 구현할 것
func application(_:willFinishLaunchingWithOptions:) -> Bool

// 구동 프로세스가 거의 끝났고 앱이 구동될 준비가 됐을 때 호출
// URL 리소스나 유저 액티비티 핸들링을 못할 때만 false 반환
func application(_:didFinishLaunchingWithOptions:) -> Bool
  • scene을 설정하고 버릴 때
// 새로운 scene을 생성하는 데 필요한 설정 데이터를 가져오는 메소드
// Info.plist에 scene-configuration 데이터가 없거나 동적으로 데이터를 변경해야 할 때 구현
// UIKit은 이 메소드를 새로운 scene 생성하기 직전에 호출
func application(_:configurationForConnecting:options:) -> UISceneConfiguration

// 사용자가 앱 스위처에서 하나 혹은 그 이상의 scene을 닫을 때 호출
func application(_:didDiscardSceneSessions:)
  • 앱 라이프사이클 이벤트에 대응
// 앱이 active 상태가 됐을 때 호출
// scene을 사용 중이라면 이 메소드는 호출되지 않음. 그 경우 sceneDidBecomeActive(_:) 호출
// scene 사용 여부와 관계없이 이 메소드 호출 후 UIKit은 didBecomeActiveNotifcation post
func applicationDidBecomeActive(_:)

// 앱이 inactive 상태가 됐을 때 호출
// scene을 사용 중이라면 이 메소드는 호출되지 않음. 그 경우 sceneWillResignActive(_:) 호출
// scene 사용 여부와 관계없이 이 메소드 호출 후 UIKit은 willResignActiveNotification post
// 전화나 문자가 오거나 앱 종료, 백그라운드로 가는 등의 인터럽트로 inactive 상태가 됨
func applicationWillResignActive(_:)

//

 

 

(!!!) scene 지원을 하려면 Info.plist에 UIApplicationSceneManifest key를 추가하면 된다.

보통 새로 만드는 UIKit 앱의 info.plist는 이렇게 생겼다

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>UIApplicationSceneManifest</key>
	<dict>
		<key>UIApplicationSupportsMultipleScenes</key>
		<false/>
		<key>UISceneConfigurations</key>
		<dict>
			<key>UIWindowSceneSessionRoleApplication</key>
			<array>
				<dict>
					<key>UISceneConfigurationName</key>
					<string>Default Configuration</string>
					<key>UISceneDelegateClassName</key>
					<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
				</dict>
			</array>
		</dict>
	</dict>
</dict>
</plist>

Scene Manifest는 scene에 대한 delegate 클래스와 이름을 정의하고 있다. UIWindowSceneSessionRoleApplication은 Array로 되어 있는데, 여러 scene에 대해 sceneDelegate를 지정해줄 수 있는 것이다. 물론 이 Array에 Item을 2개 이상 쓰고 scene과 sceneDelegate 추가하려면 UIApplicationSupportsMultipleScenes를 true로 바꿔줘야 가능하다. 보통 이렇게 multiple windows 허용하면 iPadOS나 macOS에서 하나의 앱인데 2개 이상의 창을 띄우는 게 가능해진다. 

 

(?) SceneDelegate가 도입돼서 뭐가 달라진 건가?

iOS 13부터 애플은 AppDelegate에서 맡던 작업 몇 가지를 SceneDelegate로 옮겼다. 여전히 AppDelegate가 앱의 메인 시작점인 건 맞다. 

func application(_:didFinishLaunchingWithOptions:) -> Bool
func application(_:configurationForConnecting:options:) -> UISceneConfiguration
func application(_:didDiscardSceneSessions:)

application(_:didFinishLaunchingWithOptions:)이 제일 먼저 실행되는데, 여기서는 앱 세팅을 한다. (인증 정보라든지..) iOS 13 미만 버전에서는 UIWindow를 생성하고 설정한 다음 UIViewController 객체를 윈도우에 띄우는 작업도 여기서 할 수 있다.

만약 scene을 사용한다면 AppDelegate가 UIWindow를 설정할 필요가 없다. 앱이 이제 2개 이상의 창을 가질 수 있기 때문에 AppDelegate에서 단일 윈도우 객체를 관리하지 않는다. (하지만 iOS 13 미만 버전까지 지원한다면 AppDelegate에 윈도우 설정 코드가 필요하긴 하다)

앱이 새로운 scene이나 window가 생길 거라는 걸 예측할 때마다 application(_:configurationForConnecting:options:)이 호출된다. 앱이 최초 실행될 때 호출되는 메소드가 아니라 새로운 scene을 생성하거나 가져올 때만 호출되는 메소드이다. 

application(_:didDiscardSceneSessions:)는 사용자가 scene을 버릴 때 호출되는 메소드이다. 즉, 그 scene을 더이상 사용하지 않는다는 의미로 받아들이면 된다.

 

실제로 고려적 AppDelegate 앱에 SceneDelegate를 도입할 때는 무엇을 고려해야 할까?

 

appDelegate만 있던 프로젝트에 sceneDelegate 추가하기

iOS 개발자라면 iOS 13과 함께 등장한 sceneDelegate를 모를 수가 없을 텐데, 우리 앱은 최소 지원 버전이 iOS 11이다. 지금까지는 sceneDelegate를 도입하지 않고 AppDelegate만 관리했다. 어쨌든 Info에 scene manif

dev-dain.tistory.com

 

 

 

References

 

728x90
반응형
Comments