WWDC 2022에서 Stormy: A strom photos 앱을 소개하면서 BackgrondTask를 소개하였습니다.
앱의 간단한 소개
: 폭풍우 치는 날 하늘 사진을 찍을 수 있도록 백그라운드 작업을 이용해 사용자에게 알림을 주는 어플입니다.
: 백그라운드에서 알림을 탭하여 사진을 찍게되면 백그라운드에서 사진이 업로드가 됩니다.
Background Task는 watchOS, iOS, tvOS, MacOS, Widgets 과같은 모든 플랫폼에서 적용할 수 있는 기술입니다.
BackgroundTask란
앱에서 백그라운드 작업을 가능하게 해줍니다
backgroundTask(_:action:) scene modifier와 함께 이 유형의 값을 사용하여 시스템이 앱이나 확장 프로그램으로 보내는 백그라운드 작업에 대한 핸들러를 만드세요. 예를 들어, urlSession을 사용하여 백그라운드 URLSession의 응답을 처리하기 위해 앱이나 확장을 시작할 때 시스템이 호출하는 비동기 폐쇄를 정의할 수 있습니다.
Background Task in SwiftUI
Swift Concurrency를 활용하는 API 호출을 통해 앱이 시간에 맞춰 작업을 완료하도록 도움으로써 시스템이 백그라운드에서 앱을 종료하는 것을 피합니다.
Background에서 알림이 전송되는 과정
다음은 백그라운드 작업이 진행되는 전체 그림입니다.
먼저 가장 왼쪽에 있는 그래프는 앱이 포어 그라운드에서 작업이 수행되는 시간입니다.
다음으로는 앱이 백그라운드에서 작업을 수행하는 시간입니다.
다음은 시스템에서 작업이 진행되는 총 시간입니다.
앱이 처음으로 실행했을 때 정오에 새로고침 작업을 예약을 요청합니다.
시스템은 앱이 예약한 시간에 백그라운드에서 앱을 깨워야 한다는 것을 기억하다가 정오에 앱을 예약했기 때문에 시스템은 정오에 백그라운드에서 앱을 깨우고 백그라운드 앱 새로고침 작업을 내보냅니다.
이떄 폭풍우가 오는지를 확인해야 하기 때문에 백그라운드 작업에서 URLSession 요청을 시스템에 요청하게 됩니다.
이후 백그라운드에 예약된 URLSession으로 앱은 일시 정지 상태로도 네트워크 요청이 완료될 때까지 기다릴 수 있습니다.
백그라운드 네트워크 요청으로 날씨 데이터를 받으면 새로운 URLSession 백그라운드 작업으로 앱에 백그라운드 실행 시간이 다시 주어집니다.
요청한 날씨 데이터를 입수하면 앱은 밖에 폭풍우가 치는지 판단하고 사진을 찍으라는 알림을 앱에서 시스템으로 보낼 것인지 결정합니다.
이후 URLSession 작업이 오나료되었으므로 시스템은 다시 한번 앱을 일시정지합니다.
백그라운드 런타임 정리
1. 포어그라운드에서 예약한 시간에 시스템이 앱을 깨워서 백그라운드 작업을 실행시킵니다.
2. 백그라운드 작업에서 시스템에게 URLSession으로 밖에 폭풍우가 오는기 네트워크 요청을 합니다.
3. 앱이 작업을 수행할 백그라운드 실행 시간을 모두 소비했다면 시스템이 앱에 시간이 부족하다고 알려서 상황에 대비할 기회를 줍니다.
실행 시간이 만료될 떄까지 앱이 백그라운드 작업을 완료하지 못하면 앱은 시스템에 의해 중단되고 다음 백그라운드 작업을 위해 스토틀링을 일으킵니다.
- 이 경우에 네트워크 요청이 백그라운드 네트워크 요청인지를 확인해야 합니다.
- 만일 백그라운드 네트워크 요청이라면 앱의 새로고침 작업을 즉시 완료하고 네트워크 요청이 들어오면 백그라운드 실행 시간을 위해서 앱을 깨울 수 있게 됩니다.
4. 이후 백그라운드 URLSession이 예약되면 다시 시스템은 앱을 일시 정지할 수 있습니다.
- 만약 폭풍우를 확인하는 URLSession 네트워크 요청이였다면 앱 새로고침 작업을 즉시 완료하고 네트워크 요청이 들어오면 추가 백그라운드 실행 시간을 위해서 앱을 깨울 수 있습니다.
앱 코드
다음 날 정오에 백그라운드 새로 고침을 예약하는 코드입니다.
- 먼저 다음날을 정의하는 noon 변수를 만듭니다.
- 그다음 백그라운드에서 앱을 새로고침하는 요청을 만듭니다.
- 이때 요청의 가장 빠른 시간을 다음 날 정오로 설정합니다.
- 다음 BGTaskScheduler에 제출합니다.
다음은 예약한 백그라운드 작업을 해당 Handler에 등록하려면 새 백그라운드 작업 Scene modifier를 이용해야 합니다.
- 앱이 백그라운드 작업을 받으면 백그라운드 작업과 일치하는 modifier에 등록된 모든 블록이 실행됩니다.
- 아래 코드에서는 appRefresh를 사용하여 원하는 날짜에 백그라운드 실행 시간을 미리 예약해서 앱에 제공할 수 있게 하였습니다.
- 아래처럼 백그라운드 작업 수정자에서 요청 및 핸들러와 같은 식별자를 사용하면 앱이 해당 작업을 받을 때 시스템이 호출할 handler를 식별할 수 있게 됩니다.
backgroundTask
- 위에 코드를 그대로 사용하면 일회성이 될 수 있습니다. 하지만 backgroundTask에 scheduleAppRefresh() 함수를 넣게 된다면 재귀호출되어 매일 정오때마다 백그라운드 작업을 수행할 수 있도록 합니다.
- 다음으로 if await isStormy() 함수를 통해 네트워크 요청을 만들고 값이 반환되어 폭풍우가 오고 있는 상황일 때 알림을 띄울 작업을 if문 안에 넣어줍니다.
- backgroundTask 클로저가 끝나게 된다면 시스템을 앱을 다시 종료하게 됩니다.
notifyForPhoto 함수
- Swift Concurrency를 사용하게 되면 작업이 완료됐을 때 명시적인 콜백 없이 백그라운드에서 장기 실행 작업을 수행할 수 있습니다.
- 위 notifyForPhoto() async 함수에서 UNUserNotificationCenter 를 통해서 비동기 알림 추가 메서드로 알림을 간단하게 사용자한테 표시할 수 있습니다.
isStormy 함수
개선 전
- isStormy async 함수는 바깥 날씨를 확인하는 네트워크 요청을 해야 합니다.
- 먼저 URLSession과 URLRequest를 통해서 세션과 URL을 설정합니다.
- 이후 response를 통해서 request로 지정된 URL에서 날씨 데이터를 가져오고 await 키워드를 사용해서 작업이 완료될 때 까지 아래 명령어들이 동시에 수행되지 않도록 합니다.
- 만일 네트워크 응답으로 데이터를 가져오게 되면 해당 데이터를 읽고 결과를 반환하며 데이터가 유효하지 않으면 false를 반환합니다.
하지만 위처럼 코드를 만들게 된다면 아래처럼 서버와의 신호가 약하거나 다른 문제로 인해 주어진 실행 시간이 만료되기 전에 앱이 네트워크 요청을 완료하지 못하게 됐을 때 문제가 발생합니다
-> 이 경우 URLSession을 Background URLSession으로 설정했는지 확인하고 URLSessionConfiguration.background 작업을 이용하여 앱의 시작 이벤트를 살펴봐야 합니다.
개선 후
주어진 실행 시간이 만료된 후에서 앱이 네트워크 요청을 진행할 수 있도록 하기 위해서는 Background URLSession request를 사용해야 합니다.
- 이때 기존 URLSession을 사용하는 대신 URLSessionConfiguration에서 background를 사용하여 세션을 구성합니다.
- 여기서 중요한 것은 URLSessionConfiguration에서 sessionSendsLaunchEvents 프로퍼티를 true로 하는 것 입니다.
-> 이 코드는 앱이 일시 중지된 경우에도 네트워크 요청을 수행하고 요청이 완료되면 앱을 깨워서 URLSession 백그라운드 작업을 수행하라고 시스템에 알립니다. - 이것은 watchOS에서 중요합니다. watchOS 백그라운드에서 실행되는 앱의 네트워크 요청은 백그라운드 URLSession으로 구성되야 하기 때문입니다.
만약 백그라운드 작업의 실행 시간이 만료되면 백그라운드 작업 modifier에 제공된 클로저를 실행하는 비동기 작업을 시스템에서 취소합니다.
이것은 URLSession 백그라운드 작업에서 백그라운드 실행 시간이 만료될 때 여기서 이뤄진 네트워크 요청도 취소된다는 뜻입니다.
- 이때 실행 시간 만료로 인해 백그라운드 작업으로 네트워크 요청이 취소 될 때 Swift Concurrency에 내장된 함수인 withTaskCancellationHandler를 사용하게 된다면 결과를 직접 대기하기 보다는 withTaskCancellationHandler 호출에 다운로드를 넣고 그 결과도 함께 대기합니다.
- 위에서 session.data의 작업이 주어진 실행 시간안에 완료되지 못한다면 onCancel 작업을 수행하게 합니다
- 이때 onCancel에서 session.data에서 session.downloadTask로 작업을 승격시켜 작업을 재개합니다.
- 위처럼 구성하여 같은 기반의 네트워크 요청을 두 번 요청하지 않으며 URLSession은 진행 중인 모든 중복 요청을 제거합니다.
마지막으로 앱이 백그라운드 URLSession에서 실행을 처리하도록 설정되었는지를 확인합니다. 이때 아래처럼 backgroundTask modifier를 다시 활용해도 되지만 URLSession Task 타입으로도 가능합니다.
- 위처럼 식별자를 같게만 한다면 원하는 작업의 블록이 호출되도록 설정할 수 있습니다.
'SwiftUI' 카테고리의 다른 글
.sheet(), .transition(), .animation (0) | 2024.04.06 |
---|---|
더 적은 데이터로 더 많은 작업하기 (0) | 2024.04.06 |
Could not first-time schedule app refresh: Error Domain=BGTaskSchedulerErrorDomain Code=3 (0) | 2024.04.02 |
Custom RoundedRectangle (0) | 2024.04.02 |
SwiftUI - 클래스 간 데이터 교환, 의존성 주입 (0) | 2024.03.27 |