Фоновое обновление данных в iOS7

В конце сентября компания APPLE выпустила iOS 7, одной из особенностей этой версии стала улучшенная многозадачность и возможность обновления данных, когда приложение не активно.
Есть два варианта запуска приложения для обновления данных — периодические обновления и запуск при получении специального push-уведомления. В каждом из вариантов приложение будет запущено в фоновом режиме (background mode), и будет принудительно закрыто через 30 секунд, так что времени для обновления будет совсем мало.

Периодическое обновление данных (Background fetch)


Чтобы разрешить приложению запускаться в фоновом режиме, нужно добавить в plist приложения ключ Required background modes со значением App downloads content from the network. Это так же можно сделать поставив галочку во вкладке Capabilities



Кроме того, нужно в методе application: didFinishLaunchingWithOptions: установить минимальный интервал для запуска.
If([[[UIDevice currentDevice] systemVersion] floatValue] >=7.0){
    [[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:600];
}

(проверка на версию важна – в iOS 6 приложение падает на этой функции).
Вместо числового значения можно воспользоваться переменной UIApplicationBackgroundFetchIntervalMinimum, чтобы установить минимально возможный интервал, но особой роли это не играет, потому что устанавливается минимальный, а не гарантированный интервал. Когда конкретно приложение запустится – неизвестно. Оно будет запущено исходя из статистики работы: в котором часу приложение обычно запускается, сколько времени обычно находится в фоновом режиме. Чем меньше времени потребуется на работу программы, тем чаще она будет запускаться. Установка интервала обязательна, так как по умолчанию, это значение равно UIApplicationBackgroundFetchIntervalNever и запуска не произойдет.

При запуске приложение начинает свою работу в фоновом режиме, запуская метод application: performFetchWithCompletionHandler:

-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler{
//DO SOMETHING
completionHandler(UIBackgroundFetchResultNewData); 
}

В конце метода необходимо вызвать полученный completionHandler с параметром UIBackgroundFetchResultNewData (варианты – UIBackgroundFetchResultNoData, UIBackgroundFetchResultFailed). Но в любом случае, через 30 секунд приложение будет принудительно закрыто.
Кроме этого, как и при стандартном запуске, вызывается application: didFinishLaunchingWithOptions:
и если в нем есть код, который не должен запускаться в данной ситуации (например, обновление UI), в целях экономии процессорного времени его лучше отключить. При запуске в этом режиме я не нашел дополнительных ключей в launchOptions, так что нужно проверять параметр application.applicationState, который будет равен UIApplicationStateBackground.
N.B. Если в течение 30 секунд, пока приложение работает в фоновом режиме, пользователь запустит приложение через UI, то повторного запуска этого метода не произойдет. Если необходимо, пропущенный код можно запустить в методе applicationWillEnterForeground:(UIApplication *)application

Для отладки запуска приложения в режиме фонового обновления в XCode есть возможность симуляции этого режима – включается галочкой в настройках схемы.



Запуск от push сообщения.


Второй возможностью получить обновление контента является запуск при получении push notification с выставленным флагом content-available.

Я не буду расписывать настройку приложения для получения push. Скажу только, что в plist приложения добавляется ключ Required background modes со значением App downloads content in response to push notifications (ну или соответсвующая галочка в настройках проекта), а к типам сообщений, которые приложение будет получать, добавляется новое значение
UIRemoteNotificationTypeNewsstandContentAvailability;

  UIRemoteNotificationType myTypes = UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeAlert|UIRemoteNotificationTypeSound|UIRemoteNotificationTypeNewsstandContentAvailability;
    [application registerForRemoteNotificationTypes:myTypes];



При получении сообщения запускается метод

-(void) application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler{
    //DO SOMETHING
    completionHandler(UIBackgroundFetchResultNewData); 
}

которому тоже
будет дано всего лишь 30 секунд и в конце которого надо вызвать completionHandler с параметром.

Эта функция будет запущена вне зависимости от того, в каком состоянии сейчас приложение, даже если оно в активном режиме.
Если приложение запускается в фоне, то так же будет вызван метод application: didFinishLaunchingWithOptions:, но в данном случае в launchOptions будет запись с ключом UIApplicationLaunchOptionsRemoteNotificationKey.

Первый подводный камень – push не приходит, если кроме флага content-available ничего нет. Если добавить текст или звук, то все нормально. Поскольку никто не хочет спамить пользователя сообщениями, то посылаем пустой звук:
aps =     {
        "content-available" = 1;
        sound = "";
    };



Что можно успеть за 30 секунд?


В принципе, Apple не ограничивает нас в выборе метода, которым будет проводиться обновление. Однако, в iOS 7 появился новый класс NSURLSession, которым компания рекомендует воспользоваться. Не углубляясь в его описание можно сказать, что одним из его достоинств является возможность скачивания данных в фоновом режиме с помощью самой iOS, даже после прекращения работы приложения.

Ложка дегтя


А вот теперь немного личного опыта или почему для нашего приложения (электронная газета на iPad) все вышеперечисленное не сработало. Планировалось, что утром пользователь проснется, достанет iPad, а газета уже у него готова и ждет прочтения. Но…
Во-первых, фоновое обновление не работает когда устройство в спящем режиме (экран погашен). Обновление пройдет сразу же как только экран разблокируется, это хорошо для мессенжера, но для нас это слишком поздно.
Во-вторых, не смотря на то что в документации написано «If your app is suspended or not running, the system wakes up or launches your app and puts it into the background running state before calling the method.», push не запускает приложение, если оно в состоянии “not running”. Это значит, что если пользователь сам удалил приложение из списка задач или система выгрузила его, то push приходит, но приложение не запускается. Не ясно, баг это или нет, но что есть, то есть. Увы.

P.S. Чтобы приложение запускалось, необходимо так же разрешить возможность запуска в настройках устройства (или вернее, не запрещать, ибо по умолчанию это включено для новых программ). Проверить это из приложения можно через контроль значения [[UIApplication sharedApplication] backgroundRefreshStatus].
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 12

    +2
    почему для нашего приложения (электронная газета на iPad) все вышеперечисленное не сработало
    А почему не сработал newsstand?
      +1
      По нескольким причинам.
      Во-первых, в newsstand можно посылать всего один push день — а у нас несколько выпусков. Во-вторых, newsstand довольно серьезно завязан на автоматически обновляемую подписку, а она не во всех странах доступна. Ну и кроме этого, newsstand требует хранить данные у себя и может удалить не активный в данный момент контент, если он решит что у него осталось мало места…
      0
      С такой ложкой дегтя от бочки меда никакого проку. Ну не то чтобы никакого, но очень существенно снижает полезность API. Я понимаю, что они борятся за жизнь батареи, но подчас методы совсем уж драконовские.
        0
        В iOS 7 та же самая ситуация с Significant Location Changes — если устройство было перезагружено или приложение было удалено из списка задач пользователем, то обновления не приходят и приложение не просыпается. В iOS 6 такой проблемы не было, никак не пойму баг ли это сейчас или фича и как обойти.
          0
            0
            Да, фича явно глючная, т.к. нотификации не присылаются не только погда приложение было закрыто пользователем, а ещё и когда села батарейка, к примеру, и телефон был перезагружен…
              –1
              После перезагрузки весь список задач сохраняется и если не запускается, то скорее всего это баг.
          0
          При тестировании данной фичи я написал апп, который показывал локальную нотификацию всякий раз, когда система запускала backgroundFetch, а параметр minimumBackgroundFetchInterval = UIApplicationBackgroundFetchIntervalMinimum. За пару дней обычного использования айфона при наличии Wi-Fi апп запускается в фоне несколько раз в день. Иногда подряд около 5-10 раз с интервалом 10-20 мин.

          Есть ещё один момент, если возвращать через completionHandler значение UIBackgroundFetchResultNewData, то система будет считать, что вы сохранили новые данные и скорее всего ваш UI тоже будет выглядеть по новому и соответственно скриншот аппа в списке задач устарел. Поэтому, по не понятным правилам, но апп будет отдельно запущен в фоновым режиме для перегенерации скриншота. Поэтому апп Погоды «всегда» показывает актуальную температуру на скриншоте в списке задач.
          0
          Есть ли возможность делать снимки фотокамерой внутри performFetchWithCompletionHandler?
            0
            Забавная идея, надо попробовать.
              0
              попробовал. С первого раза не получилось (фото не делается). Но в принципе можно попробовать покопать в сторону private API, поскольку такая шпионская программа в AppStore скорее всего не попадет…

          Only users with full accounts can post comments. Log in, please.