iOS開發彈窗順序顯示

需求:app啓動的時候總是會顯示許許多多的彈窗,那麼有一個需求就是讓這種彈窗一個個的顯示,點掉一個顯示下一個。碰到這樣的需求該如何搞定呢。
解決方案:有2種實現方案,第一種是創建一個彈窗池來管控彈窗順序,第二種是利用線程、信號量來完成順序執行。

第一種方案:

封裝GYPopupManager彈窗池,裏面維護了一個彈窗的數組。

//
//  GYPopupManager.h
//  testUI
//
//  Created by gaoyu on 2021/9/8.
//

/**
 ⚠️⚠️⚠️:GYPopupManager
 此管理類不關心彈窗的show和hide
 只約束彈窗是否被攔截一個一個展示、或者某幾個同時展示
 */
#import <Foundation/Foundation.h>

///優先級枚舉
typedef NS_ENUM(NSUInteger, GYPopupPriority) {
    GYPopupPriorityLow = 1,     /// 低
    GYPopupPriorityMedium,      /// 中
    GYPopupPriorityHigh         /// 高
};

/// 回調
typedef void (^GYPopupBlock)(void);

// MARK: - GYPopupConfig 彈出內容配置信息
@interface GYPopupConfig : NSObject

/// 是否被攔截:默認YES
@property (nonatomic, assign) BOOL isIntercept;
/// 當前彈窗是否在展示
@property (nonatomic, assign) BOOL isShowing;
/// 彈窗優先級:默認爲High(相同優先級的彈窗後加入的先展示,因爲字典添加元素後默認會在第一位,所以轉化爲數組後也是第一個)
@property (nonatomic, assign) GYPopupPriority priority;
/// 彈窗標識:以類名爲標識,便於排查
@property (nonatomic, copy, nonnull) NSString *popupClassName;
/// 展示回調
@property (nonatomic, copy, nonnull) GYPopupBlock showBlock;
/// 隱藏回調
@property (nonatomic, copy, nullable) GYPopupBlock dismissBlock;

@end




// MARK: - GYPopupManager 彈出內容管理類
@interface GYPopupManager : NSObject

/// 單例對象
+ (instancetype _Nonnull)shared;

/// 展示彈窗
/// @param popupClass 彈出內容class
/// @param showBlock 展示回調
/// @param dismissBlock 隱藏回調
- (void)popupWithClass:(_Nonnull Class)popupClass show:(_Nonnull GYPopupBlock)showBlock dismiss:(_Nullable GYPopupBlock)dismissBlock;

/// 展示彈窗
/// @param popupClass 彈出內容class
/// @param priority 優先級
/// @param showBlock 展示回調
/// @param dismissBlock 隱藏回調
- (void)popupWithClass:(_Nonnull Class)popupClass priority:(GYPopupPriority)priority show:(_Nonnull GYPopupBlock)showBlock dismiss:(_Nullable GYPopupBlock)dismissBlock;

/// 展示彈窗
/// @param popupClass 彈出內容class
/// @param priority 優先級
/// @param isIntercept 是否需要被攔截
/// @param showBlock 展示回調
/// @param dismissBlock 隱藏回調
- (void)popupWithClass:(_Nonnull Class)popupClass priority:(GYPopupPriority)priority isIntercept:(BOOL)isIntercept show:(_Nonnull GYPopupBlock)showBlock dismiss:(_Nullable GYPopupBlock)dismissBlock;

/// 隱藏彈窗
- (void)dismissPopup;

@end

.m實現

//
//  GYPopupManager.m
//  testUI
//
//  Created by gaoyu on 2021/9/8.
//

#import "GYPopupManager.h"

// MARK: - PRAlertViewConfig
@implementation GYPopupConfig

- (instancetype)init {
    self = [super init];
    if (self) {
        self.isIntercept = YES;
        self.popupClassName = @"defaultName";
        self.priority = GYPopupPriorityHigh;
    }
    return self;
}

@end




// MARK: - GYPopupManager
@interface GYPopupManager()

/// 彈出池:key:彈窗className、value:config對象
@property (nonatomic, strong) NSMutableDictionary *popupPool;
/// 當前彈窗內容
@property (nonatomic, strong) GYPopupConfig *currnetPopup;

@end

@implementation GYPopupManager

static GYPopupManager *_instance = nil;

+ (instancetype)shared {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[GYPopupManager alloc] init];
    });
    return _instance;
}

// MARK: - Public Methods
- (void)popupWithClass:(Class)popupClass show:(GYPopupBlock)showBlock dismiss:(GYPopupBlock)dismissBlock{
    [self popupWithClass:popupClass priority:GYPopupPriorityHigh isIntercept:YES show:showBlock dismiss:dismissBlock];
}

- (void)popupWithClass:(Class)popupClass priority:(GYPopupPriority)priority show:(GYPopupBlock)showBlock dismiss:(GYPopupBlock)dismissBlock {
    [self popupWithClass:popupClass priority:priority isIntercept:YES show:showBlock dismiss:dismissBlock];
}

- (void)popupWithClass:(Class)popupClass priority:(GYPopupPriority)priority isIntercept: (BOOL)isIntercept show:(GYPopupBlock)showBlock dismiss:(GYPopupBlock)dismissBlock {
    GYPopupConfig *config = [[GYPopupConfig alloc] init];
    config.popupClassName = NSStringFromClass(popupClass);
    config.priority = priority;
    config.isIntercept = isIntercept;
    [self popupWithConfig:config show:showBlock dismiss:dismissBlock];
    
}

