作者简介

jun.liu,货拉拉高档客户端工程师,首要担任货拉拉iOS移动端的开发工作。

一、布景

跟着货拉拉移动端APP的事务不断的发展,为了统筹需求迭代功率,以及降低项意图保护杂乱度,咱们做了许多的功用和事务逻辑架构上的优化,其中就包括iOS端的弹窗调度办理办理,那么这篇文章就详细论述下弹窗调度优化项意图一些细节和原理。

在进入正题之前,咱们先来聊一下为什么咱们要做弹窗调度组件,以及现在市道上是否有相似成熟的处理计划,「弹窗调度」这个名词关于有些开发者或许感觉有点陌生,可是「弹窗」这个词大家都会比较熟悉,UIKit中的UIAlertController便是Apple为开发者供给的最根底的系统弹窗控件,可是往往咱们在详细的项目开发中,会需求不同UI风格的弹窗款式,且场景也是跟着事务场景林林总总,所以不同风格款式的弹窗,以及不同场景的弹窗就会跟着事务的迭代,变的越来越多且比较零星欠好办理保护,下面就拿咱们货拉拉APP的一些场景举几个比方:

  • 主页启动后的一些弹窗(优惠活动、未完单信息、版本更新提示、公告等)之类的弹窗应该怎样更好的进行次序展现?弹窗完成上怎样能更好的逻辑解耦?
  • 事务开发中的一些辅佐半页弹窗,一般是当时页面用来填写交互信息的弹窗,这些半页之类的弹窗应该怎样在事务中便利的去完成,让事务同学只重视弹窗内容,而不去考虑当时页面的其他弹窗之间的交互。
  • 一些强事务相关性的场景,比方:司机报价告诉、用户报价、用户加价等交互性比较强的弹窗,怎样在事务中更好更便利的完成?

等等这些都是咱们开发中跟着事务的迭代,呈现的一些痛点,而弹窗调度的规划初衷便是力求处理这些痛点,而现在市道并没有一个能够说很好处理当时痛点的一些开源计划,也是由于这些场景比较依靠事务逻辑,没有特定的杂乱事务场景去测验,很难去提炼出好的适合的一个处理计划。

因此,咱们开始了自研「iOS弹窗调度」计划,一方面为了更好的处理咱们项目中的痛点,另一方面也打算经过开源方法来吸取更多的优秀建议,让它越来越完善。

这是咱们GitHub的开源代码:github.com/HuolalaTech…

二、聊一聊都有哪些弹窗?

其完成在咱们大部分人都是手机的重度运用者,咱们在运用手机上的APP时,或多或少都会看到过各品种型的弹窗视图,比方:新版本晋级提示啊、优惠活动啊、或许一些手机权限的请求弹窗等等。

货拉拉iOS弹窗调度方案设计与实践

尽管弹窗的款式有许多,但假设咱们依照类型可大致分为以下几类:

货拉拉iOS弹窗调度方案设计与实践

各类型弹窗的详细解释:

  • 运营相关

    • 这类首要以APP相关运营概念的一些弹窗,比方电商类APP的优惠券下发,以及一些活动海报等,一般展现在手机屏幕中心,曝光给用户
    • 货拉拉iOS弹窗调度方案设计与实践
  • 事务相关

    • 这类就比较归于功用类的弹窗了,一般和用户的运用场景密切相关,比方有司机给用户下的拉货单报价了一个新价格,咱们要及时展现给用户查看等,这些就比较多了,所以归归于APP产品的功用事务这块,展现的款式也是最杂乱多样的。

    • 货拉拉iOS弹窗调度方案设计与实践
      货拉拉iOS弹窗调度方案设计与实践

  • 轻提示(APP内顶部告诉条)

    • 这类严厉意义上不归于弹窗,由于弹窗有一个根本的断定因素是:有必要让用户聚集当时弹窗,什么意思呢,便是要让用户马上处理掉当时的弹窗,比方点击按钮关闭,或许点击布景关闭,而告诉条作为一个轻提示,在APP中也是不乏呈现,所以咱们暂时也给他当作一类,可是详细处理的时分会依据告诉条的特性区别对待。
    • 货拉拉iOS弹窗调度方案设计与实践
  • 不规则弹窗

    • 为什么分出来一类不规则弹窗呢,由于有些款式关于用户而言第一次看并不会觉得是一个弹窗,可是关于咱们写程序而言它也是能够被兼容到弹窗处理中的,比方一些闪屏页、一些新版本对用户进行引导的一些视图、或则是一些页面上呈现的小的提示视图等,这些尽管是不规则的,可是只要他满意了弹窗的根本特性,那么就能用程序给他归类兼容。

