‼️ 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
)
}
}
코드에서 우리는 몇가지 디테일을 볼 수 있다:
- `Event.Kind`는 enumeration이다. 이것은 우리가 추적하려는 라이프 사이클 이벤트를 전부 포함한다.
- 나는 몇가지 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`가 변하는지 볼 수 있다.
이 짧은 비디오에서, 우리는 앱 시작일 때 이벤트 나열을 볼 수 있다:
- `appWillFinishLaunching` → `UIApplication.State`는 `inactive`
- `appDidFinishLaunching` → `UIApplication.State`는 `inactive`
- `appDidBecomeActive` →` UIApplication.State`는 `active`
모든 이벤트동안, 앱은 포그라운드에 있고 보여진다.
대신, 앱을 백그라운드로 옮기고 다시 앱으로 돌아오면 이벤트 나열은 다음과 같다:
- `appWillResignActive` → `UIApplication.State`는 `active`
- `appDidEnterBackground` → `UIApplication.State`는 `background`
- `appWillEnterForeground` → `UIApplication.State`는 `background`
- `appDidBecomeActive` → `UIApplication.State`는 `active`
이 경우에, 앱은 `inactive` 상태가 되지 않지만 대신 `active`에서 `background`로 왔다갔다 한다. 또한, `appWillEnterForeground`가 수행될 때, 앱은 여전히 백그라운드 상태라는 것이다!
Edge Cases
살펴볼 만한 몇가지 드문 케이스가 있다. silent notification이나 background fetch 때문에 앱이 백그라운드에서 시작하면 무슨 일이 일어날까? 사용자가 앱 switcher를 발생시키면?
두 가지 경우를 살펴보자!
Background Launch
현대 어플리케이션은 시스템에 의해 launch되어서 앱이 열리자마자 사용자의 데이터를 준비하거나 새로운 컨텐츠를 사용할 수 있는 경우, 서버로부터 데이터를 다운로드 받는다.
이 모든 계산은 백그라운드에서 이루어진다. 심지어 사용자에게 알리지도 않는다. 시스템이 앱을 런칭하고, 앱이 정해진 시간 안에 몇가지 일을 수행할 수 있도록 해준다. 그리고 앱을 다시 잠들게 한다. 어떤 이벤트가 발생되는가? `UIApplication.State`는 이러한 상황에 대응하는가?
운 좋게도, 우리가 곧 만들어볼 코드는 이런 케이스에 적합하다. 우리가 해야할 일은 이 이벤트들 중 하나를 시뮬레이션해보는 것이다. background fetch를 시뮬레이션해보기 위해 다음과 같이 한다:
- Project Navigator에서 프로젝트를 선택한다
- `Signing and Capabilities` 탭을 선택한다
- `Capability` 버튼을 누른다
- `Background Modes`를 더블클릭한다
- 새로운 패널에, `Background Fetch`를 선택한다.
이 단계들은 백그라운드 패치를 발생하게 한다. 이제, 우리는 해당 모드로 앱을 런칭하면 된다. schema에서 동작을 설정할 수 있다.
- Xcode play 버튼 옆에 있는 앱 이름을 클릭한다.
- `Edit Schema`를 선택한다
- `Options`를 선택
- `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`는 바뀔 수 있다는 점이다…그리고 우리가 예상한 대로 항상 값이 변하는 건 아니라는 점이다.
😭 파파고를 돌렸지만 그래도 해석이 명확하지 않은 부분이 많네요. 틀린 부분이 있으면 지적해주세요 감사합니다.