iOS App LifeCycle 설명하기 (의역)

EmilY
16 min readJan 26, 2021

--

‼️ iOS App Lifecycle Explained 를 의역했습니다. 더 정확한 내용은 원문을 참고해주세요.

iOS 라이프 사이클이 어떻게 동작하는지에 대한 질문이 들어왔다. 이 문제를 적절하게 말하기 위해, 당신은 우리가 두 가지 방법으로 iOS의 라이프사이클과 상호작용한다는 걸 알아야 한다:

  • AppDelegate의 몇 가지 메소드(`application(_:didFinishLaunchingWithOptions:)`)를 오버라이딩하여 라이프사이클의 단계 변화에 대응한다.
  • `UIApplication.shared.applicationState`를 체크하여 앱이 어떤 상태에 있는지 알 수 있다. 해당 enum은 세 가지 값을 가진다: `active`, `inactive`, `background` Note: 해당 property는 메인 스레드에서만 접근할 수 있다.

Note: 라이프 사이클 변화에 대응하는 다른 매커니즘이 있다. 우리는 `NotificationCenter`에 `observer`를 등록할 수 있다. 우리는 `UIApplication` 네임스페이스에 속하는 `Notification`들의 이름에 반응할 수 있다. 그러나, 이 메소드는 `AppDelegate`에 있는 오버라이딩 메소드와 비슷하다.

질문은 다음과 같다: `UIApplication.State`와 `AppDelegate` 메소드 사이의 관계는 무엇인가? 이러한 메소드들이 발생될 때 어떻게 상태가 변화하는가?

이상하게도, 우리는 문서에 관계를 명확하게 설명하는 어떠한 관련된 조각도 찾을 수 없을 것이다. 우리는 app lifecycle에 대한 많은 정보를 찾는다. `UIApplication.State` 프로퍼티의 문서에서도 찾는다. 하지만 이것을 같이 명확하게 설명하는 곳을 찾을 수 없다.

따라서, 어떻게 두 가지가 상호작용하는지 알아보는 작은 실험을 해보기로 했다.

The Experiment

실험은 간단하다. 앱을 백그라운드로 돌렸을 때 백그라운드 시간이 얼마나 남았는지 탐색할 때 했던 작업이 약간 떠오른다.

우리는 초보적인 앱을 만들것이다. 이 앱은 라이프 사이클 이벤트가 발생하면 iOS에 의해 발생되는 이벤트들을 모은다. 그리고 `UIApplication.State` 프로퍼티의 현재 값을 이벤트와 연관짓는다. 우리는 이벤트를 타임스탬프로 장식하여 그들의 타임라인을 나타낼 수 있다. 마지막 은 이것들을 즉시 디스크에 저장하여 기록을 잃지 않게 한다. 특히 드문 케이스를 조사할 때 중요하다.

앱의 마지막 스탭 UI : 나는 기본적인 테이블 뷰를 사용한다. 각 이벤트가 cell에 랜더링 될 것이다.

Preparing the Models

이 앱의 모델은 두 개다 : 우리는 `state`마다 이벤트를 저장해야 한다. 이벤트는 디스크에 인코딩되어 저장된다. 매 이벤트는 세 가지 프로퍼티를 가진다: `kind`, `date` 그리고 연관된 `UIApplication.State`

모델 코드는 이렇다:

struct State: Codable {
var events: [Event] = []
}
struct Event: Codable {
let kind: Kind
let date: Date
let applicationStatus: String
enum Kind: String, Codable {
case appWillFinishLaunching
case appDidFinishLaunching
case appWillTerminate
case appWillResignActive
case appDidBecomeActive
case appDidEnterBackground
case appWillEnterForeground
}
}
// Helpers
extension State {
func byAppending(event: Event) -> State {
var newState = self
newState.events.append(event)
return newState
}
}
extension Event {
init(with application: UIApplication, event kind: Kind) {
self = Event(
kind: kind,
date: Date(),
applicationStatus: application.applicationState.humanReadableDescription
)
}
}

코드에서 우리는 몇가지 디테일을 볼 수 있다:

  1. `Event.Kind`는 enumeration이다. 이것은 우리가 추적하려는 라이프 사이클 이벤트를 전부 포함한다.
  2. 나는 몇가지 extension을 추가했다. 이것은 새로운 `State`를 생성하는 데 도움을 줄 것이다. 새로운 `State`는 이전 것에 새로운 이벤트를 추가한다. 그리고 이벤트에 대한 다른 `init`을 추가하였다.

Storing and Retrieving the State

앱의 다른 중요한 요소는 매커니즘이다:

  • State가 바뀔때 항상 저장하라.
  • 앱이 런칭될 때 State를 가져온다.

단순함을 위해서, `UserDefault`를 이용해서 `State`를 저장했다.

상태를 저장하고 가져오는 코드는 이렇다 :