// MARK: - Private Methods
- (void)popupWithConfig:(GYPopupConfig *)config show:(GYPopupBlock)showBlock dismiss:(GYPopupBlock)dismissBlock {
    config.showBlock = showBlock;
    config.dismissBlock = dismissBlock;
    config.isShowing = YES;
    
    // 加入彈出池,同一個彈窗避免重複添加
    [self.popupPool setObject:config forKey:config.popupClassName];
    
    // 當前有彈窗在顯示:self.popupPool.allKeys.count > 1
    // 當前彈窗被攔截:isIntercept == YES
    if (config.isIntercept && self.popupPool.allKeys.count > 1) {
        config.isShowing = NO;
        return;
    }
    
    self.currnetPopup = config;
    
    //回主線程
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        showBlock ? showBlock() : nil;
    });
}


/// 清除彈出內容
- (void)dismissPopup {
    // 獲取到當前彈出內容並刪除
    [self deletePopupWithConfig:self.currnetPopup];
    
    // 是否有彈窗在顯示 是:攔截其他未顯示彈窗;否:查看是否有下一個可顯示彈窗並顯示
    if ([self isShowingSomeAlert]) {
        return;
    } else {
        // 優先級排序
        NSArray * values = [self.popupPool allValues];
        values = [self sortByPriority:values];
        
        // 當前沒有正在展示的彈窗,則展示被攔截的彈窗
        if (values.count > 0) {
            // 查詢是否有可以展示的彈窗:條件:1.已加入緩存、2.被攔截 3、實現了展示回調
            // 優先級 1 > 2 > 3
            // 找到一個立即展示,並退出循環(相同優先級的彈窗後加入的先展示,因爲字典添加元素後默認會在第一位,所以轉化爲數組後也是第一個)
            for (GYPopupConfig *config in values) {
                GYPopupBlock showBlock = config.showBlock;
                if (config.isIntercept && showBlock) {
                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                        config.isShowing = YES;
                        self.currnetPopup = config;
                        showBlock();
                    });
                    break;
                }
            }
        }
    }
}

/// 是否正在展示某個彈窗
- (BOOL)isShowingSomeAlert{
    __block BOOL isShowSomeAlert = NO;
    [self.popupPool enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        GYPopupConfig *config = obj;
        if (config.isShowing) {
            isShowSomeAlert = YES;
            *stop = YES;
        }
    }];
    return isShowSomeAlert;
}

/// 根據優先級排序
/// @param configArray 彈窗配置數組
- (NSArray *)sortByPriority:(NSArray *)configArray {
    NSComparator comparator = ^(GYPopupConfig *obj1, GYPopupConfig *obj2) {
        if (obj1.priority > obj2.priority) {
            return NSOrderedAscending;
        }
        if (obj1.priority < obj2.priority) {
            return NSOrderedDescending;
        }
        return NSOrderedSame;
    };
    return [configArray sortedArrayUsingComparator:comparator];
}

/// 根據配置刪除指定彈出內容
/// @param config 彈出配置
- (void)deletePopupWithConfig:(GYPopupConfig *)config {
    if (config == nil) return;
    GYPopupBlock dismissBlock = config.dismissBlock;
    dismissBlock ? dismissBlock() : nil;
    if ([self.popupPool.allKeys containsObject:config.popupClassName]) {
        [self.popupPool removeObjectForKey:config.popupClassName];
    }
    self.currnetPopup = nil;
}
 
// MARK: - Getters
- (NSMutableDictionary *)popupPool {
    if (!_popupPool) {
        _popupPool = [[NSMutableDictionary alloc] init];
    }
    return _popupPool;
}

@end


把彈窗全部加到封裝好的GYPopupManager彈窗池中

[[GYPopupManager shared] popupWithClass:[Toast class] priority:GYPopupPriorityMedium show:^{
            // 彈窗的展示
            [[Toast shared] show];
} dismiss:^{

}];

// 彈窗消失
[[GYPopupManager shared] dismissPopup];
第二種方案:

通過子線程+信號量去阻塞後續彈窗的展示,來完成順序彈窗

思路:
創建全局只容納1個單位的信號量
Show的時候 Lock
Dismiss的時候 Release Lock

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface BaseAlertView : UIView

- (void)show;

- (void)dismiss;

@end

NS_ASSUME_NONNULL_END

.m實現

#import "BaseAlertView.h"

//全局信號量
dispatch_semaphore_t _globalInstancesLock;
//執行QUEUE的Name
char *QUEUE_NAME = "com.alert.queue";

//初始化 -- 借鑑YYWebImage的寫法
static void _AlertViewInitGlobal() {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _globalInstancesLock = dispatch_semaphore_create(1);
    });
}

@implementation BaseAlertView
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:[UIScreen mainScreen].bounds];
    if (self)
    {
        _AlertViewInitGlobal();
    }
    return self;
}

#pragma mark - public
- (void)show
{
    //位於非主線程 不阻塞
    dispatch_async(dispatch_queue_create(QUEUE_NAME, DISPATCH_QUEUE_SERIAL), ^{
        //Lock
        dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
        //保證主線程UI操作
        dispatch_async(dispatch_get_main_queue(), ^{
            [[UIApplication sharedApplication].windows.firstObject addSubview:(UIView *)self];
        });
    });
}

- (void)dismiss
{
    dispatch_async(dispatch_queue_create(QUEUE_NAME, DISPATCH_QUEUE_SERIAL), ^{
        //Release Lock
        dispatch_semaphore_signal(_globalInstancesLock);

        dispatch_async(dispatch_get_main_queue(), ^{
            [self removeFromSuperview];
        });
    });
}

@end

實現方法:彈窗的view繼承於BaseAlertView,在展示的時候調用父類的show方法即可。

參考文檔:
iOS彈窗順序彈出管理
iOS 彈窗順序顯示 -- 信號量實踐

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章