你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> 基於AOP的iOS用戶操作引導框架設計

基於AOP的iOS用戶操作引導框架設計

編輯:IOS開發綜合

背景

有一種現象,App設計者覺得理所當然的操作方式,卻常常被用戶所忽視,為了防止這種現象發生,就要為App設計一個幫助,一種低成本的方案是將幫助文檔寫成HTML然後展示給用戶,這樣的方式常常不能帶來好的效果,一種較好的方式是高亮用戶應該點擊的區域,對其他部分進行遮蓋,並用說明文字提醒用戶,如下圖所示。
\

下載

框架SGUserGuide已經上傳到github,點擊前去github下載,歡迎Star!

關鍵

要實現這種引導,關鍵問題有二,一是如何拿到允許交互的控件,二是如何處理引導步驟的推進關系。
對於第一個問題,可以通過keyPath解決,keyPath的強大之處在於可以用點語法拿到更深層的私有,例如我們的ViewController有一個私有屬性topView,而topView又有私有屬性topButton,那麼我們使用topView.topButton即可從ViewController中拿到控件topButton而絲毫不破壞其封裝性。
對於第二個問題,可以通過AOP編程解決。我們知道大部分的交互都涉及頁面切換,例如上圖點擊按鈕後進入編輯頁面,因此頁面的切換可以作為一個“切面”,我們通過這個切面來處理大部分的引導步驟推進。我們可以通過Method Swizzling來攔截所有的viewWillAppear:方法,並處理引導步驟的判斷與推進,需要注意的是還有一些不涉及頁面切換的引導步驟,則需要在適當的地方手動推進。

實現

描述用戶引導步驟的類的設計

為了描述一個引導步驟,首先要判斷當前頁面是否應該被引導,通過ViewController的類型來判斷;其次需要的是可交互控件,通過keyPath來尋找;除此之外,還需要對用戶的提示信息,這個類的具體設計如下:

@interface SGGuideNode : NSObject

@property (nonatomic, assign) Class controllerClass;
@property (nonatomic, strong) NSString *permitViewPath;
@property (nonatomic, copy) NSString *message;
@property (nonatomic, assign) BOOL reverse;

+ (instancetype)nodeWithController:(Class)controller permitViewPath:(NSString *)permitViewPath message:(NSString *)message reverse:(BOOL)reverse;
+ (instancetype)endNodeWithController:(Class)controller;

@end

其中reverse是一個用於反轉遮蓋與可交互控件的屬性,用於類似於“進行一項除去退出以外的操作”的情景。
通過兩個類方法可快速的創建一個步驟結點,endNode作為結束結點,用於判斷用戶引導是否結束。

遮蓋層視圖設計

攔截交互事件

遮蓋層視圖需要蓋住界面,並且在可交互區域“挖洞”,要實現這種功能,可以通過pointInside:withEvent:方法處理點擊事件,對於落在洞外的點交給遮蓋層處理,也就是返回YES,這樣就保證了原來的交互事件被攔截。
其中permitRect為允許交互的視圖的

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    BOOL ret = !CGRectContainsPoint(self.permitRect, point);
    if (self.node.reverse) {
        ret = !ret;
    }
    return ret;
}

繪制遮蓋區域與允許點擊區域

處理完了點擊事件,我們只需要通過drawRect:在遮蓋區繪制透明的灰色,在允許交互區域繪制透明色即可做出預想的效果。
首先我們要定義出maskColor和holeColor,然後先對整個遮蓋層視圖填充maskColor,再對允許交互區填充holeColor。

- (void)drawRect:(CGRect)rect {
    // 省略maskColor、holeColor的定義與賦值代碼
    [maskColor setFill];
    UIRectFill(rect);
    // 省略允許點擊區域permitRect的計算代碼
    [holeColor setFill];
    UIRectFill(self.permitRect);
}

計算說明文字的區域

接下來一個問題是提示文字的位置,提示文字應該緊貼可交互區域,並且應該盡可能擁有更多的空間,因此我們需要計算可交互區域四周的面積,並選擇一塊最大的區域。