extension AppDelegate {
static let stateKey = "persistence_state_key"
func retrieveState() -> State {
let storedState = UserDefaults.standard
.data(forKey: Self.stateKey)
.flatMap { try? JSONDecoder().decode(State.self, from: $0) }
guard let state = storedState else {
return State()
}
return state
}
func store(state: State) {
guard let stateData = try? JSONEncoder().encode(state) else { return }
UserDefaults.standard.setValue(stateData, forKey: Self.stateKey)
}
}

여기서 우리는 static key를 생성하여 정보를 UserDefaults에 저장한다. 그러고나서, `retrieveState()` 메소드에서, 우리는 `UserDefaults`에 접근하여 data를 가져와서 디코딩한다. 데이터가 없을 경우를 대비해서 우리는 해당 launch에 대해 새로운 struct를 생성한다.

`store(state:)` 메소드에서, 우리는 struct를 인코딩하고 그것을 user defaults에 저장한다.

앱은 AppDelegate:에 있는 이 메소드를 사용하여 `var state: State` 변수를 정의한다. 그리고 셋팅되면 자동적으로 새로운 값을 `UserDefaults`에 저장한다. 그러고 나서, `application(_:willFinishLaunchingWithOptions:)`를 오버라이딩하여 조만간 state를 생성한다.

  • Note: 우리는 `application(:willFinishLaunchingWithOptions:)`를 이용하여 `State`를 불러오고 application(_:didFinishLaunchingWithOptions:)는 쓰지 않는다. 앞전의 메소드가 이후의 메소드 전에 실행되고 만약 `state`를 나중에 생성하면, 우리는 앞전 이벤트를 추적할 수 없다.

모든 코드를 합치면 이렇게 된다.

class AppDelegate: UIResponder, UIApplicationDelegate {  var window: UIWindow?  var state: State! {
didSet {
self.store(state: self.state)
**(self.window?.rootViewController as? ViewController)?.state = state**
}
}

func application(
_ application: UIApplication,
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
) -> Bool {
self.state = self.retrieveState()
let event = Event(with: application, event: .appWillFinishLaunching)
self.state = self.state.byAppending(event: event)
return true
}

// Other AppDelegate methods
}

7번째 줄에서, 우리는 매번 state가 변할 때마다, ViewController의 state도 업데이트되는 걸 볼 수 있다. 그리하여 UI에 업데이트 영향을 준다.

Tracking the Events

코드의 가장 중요한 마지막은 우리가 알길 원하는 모든 이벤트를 추적하는 것이다. Event.Kind enum으로, 우리는 이 이벤트들이 다음과 같은 타입을 가진다는 걸 알 수 있다:

  • appWillFinishLaunching
  • appDidFinishLaunching
  • appWillTerminate
  • appWillResignActive
  • appDidBecomeActive
  • appDidEnterBackground
  • appWillEnterForeground

우리는 쉽게 AppDelegate에 있는 모든 메소드들을 직접 오버라이딩 할 수 있다. 코드는 이렇게 생겼다:

class AppDelegate: UIResponder, UIApplicationDelegate {

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
self.window = UIWindow()
self.window?.rootViewController = ViewController(state: self.state)
self.window?.rootViewController?.view.backgroundColor = .purple
self.window?.makeKeyAndVisible()
self.updateState(with: application, event: .appDidFinishLaunching) return true
}
func applicationWillTerminate(_ application: UIApplication) {
self.updateState(with: application, event: .appWillTerminate)
}
func applicationDidBecomeActive(_ application: UIApplication) {
self.updateState(with: application, event: .appDidBecomeActive)
}
func applicationWillResignActive(_ application: UIApplication) {
self.updateState(with: application, event: .appWillResignActive)
}
func applicationDidEnterBackground(_ application: UIApplication) {
self.updateState(with: application, event: .appDidEnterBackground)
}
func applicationWillEnterForeground(_ application: UIApplication) {
self.updateState(with: application, event: .appWillEnterForeground)
}

// Helper
fileprivate func updateState(with application: UIApplication, event kind: Event.Kind) {
let event = Event(with: application, event: kind)
self.state = self.state.byAppending(event: event)
}
}

가장 흥미로운 점은 이것이다:

  • `application(_:didFinishLaunchingWithOptions:)` 메소드는 ViewController 생성하는 역할을 맡고 있고 스크린에 보이도록 한다.
  • `updateState(with:event:)`는 몇가지 명령어가 함께 있다. 따라서 계속해서 반복될 수 있다.

Running the app

이제 모든게 구현되었으니, 우리는 앱을 실행하고 어떻게 라이프 사이클 이벤트의 진화에 따라 `UIApplication.State`가 변하는지 볼 수 있다.

이 짧은 비디오에서, 우리는 앱 시작일 때 이벤트 나열을 볼 수 있다:

  1. `appWillFinishLaunching` → `UIApplication.State`는 `inactive`
  2. `appDidFinishLaunching` → `UIApplication.State`는 `inactive`
  3. `appDidBecomeActive` →` UIApplication.State`는 `active`

모든 이벤트동안, 앱은 포그라운드에 있고 보여진다.

