2017.01.15

一、前言,为什么要做免登陆

  • 2017年1月9日,蓄势已久的小程序正式上线,着实,张小龙 用完即走 的理念发挥的淋漓尽致,无需下载,扫码可用,用完即走
  • 2017年互联网人口红利结束了,那么接下来除了__内容的精耕细作__外,就是__提高流量的转化率__,然而在流量转化为真实用户的道路上,一个登陆注册的入口挡住了运营活动多少真金白银砸出来的流量?
  • 在谈免登陆之前呢,我想先大概说下客户端登录,想必大家都耳熟能详,一般情况下需要包含以下几个方面【括号内部分为可选项】:
  • SNS 第三方快捷登陆
  • 邮箱+(验证码)+密码 登录注册
  • 手机号+验证码+(密码)登录注册
  • (提示用户上次在本机登录方式 && 账号)
  • 毫无疑问,相比于手机号、邮箱的登录注册,第三方登录是最方便的,在第三方app已经登录的前提下,需要以下两步操作:

    1. 用户第一次打开app需要小手点一下第三方图标
    2. 跳转到对应app后,点一下 确认授权 按钮即可返回自己的app完成登录

但是!!!

  • 在用户还没体验到你app任何亮点之前,凭什么让用户进行如此繁杂的操作,不要让用户思考!不要让用户麻烦!尤其是用户对隐私日渐重视的今天!!且不说某麦某东等用户账号密码泄露,就说前几天某德利用手中大数据强行一把秀优越。。。

我就问你要是凯迪拉克车主你还会用高德么?!(默默掏出裤兜里的地铁卡看了一眼。。)

  • 结论是用户是越来越重视自己的隐私的,用户在使用 app 的时候也不想进行任何多余的思考
  • 因此在用户下载 app 之后第一次打开,要狠下心去掉一切不必要的弹框(除国行iOS10必须弹出的蜂窝网络权限之外,其他接收通知、定位等权限最好放在需要的时候再弹出)
  • 除特殊软件(如网络电话)必须使用电话号码注册的,其他类似电商、内容浏览、交友软件、工具类等 app,都应该进行免登陆操作先让用户体验 app 的基本功能,在一些深度使用的高级功能上个添加门槛,提示用户进行登录注册操作

二、来几个常用 app 的例子

1. 今日头条:
  • 打开 app 后以游客身份进入,可以进行常规的新闻浏览、查看评论、收藏、分享、消息反馈等操作
  • 进行爆料、评论、查看阅读历史等操作的时候弹出登录框
  • 登录成功后,之前收藏的数据已迁移到正式用户名下
  • 如果实在发送评论的时候触发的登录操作,登录成功后评论发出,提示用户评论发送成功
2. 每日开眼
  • 同样的,进入 app 后可正常浏览,视频状态下进行点赞操作触发登录,你看这位女施主悬浮在泳池中,享受着柔和的阳光和微微清风,那曼妙的身材真是让作为用户的我忍不住登录,再退出,再登录。。。

但是!!!

如果你觉得我是因为女主人公的照片才举这个例子,呵呵,在下可不是那么肤浅的人,开眼的内容和设计以及 app 整体流畅度都很棒,但是免登陆这里有两个小瑕疵,在游客+横屏状态下

  • 观看视频的时候,点击收藏按钮,直接modal出竖屏的登录框,这点对用户不是很友好
  • 登录成功后,没有自动延续用户在登录之前的操作(收藏)

关于这两点的技术实现后面会讲

三、整体流程

  1. 用户首次进入 app 之后,判断之前是否在本机登录过,如果是用户首次登录,就调用 游客登录API,当然这个游客 guestId 是服务器根据设备号生成的,一般情况下,一个设备对应一个游客 guestId,而且这个游客 guestId 当然是不能展示给用户的(也可以在该接口返回一个上次登录信息,提示用户上次登录方式)