看到这儿有些同学会问,为什么告诉条也算是弹窗呢?其实这儿咱们只是把告诉条类型也归入到弹窗调度概念中来,其实你能够笼统一点,把它理解为调度办理APP页面中会动态呈现的一些widget,这样就不会仅仅局限于一般弹窗方法里边了,但咱们的起点更多的还是以弹窗为主,只是让调度愈加的能兼容其他类型。

分析完了弹窗的各种款式后,咱们来看下一般咱们项目开发中的一些惯例弹窗完成方法。

三、惯例完成方法

在讲完成原理之前咱们先来看下一般情况下,咱们是怎样简略快速的Pop出一个弹窗视图的。

一般弹窗代码的写法

咱们一般在开发的过程中要进行一个弹窗展现一般第一步会创立一个自定义View来编写弹窗的详细UI页面,然后经过完成一个根底的动画来将其添加到咱们的当时的视图上,代码大概是这样的:

//创立一个自定义的View类来编写弹窗UI视图
@interface XXX_PopView : UIView
+ (instancetype)show;
@end
//然后在事务中需求弹窗的当地触发
[XXX_PopView show];
...

咱们在项目规划较小或许事务不杂乱的时分,这样写或许更简略一点,可是渐渐的你会发现,事务中弹窗多了之后。每新增一个弹窗不光要独自造一个弹窗出来,假设事务当时逻辑中有和其他当地的弹窗有依靠,那么还要考虑其他当地的弹窗是否有抵触或许谁先展现的问题。

面临难保护的问题:

  • 弹窗一般都有一个一致的黑色加了透明的布景遮挡,每次写一个弹窗也要考虑处理这块的代码,代码无法复用
  • 弹窗假设想支撑一些呈现或消失的动画,需求在自己弹窗中独自完成
  • 弹窗和弹窗之间没有优先级以及其他的束缚限制,或许会造成弹窗之间pop时分的抵触
  • 事务完成起来需求考虑太多重复的逻辑,这些重复的逻辑其实都是一切弹窗通用的,能够进行抽离

以上这些问题还只是咱们开发和保护时发现比较繁琐的问题需求处理,假设项目中弹窗越来越多,那么后续的保护难度或许会更大。

希望的弹窗完成

发现了弊端,那么咱们怎样去优化改造,优化的目标便是咱们希望的弹窗完成方法,咱们希望事务中运用弹窗的流程是这样的:

  1. 创立自定义View编写弹窗详细事务UI代码(这块有必要由事务来做,并且这儿的UI代码只包含弹窗的内容,不包含弹窗的动画、键盘适应、自动躲藏等逻辑)
  2. 简略装备弹窗特点后,直接丢给弹窗调度组件就能够了
  3. 弹窗的生命周期等回调,事务中只需求接收该回调即可

经过改造优化后,咱们事务开发中重视的焦点就首要集中在弹窗视图的UI和逻辑处理了,而不再关怀弹窗会不会和其他弹窗有影响,弹出动画的编写,以及布景色和触摸手势等一系列通用问题了,这些问题都交给弹窗调度组件去一致办理了。

四、弹窗调度的原理

前面说了这么多关于弹窗的品种和一般的完成,接下来就详细说下弹窗调度的完成原理,其实弹窗调度的原理并不杂乱,首要的逻辑处理便是在「调度」一词中,弹窗在事务而调度在办理,你能够想像一下咱们日子中的十字路口红绿灯的规划,经过红绿灯系统的调控,是怎样处理南来北往的车辆和行人的。

类图规划分析

弹窗调度的类文件总共分为三大类:

  1. 调度办理类
  2. 弹窗特点装备类
  3. 接口协议

内部的类图大致如下:

货拉拉iOS弹窗调度方案设计与实践

类图阐明:

  • HLLPopupInterface 协议

    • 这个协议首要是供给给事务中的弹窗类来完成的,里边供给了一些必要和非要的协议方法,例如弹窗类需求有必要完成:- (UIView *)supplyCustomPopupView;方法来给调度组件供给一个用来展现弹窗UI内容的View目标,除此之外还有一些弹窗当时的显现生命周期函数回调等。
  • HLLPopupConfigure 装备类

    • 这个类是一个弹窗装备类,首要是给弹窗进行一些特点装备,例如:
    • 装备场景类型,是归于底部弹窗还是中心弹窗,或许是顶部告诉栏
    • 装备你的优先级,来决议你的弹窗的展现次序
    • 装备动画类型,里边供给了几种根底动画可供运用
    • 弹窗的消失时刻
    • 等等这些特点大家能够在源码中看到运用的注释。
  • HLLPopupsManager 弹窗调度办理类

    • 这个类首要功用便是弹窗调度的办理中心类,它是一个单例类,经过公开的一些API接口,来让事务中便利的加入/移除弹窗

