Google的自定义iOS键盘Gboard如何以编程方式关闭最前端的应用?

时间:2017-02-28 18:38:08

标签: ios ios-keyboard-extension

Google的自定义iOS应用Gboard有一项有趣的功能,无法在iOS SDK中使用公共API(从iOS 10开始)。 我想知道Google究竟如何完成以编程方式弹出Gboard中应用切换堆栈中的一个应用的任务。

自定义iOS键盘有两个主要组件:容器应用和键盘应用扩展。键盘应用程序扩展程序在单独的操作系统进程中运行,只要用户在手机上需要输入文本的任何应用程序中,该进程就会启动。

这些是使用Gboard可以遵循的近似步骤,以查看以编程方式返回到以前的应用程序的效果:

  1. 用户在iPhone上启动Apple Messages应用,点按文本字段即可开始输入文字。
  2. 推出Gboard键盘扩展程序,用户可以看到Gboard自定义键盘(当它们仍在Apple Messages应用程序中时)。
  3. 用户点击Gboard键盘扩展程序内的麦克风键进行语音到文本输入。
  4. Gboard使用custom url scheme启动Gboard容器应用。 Gboard键盘和Apple消息应用程序在App堆栈中向下推送一层,而Gboard容器应用程序现在是App堆栈中最前面的应用程序。 Gboard容器应用程序使用麦克风收听用户的语音,并将其翻译成放置在屏幕上的文本。
  5. 用户点击"完成"按钮,当他们对屏幕上显示的文字输入感到满意时。
  6. 这就是魔术发生的地方......当文本输入屏幕被解除时,Gboard容器应用程序也会自动被解雇。 Gboard容器应用程序消失并被Apple Messages应用程序取代(有时Gboard键盘扩展程序仍处于活动状态,有时会重新启动,有时需要通过点击文本字段手动重新启动。)。 Google如何实现这一目标?
  7. 最后,用户会在文本输入字段中看到刚刚翻译的文本自动插入。据推测,Google会在Gboard容器应用和键盘扩展程序之间通过sharing data完成此操作。
  8. 我认为Google正在使用私有API,方法是使用Objective-C运行时内省探索状态栏的视图层次结构,并以某种方式合成tap事件或调用公开的目标/操作。我对此进行了很少的探索,并且能够在状态栏中找到有趣的UIView子类,例如包含UIStatusBarBreadcrumbItemView数组的UISystemNavigationAction。我继续探索这些课程,希望能找到一些复制用户交互的方法。

    我了解使用私有API是让您的应用提交从App Store中被拒绝的好方法 - 这不是我想在答案中解决的问题。我主要是关于Google如何完成以GET方式在App切换堆栈中以编程方式弹出一个应用程序的具体任务的具体答案。

1 个答案:

答案 0 :(得分:35)

您的猜测是正确的 - Gboard正在使用私有API来执行此操作。

...虽然不是通过探索视图层次结构或事件注入。

当语音到文本操作完成后,我们可以从Xcode或Console中检查它调用-[AVAudioSession setActive:withOptions:error:]方法的syslog。所以我对Gboard应用程序进行了逆向工程,并寻找与此相关的堆栈跟踪。

爬上调用堆栈,我们可以找到-[GKBVoiceRecognitionViewController navigateBackToPreviousApp]方法,然后......

enter image description here

_systemNavigationAction?是的,绝对是私有API。

由于class_getInstanceVariable是公共API且"_systemNavigationAction"是字符串文字,因此自动检查程序无法记录私有API使用情况,而人工审核者可能看不到任何错误。 “跳回以前的应用程序”的行为。或者可能是因为他们是谷歌而你不是......

执行“跳回上一个应用程序”操作的实际代码如下:

@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以外的所有数字都会发生工作相同,所以谷歌开发人员这次很幸运)