이전 포스팅에서 NSScreencast iOS 앱을 위한 다운로드 시스템을 어떻게 설계하는지에대해 이야기했다.

여기서 사용자에게 강제로 포그라운드(foreground)에 붙잡아두고 다운로드 받게 할 필요가 없었으므로 자연스럽게 백그라운드 다운로드를 지원하게되었다.

겉으로 보았을때는 꽤나 쉬워보였다. 세션을 백그라운드 세션 구셩으로하고, id를 부여해서, 앱과는 분리된 진행으로 다운로드가 일어날 것이다.

백그라운드 세션을 사용할 때, 세션과 델리게이트가 주어진 다운로드에서 나중에 업데이트를 받기 위해 다시 생성될 필요가 있어보였는데, 블락 기반의 작업 API를 쓸 수 없었다. 여러 시나리오를 생각했지만, 먼저 이상적인 방식을 이야기해보자.
  • 사용자가 다운로드를 시작하고 앱을 멈춘다.
  • 몇 초뒤(내 경험으로는 10-30초이다) 앱은 꺼진다.
  • 다운로드는 다른 프로세스에서 계속 진행된다.
  • 다운로드가 끝나면, 앱은 다시 켜지고 당신의 앱 델리게이트가 다운받기 시작할때 사용한 id와 함께 application(handleEventsForBackgroundSessionWithIdentifier:)에서 받는다.
팁: Xcode에서 디버깅할때, 사실상 디버거는 백그라운드에 있는 동안 앱이 꺼지는 것을 막는다. 따라서 나는 Wait for Launch 옵션을 켜고, 수동으로 앱을 키고 다운로드를 시작한 뒤, 디버거를 켜기 전에 앱을 백그라운드에 놔두었다.

이 메소드가 호출될 때, 같은 id로 새로운 세션 구성을 만들어야한다. 그리고 델리게이트 인스턴스를 심는다. 이 시스템은 다운로드 상태를 즉시 여러분의 델리게이트에 알릴 수 있다.

그러나 어떤 다운로드일까?

실제로는 모른다. 당신이 얻은 모든 것은 원래의 요청 URL인데, 충분한 정보일 수도 있고 아닐수도 있다. URL은 가끔 바뀌기도 하므로 유일한 값이 아닐 수도 있으며, 가장 좋은 키 값이 아니다. http와 https 둘 다 가지고 있을때, 이런 경로들은 같은 리소스를 가리킬 수도 있고, 아마 할쪽이 리다이렉트 할것이다. 왜 이런게 불편한지에대한 여러 이유가 있다. 내 경우 일반적인 에피소드 URL들을 연관지어서 가지고 있는데, 이 URL은 아마존 클라으드프론트 URL로 리다이렉트하여, 유일하지도않고 임시적이기까지했다. 따라서 알림을 받은 에피소드 모델로 돌아갈 방법이 없게 되버린다.

이것이 API에서 좀 이상한 부분인데, 문서에 명확하게 명시되있지 않았다. 그러나 내가 찾은 해결책은 각 다운로드마다 유일한 세션 id를 부여하고 모델에 저장해 놓는다. 그러면 어떤 다운로드가 알림이 왔는지 쉽게 찾아낼 수 있게 된다.

좋다. 이상적인 상황에서는 확실하다. 그러나 이상적이지 않은 상황은 어떨까? 만약 다운로드가 실패한다면? 셀룰러 접근을 해제하고 다운로드가 백그라운드에서 일어나는데 Wi-Fi 존을 벗어나면 어떻게 될까?

마지막 경우는 어느정도 방법을 알고 있다. 만약 보통 세션 구성으로 Wi-Fi 에서 다운받기 시작한 뒤, Wi-Fi를 끈다면 여러분은 셀룰러 다운로드가 허용되지 않았다고 즉시 에러를 받을 것이다. 그러나 백그란운드 세션을 사용하고 있다면 시스템은 똑똑하게 Wi-Fi존에 다시 들어올때까지 기다리다가 그때 요청을 다시 시도한다.