分析完了类图之后,咱们对弹窗调度的大体规划有了一个轮廓,那么下面咱们就来详细看下内部的完成。

调度原理

  • 容器

下面咱们来分析下弹窗的调度原理,首要咱们要知道弹窗在APP上呈现肯定是要有一个superview父视图来承载的,

HLLPopupConfigure装备类中供给了一个containerView特点,所以调度组件支撑事务中去决议你的弹窗应该加到哪个父视图中,默许会放到 [UIApplication sharedApplication].keyWindow 上,这样就避免了一些弹窗会依靠当时页面的ViewController来完成逻辑的麻烦了。

  • 存储行列

HLLPopupsManager内部用了一个数组的特点来寄存一切加入到调度办理中的弹窗目标,并且在加入到行列之前内部会依据设置的特点来调整其在行列中的位置,保证数组中的次序便是弹窗的显现次序。

货拉拉iOS弹窗调度方案设计与实践

除了优先级的因素以外,咱们还设置了一些其他的装备功用,比方:有些弹窗弹出后会把其他弹窗给挤掉,那么内部就会清空掉这些弹窗,再比方一些需求和键盘进行交互的底部弹窗,键盘的升起或许会和弹窗有抵触,这种内部也进行了适配处理。

  • 分组处理

分组的概念是尽管咱们的弹窗最终是依据优先级排列存储起来待展现的,可是每个弹窗目标会有一个分组ID叫做GroupID,这个分组ID的效果则是会将弹窗进行分组,不同分组的优先级次序并不会影响到其他分组。

举个比方:咱们货拉拉APP下单后在一些议价的场景中,用户会和司机进行一些议价交互,两边会进行报价,在APP中表现则是会随时有价格的提示和价格输入等交互弹窗,一些议价弹窗场景就不想被其他的一些弹窗优先级和策略影响,比方我在报价的时分进行价格输入时假设有其他高优先级的弹窗所干扰,可是其他的弹窗又需求展现出来,那么经过分组的方法就可处理这种弹窗抵触场景,你能够把它理解为在弹窗这个调度行列中经过GroupID区分了许多个容器出来,各个容器的弹窗互不影响。

告诉条在完成的时分便是经过GroupID的方法规划的,这样告诉条的显现和其他弹窗并不会有抵触,从APP的一般运用习惯规划上来讲,这种规划现在来看是合理的,尽管是告诉条可是从笼统的角度来说,他也算是一种「弹窗」。

丰厚的特性装备

除了上面介绍的调度原理之外,在此根底上弹窗调度还供给了许多个性化的装备特点,便利咱们开发中在一些特别场景中能够更好的处理需求,比方:

  • 弹窗的宿主视图
  • 守时消失
  • 弹窗时是否需求清空之前的弹窗行列
  • 布景遮罩的色彩

等等这些特点装备能够你处理大部分的弹窗需求,且装备起来也很便利,只需求创立一个HLLPopupConfigure装备目标,将其传入到弹窗API参数中即可。

示例代码:

/// 弹窗场景风格
@property (nonatomic, assign) HLLPopupScene sceneStyle;
/// 点击弹窗布景(弹窗内容之外的区域)弹窗是否消失 default NO
@property (nonatomic, assign, getter=isClickOutsideDismiss) BOOL clickOutsideDismiss;
/// 弹窗的容器视图,默许是当时APP的keywindow,能够设置成其他容器
@property (nonatomic, weak) UIView *containerView;
/// 继续时长 设置后会在设守时刻完毕后自动dismiss,不设置不会自动消失
@property (nonatomic, assign) NSTimeInterval dismissDuration;
/// 该特点默许NO。设置YES会让之前的一切同组弹窗悉数清除去(优先级特点失效)
@property (nonatomic, assign, getter=isAloneMode) BOOL aloneMode;
/// 和aloneMode模式相似,不过terminatorMode会清除去之前一切分组的弹窗
@property (nonatomic, assign, getter=isTerminatorMode) BOOL terminatorMode;

五、详细场景实践

接下来经过几个小的详细事务场景来看下「弹窗调度」的详细实践,

  • 顶部告诉条

首要咱们先来看下顶部告诉条这种运用弹窗调度组件应该怎样去办理,首要咱们需求一个告诉条的自定义view类,那么一般他是继承自UIView的,然后恪守咱们的弹窗调度协议:

@interface TopBarPopView : UIView<HLLPopupInterface>
@end

依据协议供给的方法进行完成装备,必要方法是你要供给给「弹窗调度」一个详细的弹窗view视图,其他的比方生命周期回调,则能够依据详细场景挑选完成。

