通过UIApplicationMain实现应用内多种事件拦截

简介

UIApplicationMain 大家并不陌生,因为在通过 XCode 建立 iOS 的 Ojective-C 工程时肯定会看到。新建的 main.m 文件长这样:

int main(int argc, char * argv[]) {
    NSString* appDelegateClassName;
    @autoreleasepool {
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

大部分人都不会在意这个函数,其实它非常有用。

UIApplicationMain 的参数分 4 个部分,argc,argv,principalClassName 和 delegateClassName 。

argc 和 argv 不需要介绍,是 C 语言 main 函数的基础参数。后两个参数都是 NSString 类型的参数。也是着重要介绍的参数。

delegateClassName 参数比较好理解,即设置 App 的公用回调函数,例如如下函数就是常用的应用完成启动时的回调:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;

新建 XCode 工程时已经替开发者新建了 AppDelegate 类,但是因为后来苹果将应用回调的大部分功能转移到了 SceneDelegate 类中,所以 AppDelegate 的作用越来越小了。

注:之所以 AppDelegate 被替代,原因是 iOS 13 之后,苹果引入了多场景的概念,不同的场景对应不同的回调,而传统的 AppDelegate 不适应这种回调模式。

principalClassName 是最重要的参数,它需要开发者提供 UIApplication 或者它的子类,如果传入 nil,则默认使用 UIApplication 。

传入自定义 principalClassName

如果不用 UIApplication 作为默认的 principalClassName,而是传入它的子类,一般是为了解决产品在应用层面的管理问题。例如设计了一个 UIApplication 的子类叫 MyApplication,那么,原先的 main.m 代码就需要按如下方式重写:

int main(int argc, char * argv[]) {
    NSString* appDelegateClassName;
    NSString* applicationClassName;
    @autoreleasepool {
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        applicationClassName = NSStringFromClass([MyApplication class]);
    }
    return UIApplicationMain(argc, argv, applicationClassName, appDelegateClassName);
}

除此之外还有另一个办法来告诉项目工程自己需要用别的类替代,其方法是在 Info.plist 文件中引入 Principal class 作为 Key 的字符串键值,其值填入类名,例如本例的 MyApplication,那么,当 UIApplicationMain 所传入的 principalClassName 参数为 nil 时,会自动在 Info.plist 寻找 Principal class 的值来作为 applicationClassName。

拦截应用内的 UIEvent 事件

实现 UIApplication 子类可以从应用层面全局监控所有的 UIEvent 事件。UIEvent 事件包括用户的输入,传感器等信息,最常处理的是用户输入。

例如通过以下方式继承实现 sendEvent 后,可以在产品内观察记录所有用户的手指在屏幕上移动的事件。并记录下触摸的位置。

- (void)sendEvent: (UIEvent*)event {
    if (event.type == UIEventTypeTouches) {
        NSSet* touches = [event allTouches];
        UITouch* touch = touches.objectEnumerator.nextObject;
        if (touch) {
            if (touch.phase == UITouchPhaseMoved) {
                UIView* view = touch.view;
                CGPoint point = [touch locationInView: view];
                NSLog(@"detect move event (%lf, %lf)", point.x, point.y);
            }
        }
    }
    [super sendEvent: event];
}

因为 sendEvent 是管理着 UIEvent 的最顶层下发,这就意味着在继承实现 sendEvent 时不调用 [super sendEvent: event] 的话,UIEvent 就不会往下传递,这样就能起到拦截用户输入使它不生效的效果。

例如,以下代码可以起到“屏蔽所有用户触摸事件”的效果。

- (void)sendEvent: (UIEvent*)event {
    if (event.type == UIEventTypeTouches) {
        NSSet* touches = [event allTouches];
        UITouch* touch = touches.objectEnumerator.nextObject;
        if (touch) {
            // 屏蔽所有用户触摸事件
            return;
        }
    }
    [super sendEvent: event];
}

监控应用内的 Action

另一个常用的方法是 sendAction:

- (BOOL)sendAction: (SEL)action to:(id)target from:(id)sender forEvent:(UIEvent *)event {
    NSLog(@"send action");
    return [super sendAction: action to: target from: sender forEvent: event];
}

Action 是 Event 管理下的封装,也就是说是先有事件,才可能出现Action 。

给 UIButton 通过 addTarget 添加的动作就是 Action,通过继承实现 sendAction 可以检测到整个应用内的所有 Action 事件。

这里要注意,不能通过给 sendAction 返回 NO 来屏蔽应用内的点击响应,除非在继承实现时不调用 super 的 sendAction 。

处理应用跳转请求

[UIApplication.sharedApplication openURL: url 
                                 options: @{} 
                       completionHandler: nil];

以上这段代码大家并不陌生了,应用跳转,打开内置浏览器等都会调用这个函数。

通过继承实现 openURL 可以拦截所有 openURL 调用,这样做最大的好处是可以进行 URL 选择过滤,并且可以实现根据需要弹窗提醒用户即将进行跳转。

拦截的策略和之前 sendEvent,sendAction 一样,不需要过多赘述。

移动开发者联盟加入指引

你可能感兴趣的:(xcode,ios,objective-c)