添加遮蓋層

最最關鍵的問題是遮蓋層應該添加到誰的view身上,由於在觸發一個引導步驟時已經拿到了當前顯示的視圖控制器(引導步驟的觸發通過攔截viewWillAppear:實現,因此可以拿到視圖控制器對象),因此添加變得十分簡單。
不要簡單的認為將遮蓋層添加到視圖控制器的view即可,因為視圖控制器可能有NavigationController或者TabbarController包裹,如果只是添加到視圖控制器的view無法蓋住頂部和底部區域
基於這個考慮,我們按照tabBarController.view>navigationController.view>viewController.view的優先級來添加遮蓋層。

- (void)showInViewController:(UIViewController *)viewController {
    // 每次顯示前,保證顯示中的遮蓋層已經被移除,通過removeFromSuperview移除。
    [self hide];
    self.permitView = [viewController valueForKeyPath:self.node.permitViewPath];
    self.messageLabel.text = self.node.message;
    if (viewController.tabBarController) {
        [viewController.tabBarController.view addSubview:self];
    }else if (viewController.navigationController) {
        [viewController.navigationController.view addSubview:self];
    } else {
        [viewController.view addSubview:self];
    }
    self.frame = self.superview.frame;
    [self setNeedsDisplay];
}

這裡包含了對步驟結點的解析,注意遮蓋的尺寸與要蓋住的視圖大小一致,最後一句會觸發drawRect:根據最新的結點解析數據繪制遮蓋層與允許交互層。

移除遮蓋層

移除遮蓋層,只需要調用removeFromSuperview即可。

- (void)hide {
    [self removeFromSuperview];
}

調度器的設計

調度器類的設計

要實現步驟的切換,需要一個全局調度器,它接收切面通知或者用戶的手動通知來對步驟進行判斷與切換。所有的步驟結點都被以數組的形式保存到調度器中,調度器通過游標cur來判斷當前進行到的步驟。
為了使用方便,編程者只需要將結點數組傳遞給調度器,調度器便會自動開始處理步驟的判斷與切換,例如下面的代碼:

- (void)setupGuide {
    SGGuideDispatcher *dp = [SGGuideDispatcher sharedDispatcher];
    dp.nodes = @[
                 [SGGuideNode nodeWithController:[FirstViewController class] permitViewPath:@"addBtn" message:@"Please Click The Add Button And Choose Yes From the Alert." reverse:NO],
                 [SGGuideNode nodeWithController:[FirstViewController class] permitViewPath:@"wrap.innerView" message:@"Please Click the Info Button" reverse:NO],
                 [SGGuideNode nodeWithController:[SecondViewController class] permitViewPath:@"tabBarController.tabBar" message:@"Please Change To Third Page" reverse:NO],
                 [SGGuideNode endNodeWithController:[ThirdViewController class]]
                 ];
}

為了實現這樣的效果,需要將調度器設計成單例,並且通過nodes數組這一屬性接收步驟結點,上面提到,不涉及到頁面切換的步驟完成無法被捕獲,因此需要用戶手動推進,因此調度器還需要一個next方法來進行手動推進,綜上所述,調度器的設計如下:

@interface SGGuideDispatcher : NSObject

@property (nonatomic, strong) NSArray *nodes;

+ (instancetype)sharedDispatcher;
- (void)next;
// 重置引導步驟,用於調試
- (void)reset;

@end

攔截器設計

上文提到,我們通過攔截viewWillAppear:方法來觸發步驟的判斷與切換,可以通過為UIViewController添加分類實現,在攔截後發出通知,以供調度器接收,如下:

@implementation UIViewController (Tracking)

+ (void)load {
    method_exchangeImplementations(class_getInstanceMethod([self class], @selector(viewWillAppear:)), class_getInstanceMethod([self class], @selector(track_viewWillAppear:)));
}