之后就能够在告诉条触发的当地将它交给「弹窗调度」进行展现了:

    // 特性装备
    HLLPopupConfigure *config = [[HLLPopupConfigure alloc] init];
    config.sceneStyle = HLLPopupSceneTopNoticeView;//类型
    config.dismissDuration = 3;//推迟消失
    config.cornerRadius = 8;//UI圆角风格
    //事务自定义弹窗类
    TopBarPopView *topBar = [[TopBarPopView alloc] init];
    //「弹窗调度」组件
    [HLLPopupsManager addPopup:topBar options:config];

上述代码完成之后,你的顶部告诉条就能够在触发的时分进入行列正常显现了,假设有比它优先级高的,那么它会进入等候,假设它的优先级最高,则会当即展现,之后规守时刻内自动躲藏,而事务代码中只需求关怀顶部告诉条的款式应该怎样写就能够了。

  • 用户引导蒙层

这种事务需求咱们一样能够将其放入到「弹窗调度」组件中办理,乃至咱们能够利用分组方法来处理一些弹窗需求和引导蒙层一同呈现的场景,咱们咱们的用户蒙层呈现时,假设有重要的弹窗需求给用户展现,或则用户的订单有司机议价告诉等场景都能够满意,而咱们开发的时分只需求办理怎样写弹窗界面UI即可,不需求去考弹窗之间的交互和影响,只要你装备好了特点,一切交给弹窗调度完成就行了。

@property (nonatomic, copy) NSString *groupID;
  • 特别特点

提到一些个别场景,比方用户被挤下线了,或许订单被取消了,这些场景有些需求弹窗给用户,然后之前的弹窗就无需保留了,由于被挤下线或订单取消后假设之前有一些弹窗在行列中,比方报价告诉,订单状况等和用户或许订单强相关的交互场景,这些弹窗也就没必要展现了,此刻你就能够装备弹窗的terminatorMode特点,这意味着该弹窗呈现后,之前的行列内的弹窗会被清空。

/// 该特点默许NO。设置YES会让之前的一切同组弹窗悉数清除去(优先级特点失效)
@property (nonatomic, assign, getter=isAloneMode) BOOL aloneMode;
/// 和aloneMode模式相似,不过terminatorMode会清除去之前一切分组的弹窗
@property (nonatomic, assign, getter=isTerminatorMode) BOOL terminatorMode;
  • 页面消失

组件内还供给了一些便捷的整理弹窗和查询弹窗的一些方法,比方一些场景需求页面消失的时分关闭掉当时展现的弹窗,或许某些弹窗只在特定的页面展现,查看某一弹窗当时是否正在展现等都能够经过下面这些API进行操作

/// 移除指定弹窗
/// @param popup popup:触发弹窗时传入的恪守协议的目标
+ (void)dismissWithPopup:(id<HLLPopupInterface>)popup;
/// 移除指定弹窗
/// @param identifier identifier: 事务调用中设置的仅有标识符
+ (void)dismissPopupWithIdentifier:(NSString *)identifier;
/// 从指定容器中移除一切的弹窗
/// @param containerView 指定容器,传nil则移除当时APP的keywindow上的
+ (void)removeAllPopupFromContainerView:(UIView *)containerView;
/// 移除调度办理中之前加入的一切弹窗
+ (void)removeAllPopup;
/// 获取指定容器中的一切弹窗个数
/// @param containerView 容器view
+ (NSInteger)getAllPopupCountFromContainerView:(UIView *)containerView;

前面大致归类的这几种场景有兴趣的同学能够结合源码来运用的自己的运用中进行实践操作下,依据自己的事务场景来详细体验下「弹窗调度」的效果,场景许多怎样更好的去兼容完成达到需求的意图才是最重要的,一同也欢迎有兴趣的同学提出一些更好的场景案例,来一同交流学习,假设不支撑的我也会考虑进行一些扩展和优化来对其进行支撑。

六、总结

文章到这儿就挨近尾声了,现在货拉拉iOS项目中已经有部分事务在渐进式的进行运用这套弹窗调度组件去开发了,场景包含:承认页、下单后的一些做单页面等,大概有40+的弹窗的事务场景在运用,后续的一些新增相关弹窗方面的需求,咱们也是优先考虑运用弹窗调度组件去办理,事务代码上对比之前精简了许多,许多杂乱的场景能够只重视弹窗的页面和逻辑即可,而不在去花时刻重视弹窗的调度逻辑。

弹窗调度其实首要的中心便是以调度为轴心,经过事务中不同弹窗的特点特征,来有序且正确的对每个场景,以及每个弹窗进行展现和躲藏,最大限度的做到和事务逻辑解耦,优化惯例弹窗完成的弊端,经过概括、分类再到调度完成,来更好的处理咱们前面所提到的开发痛点。

最终感谢大家对文章的阅览,感兴趣的同学能够在github上找到咱们货拉拉技术的开源项目(github.com/HuolalaTech…