Как пользовательская клавиатура Google, Gboard, программно закрывает самое переднее приложение?
Пользовательское приложение Google для iOS, Gboard, имеет интересную функцию, которую невозможно реализовать с помощью общедоступных API для iOS SDK ( начиная с iOS 10). Я хотел бы точно знать, как Google выполняет задачу программного возврата одного приложения в стек переключения приложений в Gboard.
У пользовательских клавиатур iOS есть два основных компонента: приложение контейнера и расширение приложения клавиатуры. Расширение приложения клавиатуры запускается в отдельном процессе ОС, который запускается всякий раз, когда пользователь находится в любом приложении на своем телефоне, которое требует ввода текста.
Вот примерные шаги, которые можно выполнить, используя Gboard, чтобы увидеть эффект программного возврата к предыдущему приложению:
- Пользователь запускает приложение Apple Messages на своем iPhone и нажимает на текстовое поле, чтобы начать ввод текста.
- Расширение клавиатуры Gboard запущено, и пользователи видят пользовательскую клавиатуру Gboard (пока они все еще находятся в приложении Apple Messages).
- Пользователь нажимает клавишу микрофона внутри расширения клавиатуры Gboard, чтобы сделать голосовой ввод текста.
- Gboard использует собственную схему URL для запуска приложения контейнера Gboard. Клавиатура Gboard и приложение сообщений Apple помещаются на один уровень в стеке приложений, и приложение-контейнер Gboard теперь является самым передним приложением в стеке приложений. Контейнерное приложение Gboard использует микрофон для прослушивания речи пользователя и переводит ее в текст, который он помещает на экран.
- Пользователь нажимает кнопку "Готово", когда он удовлетворен вводом текста, который он видит на экране.
- Вот где происходит волшебство... когда экран ввода текста закрывается, приложение контейнера Gboard также автоматически закрывается. Контейнерное приложение Gboard исчезает и заменяется приложением Apple Messages (иногда процесс расширения клавиатуры Gboard все еще активен, иногда он перезапускается, а иногда его необходимо перезапустить вручную, нажав внутри текстового поля.) . Как Google достигает этого?
- Наконец, пользователь видит, что только что переведенный текст автоматически вставляется в поле ввода текста. Предположительно Google выполняет эту задачу, обмениваясь данными между приложением-контейнером Gboard и расширением клавиатуры.
Я предположил бы, что Google использует частные API, исследуя иерархию представления строки состояния, используя интроспекцию Objective-C во время выполнения и каким-то образом синтезируя события касания или вызывая выставленную цель / действие. Я очень мало это изучил и смог найти интересные подклассы UIView внутри строки состояния, такие как UIStatusBarBreadcrumbItemView, который содержит массив UISystemNavigationAction s. Я продолжаю исследовать эти классы в надежде, что смогу найти какой-нибудь способ репликации взаимодействия с пользователем.
Я понимаю, что использование частных API - хороший способ отклонить отправку вашего приложения из App Store - это не проблема, на которую я бы хотел ответить в ответе. В первую очередь я ищу конкретные ответы о том, как именно Google выполняет задачу программного возврата одного приложения в стек переключения приложений в Gboard.
2 ответа
Ваше предположение верно - Gboard использует закрытый API для этого.
… Хотя не через изучение иерархии представлений или внедрение событий.
Когда действие преобразования текста в текст выполнено, мы можем проверить системный журнал из Xcode или Console, который он вызывает -[AVAudioSession setActive:withOptions:error:]
метод. Поэтому я перепроектировал приложение Gboard и поищу трассировку стека, связанную с этим.
Поднявшись по стеку вызовов, мы можем найти -[GKBVoiceRecognitionViewController navigateBackToPreviousApp]
метод и…
... _systemNavigationAction
? Да, безусловно, закрытый API.
поскольку class_getInstanceVariable
это публичный API и "_systemNavigationAction"
является строковым литералом, автоматическая проверка не в состоянии заметить использование частного API, и рецензенты, вероятно, не видят ничего плохого в поведении "перейти к предыдущему приложению". Или, возможно, потому что они Google, а вы нет...
Фактический код, который выполняет действие "Перейти к предыдущему приложению", выглядит следующим образом:
@import UIKit;
@import ObjectiveC.runtime;
@interface UISystemNavigationAction : NSObject
@property(nonatomic, readonly, nonnull) NSArray<NSNumber*>* destinations;
-(BOOL)sendResponseForDestination:(NSUInteger)destination;
@end
inline BOOL jumpBackToPreviousApp() {
Ivar sysNavIvar = class_getInstanceVariable(UIApplication.class, "_systemNavigationAction");
UIApplication* app = UIApplication.sharedApplication;
UISystemNavigationAction* action = object_getIvar(app, sysNavIvar);
if (!action) {
return NO;
}
NSUInteger destination = action.destinations.firstObject.unsignedIntegerValue;
return [action sendResponseForDestination:destination];
}
В частности, -sendResponseForDestination:
Метод выполняет фактическое действие "назад".
(Поскольку API недокументирован, Gboard на самом деле использует API неправильно. Они использовали неверную подпись -(void)sendResponseForDestination:(id)destination
, Но бывает так, что все числа, кроме 1
будет работать так же, поэтому разработчикам гугл на этот раз повезло)
Быстрая версия ответа @kennytm:
@objc private protocol PrivateSelectors: NSObjectProtocol {
var destinations: [NSNumber] { get }
func sendResponseForDestination(_ destination: NSNumber)
}
func jumpBackToPreviousApp() -> Bool {
guard
let sysNavIvar = class_getInstanceVariable(UIApplication.self, "_systemNavigationAction"),
let action = object_getIvar(UIApplication.shared, sysNavIvar) as? NSObject,
let destinations = action.perform(#selector(getter: PrivateSelectors.destinations)).takeUnretainedValue() as? [NSNumber],
let firstDestination = destinations.first
else {
return false
}
action.perform(#selector(PrivateSelectors.sendResponseForDestination), with: firstDestination)
return true
}