- (void)track_viewWillAppear:(BOOL)animated {
    [self track_viewWillAppear:animated];
    [[NSNotificationCenter defaultCenter] postNotificationName:SGGuideTrigNotification object:@{@"viewController":self}];
}

@end

調度器開始調度的時機

上文提到調度器開始工作的時機是接收到步驟結點後,因此通過重寫結點數組的setter來注冊對攔截器通知的監聽即可。

- (void)setNodes:(NSArray *)nodes {
    _nodes = nodes;
    // 重置游標
    self.cur = 0;
    // 防止重復注冊
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(trig:) name:SGGuideTrigNotification object:nil];
}

這樣的設計十分明了,但是不利於對引導結束後再次啟動App不開啟調度的編程,故改良如下,通過Preference記錄引導步驟游標cur的值,對於結束的引導cur為-1,如果cur是-1,則不接收步驟結點,防止浪費內存。

- (void)setNodes:(NSArray *)nodes {
    if ([[NSUserDefaults standardUserDefaults] integerForKey:kSGGuideDispatcherCur] == -1) {
        return;
    }
    _nodes = nodes;
    if (self.cur < nodes.count) {
        [[NSNotificationCenter defaultCenter] removeObserver:self];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(trig:) name:SGGuideTrigNotification object:nil];
    }
}

調度器觸發的時機

通過上文我們知道,攔截器的通知觸發了調度器的trig:方法,trig:方法用於處理調度器的觸發邏輯,除此之外,還有手動觸發調度器的方式,也通過發送通知實現。

- (void)next {
    if (!self.currentViewController) return;
    [[NSNotificationCenter defaultCenter] postNotificationName:SGGuideTrigNotification object:@{@"viewController":self.currentViewController}];
}

這裡的currentViewController為當前展示的視圖控制器,這個值在每次調度器觸發時根據通知中的視圖控制器來賦值,由於next前還沒有進行頁面切換,因此當前的視圖控制器不變,依然是currentViewController。

調度器的觸發邏輯

調度器每次觸發時,首先根據游標拿出當前步驟結點,並判斷當前顯示的視圖控制器是否和步驟結點要求的匹配,如果匹配,則添加遮蓋,並將游標後移。
上文提到最後一個步驟結點是endNode,用於判斷調度的結束,endNode與其他步驟結點的區別是允許交互的視圖的keyPath為空,一旦發現keyPath為空,則認為調度結束,清空nodes釋放內存並且移除通知,並記錄游標的值為-1,以防止下次打開App時重復啟動調度。

- (void)trig:(NSNotification *)nof {
    if (self.cur >= self.nodes.count) return;
    SGGuideMaskView *maskView = [SGGuideMaskView sharedMask];
    UIViewController *topVc = nof.object[@"viewController"];
    SGGuideNode *node = self.nodes[self.cur];
    if ([topVc isKindOfClass:node.controllerClass]) {
        self.currentViewController = topVc;
        [maskView hide];
        self.cur++;
        if (node.permitViewPath == nil) {
            self.nodes = nil;
            [[NSNotificationCenter defaultCenter] removeObserver:self];
            [[NSUserDefaults standardUserDefaults] setInteger:-1 forKey:kSGGuideDispatcherCur];
            [[NSUserDefaults standardUserDefaults] synchronize];
            return;
        }
        maskView.node = node;
        [maskView showInViewController:topVc];
    }
}

總結

實現用戶引導有三個關鍵的類,引導結點SGGuideNode、遮蓋層SGGuideMaskView和調度器SGGuideDispatcher,將引導結點的數組傳遞給調度器即可開始調度,調度的觸發分為手動和自動兩種方式,攔截器(UIViewController的分類)對頁面切換進行攔截並觸發調度,不涉及到頁面切換的調度需要編程者通過調度器的next方法實現。每次觸發調度時先判斷是否與引導結點相符,相符則添加遮蓋層並向後推進。
通過這樣的設計,實現了幾乎無侵入的用戶引導,它不會破壞工程的結構,能提供良好的用戶引導效果。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved