iOS 处理首次登录和再次登录(游客登录)

很多应用都会有这样一种登录设置:初次登录显示初次登录界面,若用户不登录可选择游客身份进入应用,当点击应用显示界面的某部分时,则弹出再次登录页面提示用户登录,此时用户登录则以用户身份进入应用。明析业务需求后,便可根据业务需求进行业务设计,可以先通过逻辑图理顺业务:


根据逻辑图,我们应当在程序初次进入时,首先验证账号是否存在或合法,根据判断结果进行分支处理。为方便账号判断我们,可以先创建一个账号管理工具类统一处理账号问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@implementation CHAccountHelper

// 读取账号
+ (NSString *)account
{
return [CHUserDefaultsHelper account];
}

// 存储账号
+ (void)saveAccount:(NSString *)account
{
// Check Account

// Save Account
[CHUserDefaultsHelper saveAccount:account];
}

// 移除账号
+ (void)removeAccount
{
[CHUserDefaultsHelper removeAccount];
}

// 判断账号是否存在
+ (BOOL)isAccountExist
{
NSString *account = [CHAccountHelper account];
if (account && account.length > 0)
{
return YES;
}

return NO;
}

@end

这里,通过NSUserDefaults来模拟账号存取。同时,为方便管理及后续维护,我们通常不通过账号管理工具类来直接进行账号数据的操作,而是根据存储方式,创建与之对应的数据存储工具类管理数据层。这样做的目的在于减低耦合性,将上层业务与底层数据进行一定的隔离,当数据层变动时(如存储方式从NSUserDefaults替换为NSKeyedArchiver),只需更改数据层,业务方并不会有太大改动,便于后续维护。再而,为方便数据层的管理,我们通常还会以分类方法的形式为数据存储工具类进行业务扩展:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static NSString * const ACCOUNT_KEY = @"account";

@implementation CHUserDefaultsHelper (configureAccount)

+ (void)saveAccount:(NSString *)account
{
[CHUserDefaultsHelper saveString:account withKey:ACCOUNT_KEY];
}

+ (NSString *)account
{
return [CHUserDefaultsHelper stringWithKey:ACCOUNT_KEY];
}

+ (void)removeAccount
{
[CHUserDefaultsHelper removeObjectForKey:ACCOUNT_KEY];
}

@end

账号管理工具类设计完成后,便可根据逻辑图进一步组织业务代码。先在程序入口处理判断分支,若本地存在账号则以会员进入首页:

1
[[UIApplication sharedApplication].keyWindow setRootViewController:[CHTabBarController new]];

否则,进入登录界面(首次):

1
2
3
CHNavigationController *navigationController = [[CHNavigationController alloc] initWithRootViewController:[CHLoginViewController new]];

[[UIApplication sharedApplication].keyWindow setRootViewController:navigationController];

在登录界面(首次)用户可选择登录以会员进入首页或者选择随便看看以游客进入首页。若选择随便看看,则需根据业务需要,在部分界面进行一些拦截判断,针对会员和游客做不同处理。以下提供两种常见的判断需求处理:
点击按钮,会员显示Alert界面,游客进入登录界面(再次)提示登录:

1
2
3
4
5
6
7
8
9
10
11
// 会员
if ([CHAccountHelper isAccountExist])
{
[[[UIAlertView alloc] initWithTitle:@"You have logged in!" message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil, nil] show];
return;
}

// 呈现登录界面(再次)提示游客登录
CHNavigationController *navigationController = [[CHNavigationController alloc] initWithRootViewController:[CHReLoginViewController new]];

[self.tabBarController presentViewController:navigationController animated:YES completion:completion];

点击TabBar的某个BarItem,会员可以进入,游客则进入登录界面(再次)提示登录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController
{
if (![CHAccountHelper isAccountExist]) // 游客
{
if ([[(CHNavigationController *)viewController topViewController] isKindOfClass:[CHAboutViewController class]])
{
CHNavigationController *navigationController = [[CHNavigationController alloc] initWithRootViewController:[CHReLoginViewController new]];

[tabBarController presentViewController:navigationController animated:YES completion:completion];
return NO;
}
}

return YES;
}

需要注意的是,这里我们针对首次登录和再次登录情况分别提供了两个ViewController进行管理(CHLoginViewControllerCHReLoginViewController)。首次登录和再次登录虽然界面基本一致,但是分属不同的业务————首次登录和再次登录,不应该在一个ViewController进行业务分离(诸如LoginType等入口判断),这样一旦业务变动,判断只会愈加复杂,导致代码难以管理和维护。而将业务划归不同的ViewController管理能极大地避免这种情况,缺点仅在于多了点代码。同时,为了提高代码的复用性,我们还可以对登录界面进行封装处理,共享界面布局,通过代理或Block将点击事件回传,分别供首次登录和再次登录调用,这里以代理为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@protocol CHLoginViewDelegate <NSObject>

@optional