다른 에러로 나타날 수도 있다. 실제로 내 로컬 서버가 동작하지 않을때 커넥션 에러가 나는데, 다운로드는 계속 진행되는것 같지만 계속해서 0%를 가리켰다. 로컬 서버를 켜니 마치 아무 문제 없던것처럼 다운로드를 시작했다.

요청을 재시작할때까지 얼마나 기다려야하는지에대한 이야기가 문서에 명확하게 나와있지 않았다. 사실 사용자가 앱을 다시 실행하면 다운로드는 어떤 상태이야야할까? 우리는 어떻게 알 수 있을까? 개발을 하면서 가끔 주인없는 다운로드를 발견했다. 다운로드 정보의 상태는 .downloading인데 완료나 성공이나 다른 콜백을 받지 못했다. 그때 내가 한 것은 실패로 다시 표시하는 것이다. 그런데 사실 '언제' 그것을 해야할까? 다운로드는 시간이 좀 걸릴 수 있고 몇분 뒤에 다시 시작할 수도 있으므로 단지 x분 뒤에 실패로 표시하는것처럼 간단하지는 않ㄴ았다.

그런식으로 처리하긴 했지만 아마 옳은 방법은 아닌것 같다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,



NSScreencast iOS 앱에서 비디오를 다운받아  오프라인에서도 사용할 수 있는 기능을 넣고 싶었다. 보통 비디오는 80에서 200MB 크기인데, 이것에는 실패시 다시 복구하는 다운로드 시스템을 만들기 위해 신경을 조금 쓸 필요가 있었다.



첫번재로 해야 할 일은 에피소드 화면에 진행상태가 보이는 다운로드 버튼을 넣는 것이었다. 여기서는 iTunes에서 음악을 다운 받는 것과 비슷하게 구현했다. 다운로드는 백그라운드에서 일어나며 진행 노티피케이션을 받아서 UI에 퍼센테이지로 나타낸다.




NSOperation 서브클래스를 통해 실제 다운로드가 끝난다. 이런 방식은 동시성을 제어하고, quality of service과 비행기 모드 다운로드에서 취소시키는 메커니즘을 쉽게 구현할 수 있게 해준다. 다운로드 진행은 에피소드 id를 가진 노티피케이션을 통해 보내지는데, 이 부분과 관련된 UI는 이것을 가져와서 갱신할 수 있다.


물론 다운로드 되는 동안 사용자가 화면을 응시하고 있을 필요는 없으므로, 앱을 둘러보든 기다리든 다운로드는 계속 진행될 것이다.

다은으로 어떤것이 다운중인지, 다운받은 비디오를 오프라인에서 보고, 저장공간을 확보하기 위해 다운로드 중인 것과 예전에 다운로드 받은 것들을 한 곳에서 볼 수 있게 하고 싶었다.

이 화면은 각 줄이 에피소드로서 테이블 뷰 안에 데이터를 보여준다. 현재 다운로드 받은 셀들은 진행 노티피케이션을 받아서 빨리 다시 불러와야 한다.

이 모든것들을 하기 위해 상태를 코어데이터 모델로 저장했다. 나의 DownloadInfo 모델은 아래와 같이 생겼다.


이것을 위해 코어데이터를 활용함으로서 그 라이프 사이클 내내 다운로드 상태를 추적할 수 있다. 이전에는 plist를 사용했었는데, 간단한 저장소로 plist보다 코어데이터가 더 간편했다.

모델에 다운로드 진행 상태 퍼센테이지를 저장하는 것을 볼 수 있지만, 이것을 반복적으로 코어데이터에 저장하지는 않았다. 빠른 커넥션에서 다운로드 진행 변화가 빠르게 일어날 것이고, 코어데이터를 매번 저장할 필요는 없었다. 요청이 취소되거나 다운로드가 더이상 일어나지 않음을 UI에 보여주고 싶을 때만 데이터를 저장했다.

