iOS实战 | 根据原型图,封装最适合你APP的整套弹窗
背景
产品提供的原型图:
这一系列弹窗其实就相当于系统弹窗,改了下按钮颜色。
我看网上很多同学分享的都是使用KVC对私有属性进行赋值。虽然要的效果满足了,但我总感觉对系统控件的私有属性进行操作,不是那么让人心安。
万一苹果哪个版本把这个私有属性的名称改了,你能奈何?
而且UIAlertController
有个坑(也不能说是坑吧,反正苹果就是这样设计的):按钮点击回调block不是瞬发,而是在弹窗完全消失的时候,从点击按钮到block回调,这个过程有零点几秒。你或许觉得慢个零点几秒无所谓,但我告诉你,如果你这里要处理的逻辑是跟键盘显示隐藏相关联的,那有可能你会遇到意想不到的bug。反正我之前就是因为这个最后不得不封装了一个跟系统弹窗看起来一样的弹窗。
经验之谈,希望你们记住UIAlertController
按钮点击回调block不是瞬发,以后遇到了不要找bug找半天。
OK,回归正题——
如何封装最适合你APP的整套弹窗?
答案很简单:自定义弹窗。
唯有自定义。
自定义弹窗的文章我都写了几篇了,我感觉从第一篇到这一篇都可以做成一个自定义控件升级之路了。
经验告诉我们,一个APP中的所有弹窗,它们相似不相同。所以,封装整套弹窗,关键就是:
找不同
以上面的原型图为例:
不同的地方有:
- title可有可无
- 按钮数量不定
- 按钮字体颜色不定
封装的时候就要考虑到这几点。不同的地方,由参数体现。
因此,接口大体应该是这样的:
/**
弹窗快捷调用方法
@param title 标题
@param content 内容
@param buttonTitles 按钮title数组
@param buttonClickedBlock 按钮点击回调
@return 自定义弹窗实例
*/
+ (instancetype)showWithTitle:(nullable NSString *)title content:(NSString *)content buttonTitles:(NSArray *)buttonTitles buttonClickedBlock:(nullable void (^)(NSInteger index))buttonClickedBlock;
细节来了
title
和buttonClickedBlock
可以为空,故用nullable
修饰。
稍微细心的可能还发现了,我只传了一个按钮title数组,但是并没有说哪个是主按钮(主题色加粗按钮)。
所以我还需要添加一个参数如mainButtonIndex
来表示哪个按钮是主按钮。
不过,真的需要吗?
观察原型图可知,主按钮都在最右边,因此可猜测:项目中的主按钮都在最右边。但这仅仅是猜测,这个时候需要问产品。
我问了产品。
产品说的:“嗯嗯嗯。。。都在右边嘛。”
从产品勉强的回答中可知:他自己都没有注意到这个问题。。。
但可以确定的是主按钮基本上甚至全部都在右边。
但为了以防万一哪天有个特殊的弹窗需要把主按钮放左边,我们还是得留一手(实际上我之前做APP的时候就遇到过这种情况,产品对某个弹窗做特殊要求,这也是我这里考虑留一手的原因。经验之谈,经验之谈。)。
因此,我们这里还是需要添加一个参数表明哪个按钮是主按钮。
如果你真这样想就太young了。
你想想,每次调用弹窗快捷show方法的时候都要传一个明明就有默认值的参数,这,是不是有点蛋蛋的忧伤?
所以,更优雅的设计方案是再开一个接口:
/** 设置第几个按钮是主按钮(主按钮为主题色粗体),默认最右边那个是主按钮 */
- (void)setMainButtonIndex:(NSInteger)mainButtonIndex;
这样,绝大部分情况下,我们只需单独调用快捷方法:
[IChuAlertView showWithTitle:@"是否兑换" content:@"消耗20积分" buttonTitles:@[@"取消", @"确定"] buttonClickedBlock:^(NSInteger index) {
[CQToast showWithMessage:[NSString stringWithFormat:@"第%ld个按钮点击", index]];
}];
默认情况下主按钮在右边
特殊情况下,再调一个方法:
IChuAlertView *alertView = [IChuAlertView showWithTitle:@"做我的女朋友" content:@"请摸着你的36D酥胸回答" buttonTitles:@[@"勉强同意", @"欣然接受"] buttonClickedBlock:^(NSInteger index) {
[CQToast showWithMessage:[NSString stringWithFormat:@"第%ld个按钮点击", index]];
}];
// 设置左边的按钮为主按钮
[alertView setMainButtonIndex:0];
主按钮在左边
这样的话接口没有多余参数,干净纯粹,调用也非常方便。
配套的其它样式弹窗
- 无标题一个按钮:
[IChuAlertView showWithTitle:nil content:@"当前网络不给力" buttonTitles:@[@"知道了"] buttonClickedBlock:nil];
- 多行标题多行内容
整套弹窗的代码
.h文件:
NS_ASSUME_NONNULL_BEGIN
@interface IChuAlertView : UIView
/**
弹窗快捷调用方法
@param title 标题
@param content 内容
@param buttonTitles 按钮title数组
@param buttonClickedBlock 按钮点击回调
@return 自定义弹窗实例
*/
+ (instancetype)showWithTitle:(nullable NSString *)title content:(NSString *)content buttonTitles:(NSArray *)buttonTitles buttonClickedBlock:(nullable void (^)(NSInteger index))buttonClickedBlock;
/** 设置第几个按钮是主按钮(主按钮为主题色粗体),默认最右边那个是主按钮 */
- (void)setMainButtonIndex:(NSInteger)mainButtonIndex;
@end
NS_ASSUME_NONNULL_END
.m文件:
#import <Masonry.h>
// 主按钮字体颜色
#define MAIN_BUTTON_COLOR [UIColor orangeColor]
// 主按钮font
#define MAIN_BUTTON_FONT [UIFont boldSystemFontOfSize:18]
// 普通按钮字体颜色
#define NORMAL_BUTTON_COLOR [UIColor blackColor]
// 普通按钮font
#define NORMAL_BUTTON_FONT [UIFont systemFontOfSize:18]
// 灰色线的颜色
#define LINE_COLOR [UIColor lightGrayColor]
static NSInteger const kButtonBeginTag = 1000;
@interface IChuAlertView ()
/** 按钮数量 */
@property (nonatomic, assign) NSInteger buttonCount;
/** 内容view */
@property (nonatomic, strong) UIView *contentView;
/** 按钮点击回调 */
@property (nonatomic, copy) void (^buttonClickedBlock)(NSInteger index);
@end
@implementation IChuAlertView
#pragma mark - show
/**
弹窗快捷调用方法
@param title 标题
@param content 内容
@param buttonTitles 按钮title数组
@param buttonClickedBlock 按钮点击回调
@return 自定义弹窗实例
*/
+ (instancetype)showWithTitle:(nullable NSString *)title content:(NSString *)content buttonTitles:(NSArray *)buttonTitles buttonClickedBlock:(nullable void (^)(NSInteger index))buttonClickedBlock {
IChuAlertView *alertView = [[IChuAlertView alloc] initWithTitle:title content:content buttonTitles:buttonTitles buttonClickedBlock:buttonClickedBlock];
[[UIApplication sharedApplication].delegate.window addSubview:alertView];
[alertView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(alertView.superview);
}];
// 入场动画
alertView.alpha = 0;
alertView.contentView.transform = CGAffineTransformScale(alertView.contentView.transform, 1.2, 1.2);
[UIView animateWithDuration:0.2 animations:^{
alertView.alpha = 1;
alertView.contentView.transform = CGAffineTransformIdentity;
}];
return alertView;
}
#pragma mark - 设置主按钮
/** 设置第几个按钮是主按钮(主按钮为主题色粗体),默认最右边那个是主按钮 */
- (void)setMainButtonIndex:(NSInteger)mainButtonIndex {
if (mainButtonIndex >= self.buttonCount) {
return;
}
for (int i = 0; i < self.buttonCount; i++) {
UIButton *button = [self viewWithTag:(i + kButtonBeginTag)];
if (mainButtonIndex == i) {
[button setTitleColor:MAIN_BUTTON_COLOR forState:UIControlStateNormal];
[button.titleLabel setFont:MAIN_BUTTON_FONT];
} else {
[button setTitleColor:NORMAL_BUTTON_COLOR forState:UIControlStateNormal];
[button.titleLabel setFont:NORMAL_BUTTON_FONT];
}
}
}
#pragma mark - other
/**
自定义构造方法
@param title 标题
@param content 内容
@param buttonTitles 按钮title数组
@param buttonClickedBlock 按钮点击回调
@return 自定义弹窗实例
*/
- (instancetype)initWithTitle:(nullable NSString *)title content:(NSString *)content buttonTitles:(NSArray *)buttonTitles buttonClickedBlock:(void (^)(NSInteger))buttonClickedBlock {
if (self = [super init]) {
self.buttonCount = buttonTitles.count;
self.buttonClickedBlock = buttonClickedBlock;
self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.7];
//========== 布局 ==========//
//------- 内容view,使用自动布局,子view将内容view撑开 -------//
self.contentView = [[UIView alloc] init];
[self addSubview:self.contentView];
self.contentView.backgroundColor = [UIColor whiteColor];
self.contentView.layer.cornerRadius = 8;
[self.contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(self);
make.left.mas_equalTo(60);
make.right.mas_offset(-60);
}];
//------- title -------//
UILabel *titleLabel = [[UILabel alloc] init];
[self.contentView addSubview:titleLabel];
titleLabel.font = [UIFont boldSystemFontOfSize:18];
titleLabel.textAlignment = NSTextAlignmentCenter;
titleLabel.textColor = [UIColor blackColor];
titleLabel.numberOfLines = 0;
titleLabel.text = title;
[titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_offset(20);
make.left.mas_offset(12);
make.right.mas_offset(-12);
}];
//------- content -------//
UILabel *contentLabel = [[UILabel alloc] init];
[self.contentView addSubview:contentLabel];
contentLabel.textAlignment = NSTextAlignmentCenter;
contentLabel.numberOfLines = 0;
contentLabel.text = content;
[contentLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.mas_equalTo(titleLabel);
}];
if (!title || [title isEqualToString:@""]) {
// 没有标题时
contentLabel.font = [UIFont boldSystemFontOfSize:16];
contentLabel.textColor = [UIColor blackColor];
[contentLabel mas_updateConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(20);
}];
} else {
contentLabel.font = [UIFont systemFontOfSize:14];
contentLabel.textColor = [UIColor grayColor];
[contentLabel mas_updateConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(titleLabel.mas_bottom).mas_offset(10);
}];
}
//------- 灰色横线 -------//
UIView *lineView = [[UIView alloc] init];
[self.contentView addSubview:lineView];
lineView.backgroundColor = LINE_COLOR;
[lineView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(contentLabel.mas_bottom).mas_offset(20);
make.left.right.mas_offset(0);
make.height.mas_equalTo(1);
}];
//------- 循环创建按钮 -------//
if (buttonTitles.count == 0) {
NSAssert(nil, @"弹窗按钮数量不能为0");
} else if (buttonTitles.count == 1) {
UIButton *button = [[UIButton alloc] init];
[self.contentView addSubview:button];
button.tag = kButtonBeginTag;
[button setTitle:buttonTitles.firstObject forState:UIControlStateNormal];
[button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
[button.titleLabel setFont:MAIN_BUTTON_FONT];
[button setTitleColor:MAIN_BUTTON_COLOR forState:UIControlStateNormal];
[button mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(lineView.mas_bottom);
make.height.mas_equalTo(50);
make.bottom.mas_offset(0);
make.left.right.mas_offset(0);
}];
} else {
NSMutableArray *buttonArray = [NSMutableArray array];
for (int i = 0; i < buttonTitles.count; i++) {
UIButton *button = [[UIButton alloc] init];
[self.contentView addSubview:button];
button.tag = kButtonBeginTag + i;
[button setTitle:buttonTitles[i] forState:UIControlStateNormal];
[button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
[buttonArray addObject:button];
if (i == (buttonTitles.count - 1)) {
// 最右边的按钮
[button.titleLabel setFont:MAIN_BUTTON_FONT];
[button setTitleColor:MAIN_BUTTON_COLOR forState:UIControlStateNormal];
} else {
[button.titleLabel setFont:NORMAL_BUTTON_FONT];
[button setTitleColor:NORMAL_BUTTON_COLOR forState:UIControlStateNormal];
// 添加灰色竖线
UIView *lineView = [[UIView alloc] init];
[button addSubview:lineView];
lineView.backgroundColor = LINE_COLOR;
[lineView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.bottom.mas_equalTo(button);
make.width.mas_equalTo(1);
make.right.mas_offset(-0.5);
}];
}
}
[buttonArray mas_distributeViewsAlongAxis:MASAxisTypeHorizontal withFixedSpacing:0 leadSpacing:0 tailSpacing:0];
[buttonArray mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(lineView.mas_bottom);
make.height.mas_equalTo(50);
make.bottom.mas_offset(0);
}];
}
}
return self;
}
- (void)dealloc {
NSLog(@"弹窗已释放");
}
- (void)buttonClicked:(UIButton *)sender {
!self.buttonClickedBlock ?: self.buttonClickedBlock(sender.tag - kButtonBeginTag);
[self removeFromSuperview];
}
@end
demo
这里提供了这套弹窗的demo,也有其他弹窗或控件,需要的读者可以了解下:
https://github.com/CaiWanFeng/AlertToastHUD
重要通报
本人写UI写业务写了快3年了,现在还在写UI写业务,以后应该还是写UI写业务。公认的写业务好手,如果你们公司需要写UI写业务的专家,欢迎私聊我,保证给你写得巴巴适适。
不仅仅是写UI写业务,架构方面也有自己的心得,代码规范什么的更不用说了。
我这个全职外包做到4月底,5月即可入职。
看上的私聊哈