iPhone设备各种信息获取传送门

  • 然后使用这个游客 guestId 进行各项参数的初始化,比如数据库存取地址、下载文件路径、浏览记录等各方面操作的统计,当然该游客在进行一般操作的时候,就是使用这个游客 guestId 与服务器进行交互
  • 接着就要考虑弹出登录框的具体时机,当然每个 app 的产品特性不一样,一般会在以下几种情况下弹出登录框:收藏、评论、购买会员、下单购买商品等深度操作。
  • 还有就是万万不能在以下几种情况下弹出登录框:分享、用户反馈、添加到购物车等,因为这些操作是用户主动帮助分享app,提出意见,这时候弹出登录框,简直是搞事情!
  • 弹出登录框(注意横竖屏的适配),用户选择进行登录后,获取到一个正式的用户 userId,重新初始化各项参数,隐藏登录页,进行数据库迁移合并、下载内容路径迁移(大多下载需要用户相应的权限,防止作弊)、历史记录迁移合并、购物车内容迁移合并等
  • 最后继续进行用户需要登录之前的操作(通过block来实现)
  • 若用户进行退出登录操作,先调用退出登录的api,然后再调用游客登录的api

四、上代码之前,谈谈登录注册的一些小细节

  • 进入到登录注册页后,键盘应立刻弹出,需要邮箱的弹出字母键盘,需要手机号的弹出数字键盘
  • 当 两个输入框内容没有都达标之前,action按钮应该设置为disabled
  • 输入内容的时候考虑小屏幕适配,自动滑动到合适位置
  • 在文本输入框有内容之后,右侧应该设置❎按钮,供用户一键删除
  • 账号有没有长度限制,类似电话格式的判断在前端做比较方便,比如在密码框 becomeFirstResponder 的时候,就直接判断账号格式,如果错误需提示用户
  • 密码输入框需要设置明文暗文按钮,以供用户随时校验
  • 点击登录按钮后弹出菊花(当然我指的是 UIActivityIndicatorView,不是那个肥皂那个菊花)或者动画,防止多次发送网络请求
  • 对于登录注册信息出错,这个最好是能做到及时反馈,考虑下web端注册账户的时候,昵称是否已被占用能够在用户输入就提示,如果每次兴冲冲输入一大堆消息后,满怀期待的点击注册按钮,结果提示“您的昵称已被占用”,你对这个网站的好感是不是会降低那么一丢丢?因此最好能够在保证用户行为流畅的基础上提示用户,比如
    • 昵称限制10位,那么输入第11位的时候就应该是无效的
    • 最好统一登录注册界面:用户输入手机号、邮箱之后,实时查询数据库是否已注册,然后更新按钮状态
  • 也需要考虑网络超时、请求出错、服务器宕机、短信未发送成功等异常信息
  • 对于一些金融类相关的app,为了防止服务器被攻击(当然也),是不是要考虑同一IP请求两次后添加验证码(倒计时一般是前端固定的代码)
  • 如果登录失败,提示的信息一定要准确,比如是验证码错误,还是账户名密码错误虽然这个提示信息一般都是服务器同学来做

五、代码设计:啥都别说了,都在代码里

1. 首先在全局的控制器管理类写一个弹出 view 的方法
/** 大多情况下默认的添加方式,直接添加到最顶层的控制器上* title:弹出登录框的提示语,如登录后方可进行评论* block:用户被登录框所阻拦的操作(注意循环引用)*/
- (void)transferControlToPortalViewWithTitle:(NSString *)title block:(void(^)())block;
复制代码
2. 然后在收藏等深度操作需要提示游客登录的点击事件里面判断
- (void)favoredBtnTapped:(UIButton *)sender {// 如果是游客账户,就提示用户进行登录操作,否则就进行正常的收藏按钮点击事件if ([self.systemAccountManager isGuest]) {[self.systemVCManager transferControlToPortalViewWithTitle:@"登录后可进行收藏操作" block:^{[weakSelf doFavoredAction];}];} else {[self doFavoredAction];}
}
复制代码
3. 比如要实现上文中提到的 今日头条 样式的登录框,不能用 present 也不能用 modal,因为那样的话上一级的控制器视图就会被移到另外一个 Window 上,不能实现其在原界面添加半透明遮罩的效果,因此采用下列方式
[fatherVC addChildViewController:portalVC];
[fatherVC.view addSubview:portalVC.view];
复制代码