코어데이터를 사용해서 얻은 또다른 장점에는 빠르게 DownloadsViewController를 구성하기위해 NSFetchedResultsController의 유용한 점을 사용할 수 있다는 것이다.

실패 처리하기
네트워크는 항상 뭔가 잘못될지도 모른다는 가능성을 가지고 있다. 이러한 가능성은 큰 파일 다운로드시 더 커진다. 사람들이 Wi-Fi 존을 벗어나거나, 터널로 들어간다던지, 비행기 모드를 켠다던지, 다른 다운로드들과 함께 한꺼번에 많이 다운받는다던지 할 수도 있다. 최고로 좋은 사용자 경험을 보장하기 위해서는 이것을 다루고 싶었고 사용자가 빨리 재시도를 할 수 있도록(때때로는 자동으로 재시도 하도록) 하고 싶었다.

실패가 일어나면 Download의 state 프로퍼티를 .failed로 바꾸고 UI를 적절하게 갱신한다. 그 셀을 다시 불러오고 사용자는 다운로드를 재시도 하기위해 탭할 수 있다.

일시정지와 재개
NSURLSession API가 시작되면, 요청을 취소하고 resume data라 불리는 애매한 오브젝트를 만들어내는 기능을 추가한다. 이것을 이용하면 끊긴 곳에서 요청을 시작할 수 있으며, 한가지 의문은 나중에 다시 시작할 수 있도록이 데이터를 유지하는 방법이다. NSScreencast 모델에 추가하는 것이 딱 알맞았다. 사용자가 진행중에 다운로드를 누르면 downloadTask.cancel(byProducing:)을 호출하고 나중에 사용하기위한 재개 데이터를 모델에 저장한다.

다운로드가 시작되고 모델 데이터가 재개 데이터라면 어느 시점에서 끊겼든지 그 곳에서 요청을 재개하는데 사용된다. 이 기능은 추가하기 쉬운데다, 큰 파일 다운로드시 굉장히 유용하다.


셀룰러 다루기
나는 다른 사람들의 데이터 요금을 불태우고 싶지 않았으므로 NSURLSessionConfigurationallowsCelluarAccessfalse로 설정해두었다. 그릭고 사용자가 원할때 셀룰러로 다운받을 수 있게 토글을 추가했다.




Fx Reachability를 사용하여 커넥션 상태를 모니터링했다. 사용자가 셀룰러일때 에피소드를 다운받으려 한다면 토글을 띄워 설정하고 다운로드 할 수 있게 하였다.

모델과 파일시스템의 동기화 유지하기(Keeping the Model and FileSystem in Sync)
파일에대한 메타데이터는 디스크에 저장하기 때문에 이것이 항상 동기화 되어있음을 보장해야한다. 에피소드를 하나 삭제하면 코어데이터 모델 뿐만 아니라 디스크에 파일도 삭제해주어야한다. 이 동기화를 보장해주기위해 나는 CleanupDownloadsOperation을 가지고 있는데, 이것은 앱이 켜질때 실행되며, 각 저장된 DownloadInfo가 디스크에 옳바른 파일을 가지고 있는지(혹은 삭제되었는지) 확인하고, 다운로드 폴더의 각 파일이 코어데이터에 저장되 있는지(혹은 삭제되었는지) 확인한다.

이렇게하여 뭔가 잘못되거나 두가지 상태(데이터페이스/디스크)가 싱크가 맞지 않을때의 대비책을 마련해 둔 것이다.

백그라운드 다운로드
겉으로 보기엔 간단해 보이지만 백그라운드 다운로드는 혼란과 복잡함의 근원으로 표현된다. 이 주제에 대해서는 나의 다음 포스팅에서 다루겠다.

그들이 말하는 그냥 오프라인 다운로드 추가하기
처음에 오프라인 다운로드를 앱에 넣어보기 전까지는 그냥 하루 이틀정도 더 걸리는 작업이라 생각했지만, 어마무시하게 복잡한 작업이 되버렸다.

역시 이런것이 소프트웨어라 생각된다.




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,