[iOS] Architecting Your App for Multiple Windows

MSDev·2022년 7월 12일
0

오늘은 Architecting Your App for Multiple Windows 라는 주제의 WWDC 영상을 보고 정리하고 공부해보겠습니다.

목차는 크게

1. Changes to app lifecycle

2. Using the scene delegate

3. Architecture

의 순서로 이루어져 있습니다.


Changes to app lifecylce

먼저 iOS 13 이전의 버전에서 App Delegate의 역할에 대해서 보겠습니다.

위의 그림과 같이 App Delegate는 앱의 process level event(앱이 켜지고 꺼지는 등)를 알려주는 역할앱 UI의 state(상태) 변화를 앱에 알리는 역할 크게 두 가지의 주요한 역할을 가지고 있었습니다.
(앱의 생명주기(앱의 실행과 종료 등)와 UI 라이프 사이클(백그라운드 상태 로직 등)을 모두 App Delegate에서 처리했습니다.)

아래의 구조처럼 iOS 13 이전에는 하나의 앱은 하나의 UI 인스턴스만을 가지고 있었습니다. 이 말은 앱 하나당 하나의 window를 가지고 있다는 말과 같습니다.

그렇게 때문에 App Delegate에서 아래 그림과 같이 non-UI setup인 global setup이나 UI setup을 함께 작성했었습니다.

하지만 iOS 13부터는 하나의 앱은 하나의 프로세스를 가지지만 여러 UI 인스턴스나 scene session들을 가질 수 있기 때문에 이를 관리해줄 객체가 필요해졌습니다.
그렇게 도입된 게 SceneDelegate 입니다!

그 결과 App Delegate는 Process Lifecycle 역할은 그대로 가져가면서 UI Lifecycle에 대한 역할은 Scene Delegate로 넘겨주게 됩니다.

대신 App Delegate는 Session Lifecycle을 다루는 부분이 추가되었습니다.
scene session이 생성되거나 사라지는 경우에 알려주게 되는 것이죠.

따라서 UI 상태에 관한 App Delegate의 메서드들은 Scene Delegate의 메서드들로 대체되어 호출됩니다.
대부분 App Delegate에 있었던 함수들과 1대1 대응이 되기 때문에 크게 달라지진 않았습니다.

실제로 iOS 13부터 애플리케이션이 scene 생명주기를 채택하면, UIKit은 UI state와 관련된 App Delegate 메서드 호출을 중단하고 새로운 Scene Delegate 메서드를 호출하게 됩니다.

예를 들어 아래의 그림처럼 App Delegate와 Scene Delegate 모두에서 DidEnterBackground의 메서드를 작성한다고 한다면 Scene Delegate의 메서드만 실행되어 "scene delegate - did eneter background" 만이 출력될 것입니다.

그럼 여기서 의문이 생길 수 있습니다.
그럼 iOS 13에서 멀티 window 지원을 하려는 경우 iOS 13 이전의 앱들은 지원을 중단해야하는 거야?
그렇지 않습니다! 다시 배포를 할 때 위의 2가지 경우에 대해서 모두 작성을 해놓고 유지한다면 UIKit이 알아서 런타임에 올바른 메서드들을 불러주게 됩니다.


Using the scene delegate

자 그럼 iOS 13부터 애플리케이션을 실행하면 어떻게 진행되는지 자세히 살펴보겠습니다.

하나의 앱을 실행했다고 가정해봅시다.

그럼 먼저 이전과 동일하게 App Delegate에서 didFinishLaunchingWithOptions 메서드를 호출하게 됩니다. 여기서는 위에서 본 데이터베이스 연결 등 UI가 아닌 일회성 작업을 작성하면 좋습니다.

   func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }

직후 시스템은 scene session을 생성합니다. 이 때 scene을 생성하기 위해 어떻게 구성되어있는지 알기 위해 시스템은 UISceneConfiguration을 요청합니다.

UISceneConfiguration은 특정 scene을 만들 때 사용할 UIKit의 객체들과 스토리보드에 관한 정보입니다.
특히 특정 scene의 클래스, 앱이 해당 유형의 scene을 관리하는 데 사용하는 scene delegate의 클래스, scene의 initial view controller를 포함하고 있는 storyboard를 정의해주어야 합니다.

이러한 configuration은 코드를 통해 동적으로 정의할 수도 있고, info.plist 파일을 통해 정적으로 정의할 수도 있습니다(info.plist를 활용하는 것이 선호된다고 하네요).

앱은 main scene configuration과 accessory scene configuration을 가질 수 있기 때문에 options 매개변수들을 보고 이를 context로 사용해서 올바른 scene configuration을 선택해줘야 합니다.