此处更正一下,感谢 CZAnchor 提出的方法,这里是可以通过 present 方式实现的,代码如下:

UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController;
UIViewController *baseVC = rootVC;while (baseVC.presentedViewController) {baseVC = baseVC.presentedViewController;
}if ([[UIDevice currentDevice].systemVersion floatValue] >= 8.0) {portalVC.modalPresentationStyle = UIModalPresentationOverCurrentContext;baseVC.definesPresentationContext = YES;[baseVC presentViewController:portalVC animated:NO completion:^{}];
} else {baseVC.modalPresentationStyle = UIModalPresentationCurrentContext;[baseVC presentViewController:portalVC animated:NO completion:^{}];
}
复制代码
4. 在调用登录接口的成功回调里面,需要进行两个操作
4.1 首先进行数据迁移:
  • 已下载内容文件 的迁移,由于某些下载内容是需要相应权限的,因此都是每个账号对应一个存储路径,也是在一定程度上防止账号过分共享造成的利益损失
/** 迁移已下载的文件 */#warning 关于游客状态下下载的内容,需要考虑两部分:1. 登录的正式用户之前未在本机上登录过,创建用户的下载路径后直接将游客的下载内容全部迁移过去(若只是登录过没有下载内容,就直接全部迁移过去);2. 登录的正式有用户之前在本机登录过并有下载内容,则需要将两个路径下的下载内容合并- (void)transferDownLoadedFile {    // 获取下载文件根路径    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);    NSString *libraryDir = [paths objectAtIndex:0];    NSString *rootFilePath = [NSString stringWithFormat:@"%@/%@",libraryDir,@"## 这里是项目中下载文件的路径 ##"];

    // 分别获取游客和正式用户的下载路径(方便起见直接使用对应ID作为路径名称)    NSString *guestPath = [NSString stringWithFormat:@"%@/%@", rootFilePath, self.accountManager.guestId];    NSString *userPath = [NSString stringWithFormat:@"%@/%@", rootFilePath, self.accountManager.userId];

    // 获取文件管理器    NSFileManager *manager = [NSFileManager defaultManager];

    // 获取游客的下载文件数组     NSError *error = nil;    NSArray *guestFilesArr = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:guestPath error:&error];    if (error) {        NSLog(@"contentsOfDirectoryAtPath guestPath:%@", error);    }

    // 遍历游客的文件    for (NSString *fileName in guestFilesArr) {        //  拼接处 该文件在 游客状态 && 正式用户状态 的存储路径        NSString *guestFileDir = [guestPath stringByAppendingPathComponent:fileName];        NSString *userFileDir = [userPath stringByAppendingPathComponent:fileName];        // 如果正式用户 下载文件中不包含该文件,就创建一下        if (![manager fileExistsAtPath:userFileDir]) {            [manager createDirectoryAtPath:userFileDir withIntermediateDirectories:YES attributes:nil error:&error];        }

        BOOL isDir;        if ([manager fileExistsAtPath:guestFileDir isDirectory:&isDir] && isDir) {            error = nil;            NSArray *childFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:guestFileDir error:&error];            if (error) {                NSLog(@"contentsOfDirectoryAtPath dir:%@", error);            }            // 遍历该文件夹内子文件,全部迁移到 正式用户 名下的文件            for (NSString *childFile in childFiles) {                NSString *filePath = [guestFileDir stringByAppendingPathComponent:childFile];                NSString *destPath = [userFileDir stringByAppendingPathComponent:childFile];                error = nil;                [manager moveItemAtPath:filePath toPath:userFileDir error:&error];                if (error) {                    DDLogError(@"moveItemAtPath to path error:%@", error);                    //如果正式用户下该文件存在(即用户之前在本机登录并下载过该文件)会报错,那么就将游客路径下的改文件删除                    [manager removeItemAtPath:filePath error:&error];                }            }        }    }}复制代码
  • 迁移数据库:这部分内容着实跟项目本分的业务、封装关系太大,在这里以一个 video 文件的下载记录为例,以 FMDB 为载体大概讲一下思路
