干凈優(yōu)雅的做iOS應用內全局交互屏蔽
01、交互屏蔽的需求
很多應用開發(fā)者都會遇到這樣一個需求,當程序需要處理某個敏感的核心任務,或者執(zhí)行某些動畫時,需要杜絕一切外部干擾,優(yōu)先保證任務的完成,之后再去處理其它任務。否則如果在處理過程中受到外部事件的干擾,可能會引入嚴重的問題,而規(guī)避這些問題需要額外編寫過多的邏輯。
例如,當程序在忙著清理應用內緩存的過程中去處理其它任務,這時候由于其它任務可能會產(chǎn)生新的緩存,這就會和現(xiàn)有的任務沖突。所以在清理緩存的過程中,app 一般會暫時中斷用戶和非用戶的請求,優(yōu)先保證緩存清理的完成。
所以,為了簡化產(chǎn)品設計邏輯,開發(fā)者一般會選擇在處理任務時暫時屏蔽其它任務,優(yōu)先保障現(xiàn)有任務的完成。
舉例來說,當用戶點擊清理緩存時,應用程序可能會彈出一個帶有進度條的清理界面,在該界面下,清理工作緊張的進行著,并且告知用戶正在清理任務,請稍候。
另一個需求是和動畫有關,有時候我們在應用內可能會執(zhí)行一些小動畫,例如按鈕的淡入淡出,整個頁面的切換等。這些動畫可能不會因為用戶做快速的操作導致程序崩潰,但是因為每個動畫都要時間完成,如果用戶快速亂點的話,有可能會出現(xiàn)意想不到的情況。
例如,假設用戶點擊某個開關切換按鈕,開關狀態(tài)為開時,屏幕側邊以動畫形式彈出側邊欄,當開關狀態(tài)為關時,屏幕側邊欄以動畫形式消失。那么如果用戶快速反復點擊按鈕,而開發(fā)者沒有處理好開關切換間隙的邏輯的話,那么就會出現(xiàn)側邊欄彈出動畫還沒執(zhí)行完,就立刻消失的情況。
值得注意的是,這一類事件包括但不限于用戶觸摸事件,還有屏幕重力感應的變化等非用戶輸入事件,這就意味著這一類問題如果要優(yōu)雅解決的話,不能單靠添加一個"觸摸屏蔽層"。
02、常見的解決辦法
對于以上問題,開發(fā)者選擇的解決辦法主要是兩種:
第一個辦法,設計一個布爾變量記錄當前是否正在執(zhí)行任務(或處理動畫),處理這個過程中的交互邏輯。
這個辦法本身沒什么問題,但是開發(fā)者不得不針對每個任務去編寫對應的邏輯,這樣寫起來就特別容易散亂。
第二個辦法,設置 UIView 或者控件的 userInteractionEnabled 為 false,并在合適的時機重新變?yōu)?nbsp;true 。
這樣做有個好處是,將整個 UIView 設置不可交互后,用戶點擊其它按鈕也不會造成影響,但同時,如果對每個 UIView 去處理類似的邏輯,一不小心很容易出現(xiàn) bug,最后導致整個 UIView 都卡住無法點擊。
另外,應用可能存在多個 UIView,你鎖住了一個 UIView,其它 UIView 的點擊情況是否要考慮呢?
有些開發(fā)者用了更好的辦法,他們直接用 UIViewController 的 UIView 來做 userInteractionEnabled 的處理,這樣的解決方案更進步了,但是同樣存在多個 UIViewController 這個問題,雖然有效,但還欠缺優(yōu)雅。
另一方面,如上述的所有方法,僅僅能攔截"觸摸事件",而不能攔截非觸摸事件,例如加速器,攝像頭事件等,如果代碼針對這些事件會做出響應,而開發(fā)者不希望在任務期間去響應他們,將被迫去添加邏輯來屏蔽才行。
03、重新理解 UIApplication
我們對 UIApplication 不陌生了,我們經(jīng)常需要通過調用UIApplication.sharedApplication。
在 iOS 的應用層 API 中,UIKit 最頂層的交互機制是通過 UIApplication 的 方法下發(fā)的 sendEvent :
- (void)sendEvent: (UIEvent*)event
UIEvent 不止包括觸摸事件,它還支持例如加速度事件等別的事件類型。
// UIEvent 的事件類型
typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches,
UIEventTypeMotion,
UIEventTypeRemoteControl,
UIEventTypePresses,
UIEventTypeScroll,
UIEventTypeHover,
UIEventTypeTransform,
};
以上是 UIApplication 中的事件類型,其中最值得關注的是 UIEventTypeTouches 和 UIEventTypeMotion,因為這是開發(fā)者最常用于響應輸入的事件。
04、如何攔截 sendEvent,先搞懂 UIApplicationMain
要攔截 sendEvent,就要了解 UIApplicationMain,幾乎每個 iOS 開發(fā)者都會碰到它,因為它就在 main 函數(shù)里:
int main(int argc, char * argv[]) {
NSString* appDelegateClassName;
@autoreleasepool {
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
UIApplicationMain 相當于 iOS 應用自己的 main 函數(shù),它的參數(shù)有 4 個,分別為 argc, argv,principalClassName 和 delegateClassName 。
其中前兩個參數(shù)就是 C 語言 main 函數(shù)的參數(shù),appDelegateClassName 傳入的是 AppDelegate 的類名,第三個參數(shù)則傳入用戶自定義的 UIApplication 子類。
我們定義一個繼承自 UIApplication 的 MyApplication 類后,main 函數(shù)就可以傳入 MyApplication 類了。
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);
}
還有一個方法可以不通過修改 main 函數(shù)來指定 MyApplication,在 XCode 的 Info.plist 中,新建字符串鍵 “Principal class”,其值填入子類名,即本例的 MyApplication,那么當 main 函數(shù)傳入的參數(shù)是 nil 時,Info.plist 所注冊的 “Principal class” 將會作為指定類。如果全部沒有指定,則默認為 UIApplication 。
05、sendEvent 攔截實現(xiàn)
在 MyApplication 類的實現(xiàn)部分,我們可以開始繼承 sendEvent :
- (void)sendEvent: (UIEvent*)event {
[super sendEvent: event];
}
當應用產(chǎn)生了 UIEvent 事件時,系統(tǒng)會調用 sendEvent,此時因為我們注冊了 MyApplication,所以調用的是我們定義的 sendEvent 方法。在上個例子里,sendEvent 直接調用了父類的 sendEvent,相當于對所有事件都采取了默認處理。
接下來,如果我們打算屏蔽所有的加速器事件,那么可以這么寫:
- (void)sendEvent: (UIEvent*)event {
if (event.type == UIEventTypeMotion) {
NSLog(@"UIEventTypeMotion");
return;
}
[super sendEvent: event];
}
這樣,如果應用內有處理搖一搖的功能,以上方法可以保證搖一搖事件不會下發(fā)。
06、交互屏蔽的接口設計
顯然,我們需要更彈性的處理 sendEvent,直接屏蔽的辦法是“一刀切”,更恰當?shù)淖龇ㄊ切枰帘蔚臅r候才讓它屏蔽。因此,我們可以為 MyApplication 設計一個布爾屬性 eventdisabled:
@interface MyApplication()
@property (nonatomic) BOOL eventDisabled;
@end
該屬性默認值是 false,即允許所有 event ,這樣,sendEvent 方法的實現(xiàn)更改如下:
- (void)sendEvent: (UIEvent*)event {
if (self.eventDisabled && (event.type == UIEventTypeTouches || event.type == UIEventTypeMotion)) {
return;
}
[super sendEvent: event];
}
當 eventdisabled 為真時,app 在全局范圍內禁用了用戶的觸摸和加速器輸入事件,只有當 eventdisabled 為假時,一切照常進行。
雖然現(xiàn)在可以通過變量讓 app 實現(xiàn)交互的禁用和啟用了,但是我們可以設計的更彈性一點,做一個延時機制,保證延時過后交互可以恢復正常,不然開發(fā)者四處寫布爾值設置的代碼,一旦稍有不慎,整個界面卡住就糟了,為了程序的健壯性,可以設計如下方法:
- (void)disableUserInteraction: (NSTimeInterval)duration {
self.userInteractionEnabled = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, duration*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
self.userInteractionEnabled = YES;
});
}
然后,我們把該方法放到 UIApplication 的擴展中,在 MyApplication.h 加入:
@interface UIApplication()
- (void)disableUserInteraction: (NSTimeInterval)duration;
@end
這樣,當我們需要臨時禁用用戶輸入時,可以這么調用:
[UIApplication.sharedApplication disableUserInteraction: 0.3];
此時應用會在 0.3 秒內處于禁用狀態(tài),并在稍后自動恢復。
07、如何進一步優(yōu)化
以上介紹的屏蔽方案已經(jīng)可以解決大部分日常需求,但是還有優(yōu)化空間。
例如,當有多次調用 disableUserInteraction 時,例如:
[UIApplication.sharedApplication disableUserInteraction: 0.3];
[UIApplication.sharedApplication disableUserInteraction: 1.3];
我們會發(fā)現(xiàn),0.3 秒后應用就恢復了交互,而 1.3 秒的屏蔽"失效"了。
本文轉載自微信公眾號「搜狐技術產(chǎn)品」,可以通過以下二維碼關注。轉載本文請聯(lián)系搜狐技術產(chǎn)品公眾號。