예를 들어 info.plist 안에 정의를 해두었다면, 아래의 코드와 같이 scene의 name 파라미터를 통해 가져올 수 있습니다.

아래의 메서드는 새로운 scene session을 생성할 때마다 불립니다.

  func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
        
    }

자 이제 앱이 런치됐고 scene session을 가지게 됐습니다. 하지만 아직 UI는 볼 수 없습니다.
UI를 보이게 해주는 부분은 Scene Delegate의 willConnectTo 메서드를 통해서 가능합니다.

windowScene을 통해 새로운 UIWindow를 초기화한 후, window를 구성할 때 user activities(사용자 활동), state restoration activities(상태 복원 활동) 관련성을 체크해야 합니다. 이 부분은 글의 뒷부분에 다시 한 번 더 살펴보도록 하고 자 이제 우리는 앱의 UI를 볼 수 있게 됩니다!

 func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
        guard let _ = (scene as? UIWindowScene) else { return }
    }

그럼 만약에 사용자가 홈 화면으로 가기 위해 홈버튼을 눌러 홈화면으로 돌아가게 되거나 위로 스와이프 해서 앱을 switcher로 보내면 어떻게 될까요?
이 때는 각각 Scene Delegate의 didEnterBackground와 willResignActive 메서드가 호출되어 scene을 background로 보냅니다. 그러다가 어느 시점이 되면 didDisconnect가 호출되어 scene의 연결이 해제될 수 있습니다.

scene의 연결을 해제한다는 말은 무슨 뜻일까요?

자원의 효율을 위해 시스템은 사용하지도 않는 scene에게 자원을 주고 싶지 않겠죠? 그렇기 때문에 background에 있는 scene을 메모리에서 해제해버린다는 말입니다. scene이 메모리에서 해제되면 이를 보유한 모든 window 계층, view 계층도 해제되어 버립니다.

하지만 scene이 나중에 다시 연결되거나 반환될 수도 있기 때문에 사용자 데이터나 상태를 영구적으로 삭제하지는 않아야 합니다.

그럼 사용자가 직접 app switcher에서 scene을 위로 스와이프해 없애는 경우는 어떻게 될까요?

이 때는 App Delegate의 didDiscardSceneSessions 메서드를 호출하게 됩니다.

didDiscardSceneSessions 메서드를 호출하면 텍스트 편집 앱에서 저장되지 않은 텍스트와 같이 scene에 관련해 유저 상태나 데이터가 삭제되게 됩니다.

만약 process가 not running 상태라면 시스템은 제거된 session을 계속해서 tracking하고 있다가 다음 번 해당 앱이 실행될 때 이것을 불러오게 됩니다.

Architecture

이번에는 architecture pattern에 대해 살펴보겠습니다.

State Restoration

먼저 state restoration에 대해 얘기해보겠습니다.
state restoration은 window를 구성할 때 체크해야하는 부분이라고 앞서서 말했었습니다.
간단하게 예를 들면 어떤 작업을 하다가 앱을 백그라운드로 보냈을 때, 다시 앱을 켜면 그 상태가 보존되어 있어서 작업을 이어서 하게 해주는 것이 state restoration 입니다.

iOS 13부터 앱은 scene 기반 상태 복원을 구현하는 것이 중요합니다.

예시를 보죠

하나의 문서 앱에서 서로 다른 4개의 session이 열려 있었는데, Packing List와 Agenda에 집중하고 있다고 합시다.
그럼 어느 시점에 Road Trip과 Attendees는 시스템에 의해 release되고 disconnect 됩니다.

여기서 state restoration을 구현하지 않았다면 Road Trip으로 앱을 switch 하였을 때 저장하지 않은 작성했던 정보들은 남아있지 않을 것입니다.

이것을 어떻게 해결할 수 있을까요?

이를 해결하기 위해 iOS 13은 새로운 scene-based state restoration API를 가지고 있습니다.
기존에는 state restoration을 위해 view hierachy를 encoding 했지만 이 API는 view hierachy를 encoding 하는 것이 아니라 window를 다시 만들 수 있게 하는 state를 encoding 하는 방식으로 작동합니다.

이것은 NSUserActivity를 기반으로 하기 때문에 애플리케이션이 spotlight 검색이나 handoff와 같은 기능들을 활용하는 경우 동일한 활동을 사용하여 앱의 상태를 encoding할 수 있습니다. 또한 iOS 13에서는 시스템에 반환하는 state restoration archive는 나머지 앱의 데이터 보호 등급과 일치합니다. (무슨 말일까요... 이 부분은 다시 알아보고 좀 더 쉽게 작성해봐야겠어요)

이러한 과정을 코드에서는 어떻게 사용할까요?