- (void)loginView:(CHLoginView *)loginView didClickLoginButton:(UIButton *)button;
- (void)loginView:(CHLoginView *)loginView didClickLookArroundButton:(UIButton *)button;

@end

// CHLoginViewController调用
- (void)loginView:(CHLoginView *)loginView didClickLookArroundButton:(UIButton *)button
{
[[UIApplication sharedApplication].keyWindow setRootViewController:[CHTabBarController new]];
}

// CHReLoginViewController调用
- (void)loginView:(CHLoginView *)loginView didClickLookArroundButton:(UIButton *)button
{
[self dismissViewControllerAnimated:YES completion:nil];
}

至此需求基本完成。但是观察代码,我们会发现跳转部分有大量重复代码,且跳转逻辑分散在项目的各个角落,不利于代码的管理和维护。对此,我们可以通过将跳转抽象为一类业务,从业务层上对跳转业务进行统一调度和管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@interface CHService : NSObject

// 业务回调处理
typedef void(^CHServiceSuccessHandle)();
typedef void(^CHServiceFailureHandle)();
typedef void(^CHServiceAllCompletionHandle)();

// 跳转业务
// 跳转到登录界面(首次)
+ (void)handleSkipToLoginViewControllerCompletion:(CHServiceAllCompletionHandle)completion
{
CHNavigationController *navigationController = [[CHNavigationController alloc] initWithRootViewController:[CHLoginViewController new]];

[[UIApplication sharedApplication].keyWindow setRootViewController:navigationController];

!completion?:completion();
}

// 跳转到登录界面(再次)
+ (void)handleSkipToReLoginViewControllerCompletion:(CHServiceAllCompletionHandle)completion
{
UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;

if (!rootViewController || ![rootViewController isKindOfClass:[CHTabBarController class]]) return;

CHNavigationController *navigationController = [[CHNavigationController alloc] initWithRootViewController:[CHReLoginViewController new]];

[rootViewController presentViewController:navigationController animated:YES completion:completion];
}

// 跳转到首页
+ (void)handleSkipToHomeViewControllerComletionHandle:(CHServiceAllCompletionHandle)completion
{
[[UIApplication sharedApplication].keyWindow setRootViewController:[CHTabBarController new]];

!completion?:completion();
}

// 跳转到首页(未登录)
+ (void)handleSkipToHomeViewControllerBeforeLoginCompletion:(CHServiceAllCompletionHandle)completion
{
// do something

[CHService handleSkipToHomeViewControllerComletionHandle:completion];
}

// 跳转到首页(已登录)

+ (void)handleSkipToHomeViewControllerAfterLoginCompletion:(CHServiceAllCompletionHandle)completion
{
// do something

[CHService handleSkipToHomeViewControllerComletionHandle:completion];
}

进一步,我们也可以对登录和登出业务做同样处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 模拟登录处理
+ (void)handleLoginWithParameters:(NSString *)parameters successHandle:(CHServiceSuccessHandle)success failureHandle:(CHServiceFailureHandle)failure allCompletionHandle:(CHServiceAllCompletionHandle)completion
{
// Send Login Request
BOOL sendLoginRequest = YES;
// BOOL sendLoginRequest = NO;

// If Login success
if (sendLoginRequest)
{
// Save account
[CHAccountHelper saveAccount:parameters];

!success?:success();

[CHService handleSkipToHomeViewControllerAfterLoginCompletion:completion];
return;
}

// Failure
!failure?:failure();

!completion?:completion();
}

// 模拟登出处理
+ (void)handleLogoutSuccessHandle:(CHServiceSuccessHandle)success failureHandle:(CHServiceFailureHandle)failure allCompletionHandle:(CHServiceAllCompletionHandle)completion
{
// Send Logout Request
BOOL sendLogoutRequest = YES;
// BOOL sendLoginRequest = NO;

// If Logout success
if (sendLogoutRequest)
{
// remove account
[CHAccountHelper removeAccount];

!success?:success();

[CHService handleSkipToLoginViewControllerCompletion:completion];
return;
}

// Failure
!failure?:failure();

!completion?:completion();
}

最后,我们在需要的位置调用即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)loginView:(CHLoginView *)loginView didClickLoginButton:(UIButton *)button
{
[CHService handleLoginWithParameters:@"account" successHandle:^{

} failureHandle:^{

} allCompletionHandle:nil];
}

- (void)loginView:(CHLoginView *)loginView didClickLookArroundButton:(UIButton *)button
{
[CHService handleSkipToHomeViewControllerBeforeLoginCompletion:nil];
}

以上例子除了提供处理首次登录和再次登录的一种方案外,也提供了一般开发中实现业务的思维过程:分析业务,理顺逻辑,组织代码,实现需求和分离优化。对开发者而言,或许后者比前者更为重要。

附,示例代码:https://github.com/ColinHwang/Demo-of-Tutorial-in-Blog/tree/master/iOS-FirstLoginAndReLoginDemo