대신, 앱을 백그라운드로 옮기고 다시 앱으로 돌아오면 이벤트 나열은 다음과 같다:

  1. `appWillResignActive` → `UIApplication.State`는 `active`
  2. `appDidEnterBackground` → `UIApplication.State`는 `background`
  3. `appWillEnterForeground` → `UIApplication.State`는 `background`
  4. `appDidBecomeActive` → `UIApplication.State`는 `active`

이 경우에, 앱은 `inactive` 상태가 되지 않지만 대신 `active`에서 `background`로 왔다갔다 한다. 또한, `appWillEnterForeground`가 수행될 때, 앱은 여전히 백그라운드 상태라는 것이다!

Edge Cases

살펴볼 만한 몇가지 드문 케이스가 있다. silent notification이나 background fetch 때문에 앱이 백그라운드에서 시작하면 무슨 일이 일어날까? 사용자가 앱 switcher를 발생시키면?

두 가지 경우를 살펴보자!

Background Launch

현대 어플리케이션은 시스템에 의해 launch되어서 앱이 열리자마자 사용자의 데이터를 준비하거나 새로운 컨텐츠를 사용할 수 있는 경우, 서버로부터 데이터를 다운로드 받는다.

이 모든 계산은 백그라운드에서 이루어진다. 심지어 사용자에게 알리지도 않는다. 시스템이 앱을 런칭하고, 앱이 정해진 시간 안에 몇가지 일을 수행할 수 있도록 해준다. 그리고 앱을 다시 잠들게 한다. 어떤 이벤트가 발생되는가? `UIApplication.State`는 이러한 상황에 대응하는가?

운 좋게도, 우리가 곧 만들어볼 코드는 이런 케이스에 적합하다. 우리가 해야할 일은 이 이벤트들 중 하나를 시뮬레이션해보는 것이다. background fetch를 시뮬레이션해보기 위해 다음과 같이 한다:

  1. Project Navigator에서 프로젝트를 선택한다
  2. `Signing and Capabilities` 탭을 선택한다
  3. `Capability` 버튼을 누른다
  4. `Background Modes`를 더블클릭한다
  5. 새로운 패널에, `Background Fetch`를 선택한다.

이 단계들은 백그라운드 패치를 발생하게 한다. 이제, 우리는 해당 모드로 앱을 런칭하면 된다. schema에서 동작을 설정할 수 있다.

  1. Xcode play 버튼 옆에 있는 앱 이름을 클릭한다.
  2. `Edit Schema`를 선택한다
  3. `Options`를 선택
  4. `Launch due to a background fetch ` 옵션을 누른다

이제, 앱을 런칭해보자. 당신은 앱의 팝업을 볼 수 없다. 대신, 당신은 시뮬레이터에 아이콘이 나타나는 것을 볼 것이다. 아이콘을 눌러서 앱을 런칭하자, 그러면 이런게 보일 것이다.

보는바와 같이, 이 경우에 `willFinishLaunching`과 `didFinishLaunching`은 `UIApplication.State`가 `background`이다. `active`를 가진 유일한 이벤트는 앱이 진짜로 활동할 때 뿐이다. 앱은 이 실행에서 절대 `inactive`가 되지 않음을 유념하자.

App Switcher

(참고 : app switcher란 홈버튼 더블클릭, indicator bar를 위로 스와이프해서 나오는 화면을 말한다.)

가장 마지막이다. 가장 흥미로운 점은 사용자가 app switcher를 열 때 어떤 일이 일어나느냐는 것이다. 우리의 코드는 이미 모든 이벤트를 추적하도록 설정되었으니 시뮬레이터로 액션이 발생하도록 하면 된다.

나는 이 단계가 조금 이상한 것을 발견했다: 비록 시뮬레이터가 App Switcher가 숏컷을 눌러서 발생될 수는 있지만…매번 작동하진 않는다. 그러나, 당신이 한번 성공하고 나면, 첫번째로 볼 수 있는 것은 `appWillResignActive` 이벤트이고 값은 `active`인 것을 볼 수 있다. 만약 앱으로 다시 돌아오면, `appDidBecomeActive` 이벤트가 따라오고 값은 `active`이다.

Conclusion

해당 아티클에서, 우리는 앱 라이프사이클과 상호작용하는 두 가지 매커니즘을 탐구했다. 이 매커니즘들은 서로 다르기 때문에 다른 작업에 사용되어야 한다. 그러나, 이들은 매우 엄격한 방식으로 관련되어 있다 : 라이프사이클 메소드가 발생되면, `UIApplication.State`는 바뀔 수 있다는 점이다…그리고 우리가 예상한 대로 항상 값이 변하는 건 아니라는 점이다.

😭 파파고를 돌렸지만 그래도 해석이 명확하지 않은 부분이 많네요. 틀린 부분이 있으면 지적해주세요 감사합니다.

--

--

EmilY
EmilY

Written by EmilY

iOS 하나부터 열까지 이해하기

Responses (1)