// 1. 获取游客的 db 文件路径 guestDataBasePath
// 2. 打开游客该 db 文件
fmDataQueue = [FMDatabaseQueue databaseQueueWithPath:path];[fmDataQueue inDatabase:^(FMDatabase *fmDatabase) {if ([fmDatabase open]) {[fmDatabase setShouldCacheStatements:YES];//  创建 SQL 语句NSString *sqlStr = [NSString stringWithFormat:@"%@%@%@%@%@%@%@",@"CREATE TABLE IF NOT EXISTS MYVIDEO  (VIDEOID TEXT  PRIMARY KEY ",@",videoname      TEXT",@",info           TEXT",@",coverfilename  TEXT",@",urlpath        TEXT")"];BOOL isExecute = [fmDatabase executeUpdate:createStatement];if (isExecute) {// 如有必要,可检查一下表结构是否已升级,此处不再赘述} else {NSLog(@"error occured while creating MYVIDEO table");}} else {NSLog(@"open datebase failed");}
}// 3. 查询游客账户下已下载的 video
//  创建空数组用于存放 video 对象
NSMutableArray *videoArray = [[NSMutableArray alloc] init];
[fmDataQueue inDatabase:^(FMDatabase *fmDatabase) {// 书写 sql 语句NSString *query = [NSString stringWithFormat:@"SELECT videoid,videoname,info,coverfilename,urlpath, FROM MYVIDEO "];NSString *sqlQuery;if (wheresql != nil) {sqlQuery = [NSString stringWithFormat:@"%@%@", query, wheresql];} else {sqlQuery = query;}// 按时间降序排序sqlQuery = [sqlQuery stringByAppendingString:@" ORDER BY time DESC "];FMResultSet *resultSet = [fmDatabase executeQuery:sqlQuery];if ([fmDatabase hadError]) {NSLog(@"FMDB Error %d: %@", [fmDatabase lastErrorCode], [fmDatabase lastErrorMessage]);}// 取出查询的结果集while ([resultSet next]) {VideoClass *video = [[VideoClass alloc] init];video.videoId               = [resultSet stringForColumn:@"videoid"];video.videoTitle            = [resultSet stringForColumn:@"songname"];video.videoDescription      = [resultSet stringForColumn:@"info"];video.coverFileName          = [resultSet stringForColumn:@"coverfilename"];video.path                   = [resultSet stringForColumn:@"urlpath"];[videoArray addObject:video];}[resultSet close];
}];// 4. 关闭游客 db
[fmDataQueue inDatabase:^(FMDatabase* fmDatabase) {if ([fmDatabase close]) {NSLog(@"close MYVIDEO succes ....");}else {NSLog(@"close MYVIDEO error");}
}];
[fmDataQueue close];
fmDataQueue = nil;// 5. 打开 正式用户 下的 db 文件(获取游客 db 路径后,代码同上打开 游客 db)// 6. 将 游客 下载的video 数据插入到 正式用户的 db 中
[fmDataQueue inTransaction:^(FMDatabase *db, BOOL *rollback) {[array enumerateObjectsUsingBlock:^(VideoClass  *video, NSUInteger idx, BOOL * _Nonnull stop) {[self insertOrUpdateCourse:video withDB:db];// 创建插入数据的 sql 语句NSString *insertSql = @"INSERT OR REPLACE INTO MYVIDEO (videoid,videoname,info,coverfilename,urlpath,) VALUES(?,?,?,?,?)";BOOL result = [fmDatabase executeUpdate:insertSql,video.videoId,video.videoTitle,video.videoDescription,video.coverFileName,video.urlPath];if (!result) {NSLog(@"操蛋!插入 MYVIDEO data failed");} else {NSLog(@"牛逼!Insert MYVIDEO data success, U did it!");}}];
}];// 7. 合并数据库成功后,根据游客 db 路径,删除 游客 db 文件
NSFileManager *fm = [NSFileManager defaultManager];
BOOL success = [fm removeItemAtPath:fullPath error:&error];
if (error) {NSLog(@"怎么会删除失败了,难道我姿势不对?delete file at path error:%@", error);
}复制代码
4.2 然后进行隐藏登录界面,并调用一下之前传进来的 block,继续用户之前的操作
- (void)hidePortalView {if (self.loginSucessBlock) {self.loginSucessBlock();}UIView animateWithDuration:0.2 animations:^{self.portalVC.view.alpha = 0;} completion:^(BOOL finished) {[self.portalVC.view removeFromSuperview];[self.portalVC removeFromParentViewController];}
}
复制代码
5. 进行横竖屏适配
  • 由于带有半透明背景的遮罩的视图是以addChildViewController方式实现,因此自动适应父控制器的横竖屏,这里主要讲一下再次点击其他登录方式 进行账号密码输入的传统登录注册页 的横竖屏适配
- (void)signInWithAccountBtnTapped:(UIButton *)sender {SignInController *signInVC = [[SignInController alloc] initWithType:InputViewLogin];// 设置控制器的 modal 方式为遵循当前控制器的环境,实现当前是横(竖)屏就以横(竖)屏方式modalsignInVC.modalPresentationStyle = UIModalPresentationCurrentContext;[self presentViewController:signInVC animated:YES completion:nil];
}
复制代码
  • 当然,在 SignInController 内部也要进行一些 UI 层级适配,在其 viewWillAppear 方法内部实现以下方法
// 根据状态栏方向得到当前页面横竖屏信息
UIDeviceOrientation deviceOrientation = (UIDeviceOrientation)[UIApplication sharedApplication].statusBarOrientation;
// 根据横竖屏状态,做出相应的 UI 层级调整,并做出相应标记
if (deviceOrientation == UIDeviceOrientationPortrait ||deviceOrientation ==UIDeviceOrientationPortraitUpsideDown) {[self doPortraitUIAdjustment];self.isLandScape = NO;
} else {[self doLandScapeUIAdjustment];self.isLandScape = YES;
}
复制代码
  • 然鹅,跑一下代码发现,虽然横竖屏的展示没错了,可是点击输入框后,键盘还是以竖屏的方式进行展现,因为我们只是把 SignInController 的 modal 方式和 UI 适配做了,此时控制器本身并不知道自己是横屏还是竖屏,因此要重写下面三个控制器方法
// 在横屏状态下,应该可以随设备重力感应进行 LandscapeRight 和 LandscapeLeft 两个方向的自动翻转
- (BOOL)shouldAutorotate {if (self.isLandScape) {return YES;} else {return NO;}
}// 如果是横屏状态,应该支持 LandscapeRight 和 LandscapeLeft 两个方向,竖屏状态下只支持 Portrait
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {if (self.isLandScape) {return UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight;} else {return UIInterfaceOrientationMaskPortrait;}
}// 默认的方向
-(UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {if (self.isLandScape) {return UIInterfaceOrientationLandscapeRight;;} else {return UIInterfaceOrientationPortrait;}
}#warning 至此,横竖屏适配算是大功告成了
复制代码

大概的思路就是这些,由于跟项目相关性比较大,而且代码实现方式也比较简单,因此木有 demo,如果有其他问题欢迎在留言区进行交流

iOS程序员眼中的客户端免登陆(数据迁移已更新)相关推荐

  1. iOS程序员眼中的首次使用产品体验

    2017.11.23 一. 前言 首先想说一下为什么写这篇文章: <启示录>这本书曾提到:如果开发的产品没有市场价值,那么无论开发团队多么优秀也无济于事.那么同样的,在我们程序员费尽周折抓 ...

  2. iOS 程序员眼中的 Emoji

    作者丨Hsusue 来源: https://juejin.im/post/5dc3b9a46fb9a04a95289a84 Emoji 简介 绘文字(日语:絵文字/えもじ emoji)是日本在无线通信 ...

  3. 观点:再见Objective C?程序员眼中的Swift

    对于苹果开发者来说,如今已经进入了"Swift时代".虽然编程语言Objective C备受喜爱,不过它作为苹果主流编程语言的日子已经所剩无几.随着WWDC开发者大会的落幕,Swi ...

  4. 程序员眼中最牛的UI设计师是怎样的?

    UI设计师是唯一要和程序员合作的设计师职业,我们看多了相互取笑的段子,那么怎样的UI设计师会是程序员眼中最牛的呢?这其中有3个层次. 1.懂UI 没错,首先是懂UI.UI设计师设计的是人机交互界面,界 ...

  5. 一个6年iOS程序员的工作感悟,送给还在迷茫的你

    前言 每一个开发者,都有一段不愿提起的经历,很多年前,刚刚从大学毕业的时候,很多公司来校招.其中最烂俗的一个面试问题是:"你希望你之后三到五年的发展是什么?".我当时的标准回答是( ...

  6. 程序员眼中的电脑和空调 | 每日趣闻

    戳一戳小程序查看更多! 往 期 趣 闻 ☞栈和队列的区别 | 每日趣闻 ☞程序员毕业两年,三年工作经验是怎么来的?| 每日趣闻 ☞安卓程序员永远不懂iOS程序员的痛?| 每日趣闻 ☞是你写程序时的样子 ...

  7. 【程序员眼中的统计学(12)】相关与回归:我的线条如何? (转)

    阅读目录 目录 1 算法的基本描述 2 算法的应用场景. 3算法的优点和缺点 4 算法的输入数据.中间结果以及输出结果 5 算法的代码参考 6 共享 相关与回归:我的线条如何? 作者 白宁超 2015 ...

  8. java中类图概念,程序员眼中的UML(4)--类图释疑之一,Attribute和Property之区别

    程序员眼中的UML(4) --类图释疑之一,Attribute和Property之区别 上一篇中提出了很多问题,其中最令人费解的可能就是Attribute和Property之区别了吧.我在网络上寻找良 ...

  9. GPU Saturday技术沙龙:OpenCL程序员眼中的下一代APU架构

    摘要:GPU Saturday技术沙龙在北京·3WCoffee成功举办.本次活动邀请AMD资深技术人员及清华大学项目研究员就AMD最新的GCN架构.GPU加速计算在挖掘比特币.典型图像算法.深度神经网 ...

最新文章

  1. TextView实现跑马灯效果
  2. 微软年度研究大盘点:ML突破将到来,人机交互更真实,惜别沈向洋
  3. 黑白青春-纪念那年我的秋天
  4. matlab 两列数据相乘,在EXCEL中,两列完全相同的数据,求和结果不一样??单元格两列相乘的公式...
  5. 三、css 和 js 的装载与执行
  6. #102030:在30天内运行20 10K,庆祝Java 20年
  7. Adjacent Bit Counts(01组合数)
  8. CSS3中很容易混淆的transform,translate,transition。如何去区分,以及综合写法。
  9. Ajax异步刷新,测试用户名是否被注册
  10. vc2013使用经验
  11. 方舟综合指令代码大全系统综合
  12. CSS 零基础到实战(05)布局、盒子模型、弹性盒子【前端就业课 第二阶段】
  13. MySQL(5)条件查询 | 单行函数 | 事务详解
  14. “差生”韩寒难以改变的人生戏码
  15. 使用 ChatGPT 将您的 Excel 工作效率提高 10 倍,您不再需要成为 Excel 向导才能变得超级高效。
  16. 使用 Matlab 解决数学建模问题
  17. 罗杨美慧 20190919-6 四则运算试题生成,结对
  18. IPFS云服务器预售登录系统,ipfs 云服务器
  19. 第四章 账号权限管理
  20. 2015百度面试题--对10亿个32位整数去重和排序

热门文章

  1. matlab版本的cnn代码,Deep Learning学习 之 CNN代码解析(MATLAB)
  2. MultiByteToWideChar和WideCharToMultiByte用法详解
  3. Python之路--Django--form组件与model form组件
  4. abstract不能和哪些关键字共存 学习
  5. java非递归方式实现快速排序
  6. mysql之 mysql 5.6不停机主从搭建(一主一从基于GTID复制)
  7. Exchange2010 初始化失败
  8. 统计学习方法|逻辑斯蒂原理剖析及实现
  9. python一个类调用另一个类的方法_python 类静态方法实例化另一个类对象的问题?...
  10. ASP.NET MVC WebAPI实现文件批量上传