아래 하이라이트된 코드는 Scene Delegate에서 scene에 대한 state restoration activity를 구현하고 현재 window에서 가장 유저 활동이 활발한 것을 찾는 메소드(fetchCurrentUserActivity)를 호출하는 부분입니다.

잠시 후 scene이 foreground로 다시 들어오는 순간 connect되고, 해당 session에 state restoration activity가 있는지 확인하고 있다면 이를 사용해 다시 설정해주고 없다면 아무런 state가 없는 새로운 윈도우를 만들어 내게 됩니다.

이것은 사용자는 background에서 scene의 연결이 끊기는 것을 알 수 없다는 것을 의미합니다.
왜냐하면 다시 켰을 때는 상태를 복원해주는 함수들이 있기 때문입니다. 그러나 실제로는 background에서 scene의 연결은 끊어집니다.

Keeping Scenes in Sync

마지막으로 multiple window를 지원하는 앱을 구현할 때 발생가능한 중요한 issue에 대해 알아보도록 하겠습니다.

바로 아래 그림처럼 scene이 동시에 켜져있는 경우에 sync를 맞춰주는 것에 대한 issue 입니다.

같은 화면에서 문자를 보냈지만 한 쪽만 문자가 가고 다른 쪽은 문자가 발송되었다는 것을 UI에 업데이트해주고 있지 않습니다.

이런 문제가 발생하는 이유는
바로 iOS 앱 개발의 구조는 아래 그림과 같은 구조를 따르고 있기 때문입니다.

즉, button을 눌렀을 때 view controller가 event를 받고, view controller가 스스로 자신의 UI를 업데이트하게 되고 이를 model에 알리고 model Controller가 업데이트되는 구조입니다.

하나의 UI 객체를 다룰 때에는 괜찮은 구조이지만 다른 scene에 있지만 같은 데이터를 보여주는 second view controller는 새로운 event가 발생했고 이에 맞춰서 UI 업데이트를 해줘야한다는 것을 모르는 문제가 발생합니다.

이를 해결하는 방법은 view controller가 event를 수신하면 model controller에게 알리는 것입니다.
이렇게 되면 model controller가 관련된 subscriber들이나 view controller들에게 새로운 데이터로 업데이트하라고 알릴 수 있게 되는 것이죠.

이것을 구현하는 데에는 delegate, notification, combine 등 다양한 방법이 있지만 영상에서는 notification으로 구현했습니다.

예제를 보겠습니다.
아래 코드는 이를 반영하지 않은 코드입니다.
2개의 UI가 있더라도 하나의 view controller만 수정되던 상황입니다.

가장 먼저 해야 할 일은 view controller가 자체 view state를 변경하지 않아야 한다는 것입니다.

이제 model controller의 add 메서드에 대해 보겠습니다.
add 메서드의 역할은 새로운 메시지를 계속 추가하는 것입니다. 그런 뒤에 연결된 view controller나 scene에 업데이트가 있다고 계속 알리기를 원하는 데 이것은 어떻게 구현할 수 있을까요?

이를 형식화하고 쉽게 디버깅하고 테스트할 수 있도록 enum 타입을 활용해 구현해보겠습니다.
NewMessage(message: Message)는 model controller가 새로운 메시지를 받게되면 생성되고 이를 관련된 view controller나 scene에게 보내줄 것입니다.

이를 알리기 위해서 NSNotificationCenter를 사용한 백업 저장소를 사용했고 새로운 업데이트 이벤트를 생성한 뒤 연결된 곳으로 보내는 notification 방법을 추가합니다.
여기서 notification 객체에 업데이트 객체 자체를 포함한다는 것이 중요합니다.

이제 model controller가 새 메시지를 받았을 때 새로운 이벤트를 만들고 이르 알릴 수 있게 되었습니다.

그런 뒤 view controller에서는 새로운 이벤트를 관찰하면 됩니다. observer를 추가해서 아까 만든 이벤트를 관찰하고 해당 이벤트가 발생하면 작동하는 handler 메서드를 만들어서 해당 이벤트를 수행하면 됩니다.

이렇게 Architecting Your App for Multiple Windows WWDC 영상을 보고 공부해봤는데요! 너무 어려워요...

영상을 보고 정리하면서 Scene이나 Session, Window라는 용어들이 정확히 어떤 것을 의미하는지 제대로 알지 못하는 것 같아서 간단하게 알아봤지만 확실한 이해가 필요한 것 같아 이 부분도 다시 공부해봐야겠어요.

그래도 Scene Delegate가 필요한 이유와 무심코 쓰던 App Delegate와 Scene Delegate들의 메서드들에 대해서 많이 알게 됐던 영상이었습니다.

다음에 multiple window를 지원하는 앱도 뚝딱 만들 수 있는 날이 오기를 바라며...


[참고]

profile
iOS 개발자

0개의 댓글