我正在使用Storyboard构建一个iOS应用程序。根视图控制器是一个标签栏控制器。我正在创建登录/注销过程,它基本上工作正常,但我有一些问题。我需要知道最好的方法来设置这一切。
我想做到以下几点:
在应用程序第一次启动时显示登录屏幕。当他们登录时,转到标签栏控制器的第一个标签。
任何时候他们启动应用程序之后,检查他们是否登录,并直接跳到根标签栏控制器的第一个标签。
当他们手动单击登出按钮时,显示登录屏幕,并清除视图控制器中的所有数据。
到目前为止,我所做的是将根视图控制器设置为标签栏控制器,并创建了一个自定义segue到Login视图控制器。在我的标签栏控制器类中,我检查他们是否在viewDidAppear方法中登录,并执行segue: [self performSegueWithIdentifier:@"pushLogin" sender:self];
我还设置了一个通知,当注销操作需要执行:[[NSNotificationCenter defaultCenter] addObserver:自我选择器:@选择器(logoutAccount)名称:@“logoutAccount”对象:nil];
注销后,我从Keychain中清除凭据,运行[self setSelectedIndex:0],并执行segue再次显示登录视图控制器。
这一切都很好,但我想知道:这个逻辑应该在AppDelegate中吗?我还有两个问题:
他们第一次启动应用程序时,标签栏控制器在segue执行之前简要显示。我已经尝试移动代码到viewWillAppear,但segue不会工作那么早。
注销时,所有数据仍在所有视图控制器中。如果他们登录到一个新帐户,旧帐户数据仍然显示,直到他们刷新。我需要一种方法来清除这很容易登出。
我愿意重新修改。我考虑过让登录屏幕成为根视图控制器,或者在AppDelegate中创建一个导航控制器来处理所有事情…我只是不确定目前最好的方法是什么。
感谢bhavya的解决方案。关于swift,有两种答案,但都不是很完整。我已经用swift3做过了。下面是主要代码。
在AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// seclect the mainStoryBoard entry by whthere user is login.
let userDefaults = UserDefaults.standard
if let isLogin: Bool = userDefaults.value(forKey:Common.isLoginKey) as! Bool? {
if (!isLogin) {
self.window?.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "LogIn")
}
}else {
self.window?.rootViewController = mainStoryboard.instantiateViewController(withIdentifier: "LogIn")
}
return true
}
在SignUpViewController.swift
@IBAction func userLogin(_ sender: UIButton) {
//handle your login work
UserDefaults.standard.setValue(true, forKey: Common.isLoginKey)
let delegateTemp = UIApplication.shared.delegate
delegateTemp?.window!?.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Main")
}
在logOutAction函数中
@IBAction func logOutAction(_ sender: UIButton) {
UserDefaults.standard.setValue(false, forKey: Common.isLoginKey)
UIApplication.shared.delegate?.window!?.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()
}
我用这个来检查第一次发射:
- (NSInteger) checkForFirstLaunch
{
NSInteger result = 0; //no first launch
// Get current version ("Bundle Version") from the default Info.plist file
NSString *currentVersion = (NSString*)[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
NSArray *prevStartupVersions = [[NSUserDefaults standardUserDefaults] arrayForKey:@"prevStartupVersions"];
if (prevStartupVersions == nil)
{
// Starting up for first time with NO pre-existing installs (e.g., fresh
// install of some version)
[[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithObject:currentVersion] forKey:@"prevStartupVersions"];
result = 1; //first launch of the app
} else {
if (![prevStartupVersions containsObject:currentVersion])
{
// Starting up for first time with this version of the app. This
// means a different version of the app was alread installed once
// and started.
NSMutableArray *updatedPrevStartVersions = [NSMutableArray arrayWithArray:prevStartupVersions];
[updatedPrevStartVersions addObject:currentVersion];
[[NSUserDefaults standardUserDefaults] setObject:updatedPrevStartVersions forKey:@"prevStartupVersions"];
result = 2; //first launch of this version of the app
}
}
// Save changes to disk
[[NSUserDefaults standardUserDefaults] synchronize];
return result;
}
(如果用户删除应用程序并重新安装,则算作第一次启动)
在AppDelegate中,我检查了第一次启动,并创建了一个带有登录屏幕(登录和注册)的导航控制器,我把它放在当前主窗口的顶部:
[self.window makeKeyAndVisible];
if (firstLaunch == 1) {
UINavigationController *_login = [[UINavigationController alloc] initWithRootViewController:loginController];
[self.window.rootViewController presentViewController:_login animated:NO completion:nil];
}
因为它在常规视图控制器的顶部它独立于应用的其他部分如果你不再需要它,你可以解散视图控制器。如果用户手动按下按钮,您也可以以这种方式显示视图。
顺便说一句:我保存用户的登录数据是这样的:
KeychainItemWrapper *keychainItem = [[KeychainItemWrapper alloc] initWithIdentifier:@"com.youridentifier" accessGroup:nil];
[keychainItem setObject:password forKey:(__bridge id)(kSecValueData)];
[keychainItem setObject:email forKey:(__bridge id)(kSecAttrAccount)];
对于注销:我从CoreData(太慢)切换到现在使用nsarray和nsdictionary来管理我的数据。注销仅仅意味着清空这些数组和字典。另外,我确保在viewWillAppear中设置我的数据。
就是这样。
以下是我的霉霉解决方案,以飨未来的围观群众。
1)创建一个协议来处理登录和注销功能:
protocol LoginFlowHandler {
func handleLogin(withWindow window: UIWindow?)
func handleLogout(withWindow window: UIWindow?)
}
2)扩展上述协议,并提供此处注销的功能:
extension LoginFlowHandler {
func handleLogin(withWindow window: UIWindow?) {
if let _ = AppState.shared.currentUserId {
//User has logged in before, cache and continue
self.showMainApp(withWindow: window)
} else {
//No user information, show login flow
self.showLogin(withWindow: window)
}
}
func handleLogout(withWindow window: UIWindow?) {
AppState.shared.signOut()
showLogin(withWindow: window)
}
func showLogin(withWindow window: UIWindow?) {
window?.subviews.forEach { $0.removeFromSuperview() }
window?.rootViewController = nil
window?.rootViewController = R.storyboard.login.instantiateInitialViewController()
window?.makeKeyAndVisible()
}
func showMainApp(withWindow window: UIWindow?) {
window?.rootViewController = nil
window?.rootViewController = R.storyboard.mainTabBar.instantiateInitialViewController()
window?.makeKeyAndVisible()
}
}
3)然后我可以使我的AppDelegate符合LoginFlowHandler协议,并在启动时调用handleLogin:
class AppDelegate: UIResponder, UIApplicationDelegate, LoginFlowHandler {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow.init(frame: UIScreen.main.bounds)
initialiseServices()
handleLogin(withWindow: window)
return true
}
}
从这里,我的协议扩展将处理逻辑或确定用户是否登录/退出,然后相应地更改windows rootViewController !
在这里输入图像描述
在App Delegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[[UIBarButtonItem appearance] setBackButtonTitlePositionAdjustment:UIOffsetMake(0, -60)
forBarMetrics:UIBarMetricsDefault];
NSString *identifier;
BOOL isSaved = [[NSUserDefaults standardUserDefaults] boolForKey:@"loginSaved"];
if (isSaved)
{
//identifier=@"homeViewControllerId";
UIWindow* mainWindow=[[[UIApplication sharedApplication] delegate] window];
UITabBarController *tabBarVC =
[[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateViewControllerWithIdentifier:@"TabBarVC"];
mainWindow.rootViewController=tabBarVC;
}
else
{
identifier=@"loginViewControllerId";
UIStoryboard * storyboardobj=[UIStoryboard storyboardWithName:@"Main" bundle:nil];
UIViewController *screen = [storyboardobj instantiateViewControllerWithIdentifier:identifier];
UINavigationController *navigationController=[[UINavigationController alloc] initWithRootViewController:screen];
self.window.rootViewController = navigationController;
[self.window makeKeyAndVisible];
}
return YES;
}
视图controller.m
视图中didload
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
UIBarButtonItem* barButton = [[UIBarButtonItem alloc] initWithTitle:@"Logout" style:UIBarButtonItemStyleDone target:self action:@selector(logoutButtonClicked:)];
[self.navigationItem setLeftBarButtonItem:barButton];
}
在登出按钮动作
-(void)logoutButtonClicked:(id)sender{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Alert" message:@"Do you want to logout?" preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"Logout" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:NO forKey:@"loginSaved"];
[[NSUserDefaults standardUserDefaults] synchronize];
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
UIStoryboard * storyboardobj=[UIStoryboard storyboardWithName:@"Main" bundle:nil];
UIViewController *screen = [storyboardobj instantiateViewControllerWithIdentifier:@"loginViewControllerId"];
[appDelegate.window setRootViewController:screen];
}]];
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
[self dismissViewControllerAnimated:YES completion:nil];
}]];
dispatch_async(dispatch_get_main_queue(), ^ {
[self presentViewController:alertController animated:YES completion:nil];
});}
我不喜欢bhavya的答案,因为在视图控制器内使用AppDelegate和设置rootViewController没有动画。Trevor的回答是关于iOS8上闪烁视图控制器的问题。
UPD 07/18/2015
视图控制器内部的AppDelegate:
在视图控制器内更改AppDelegate状态(属性)会破坏封装。
每个iOS项目中非常简单的对象层次结构:
AppDelegate(拥有窗口和rootViewController)
ViewController(拥有视图)
顶部的对象可以改变底部的对象,因为它们正在创建这些对象。但是如果底部的对象改变它们上面的对象是不行的(我描述了一些基本的编程/OOP原则:DIP(依赖倒置原则:高级模块不能依赖于低级模块,但它们应该依赖于抽象))。
如果任何对象将改变这个层次结构中的任何对象,那么代码中迟早会出现混乱。在小项目上可能没问题,但在小项目上挖掘这个混乱是没有乐趣的=]
UPD 07/18/2015
我复制模态控制器动画使用UINavigationController (tl;dr:检查项目)。
我使用UINavigationController在我的应用程序中呈现所有控制器。最初,我在导航堆栈中显示登录视图控制器与普通的推送/弹出动画。然后我决定用最小的变化把它改成模态。
工作原理:
Initial view controller (or self.window.rootViewController) is UINavigationController with ProgressViewController as a rootViewController. I'm showing ProgressViewController because DataModel can take some time to initialize because it inits core data stack like in this article (I really like this approach).
AppDelegate is responsible for getting login status updates.
DataModel handles user login/logout and AppDelegate is observing it's userLoggedIn property via KVO. Arguably not the best method to do this but it works for me. (Why KVO is bad, you can check in this or this article (Why Not Use Notifications? part).
ModalDismissAnimator and ModalPresentAnimator are used to customize default push animation.
动画师的逻辑工作原理:
AppDelegate sets itself as a delegate of self.window.rootViewController (which is UINavigationController).
AppDelegate returns one of animators in -[AppDelegate navigationController:animationControllerForOperation:fromViewController:toViewController:] if necessary.
Animators implement -transitionDuration: and -animateTransition: methods. -[ModalPresentAnimator animateTransition:]:
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
[[transitionContext containerView] addSubview:toViewController.view];
CGRect frame = toViewController.view.frame;
CGRect toFrame = frame;
frame.origin.y = CGRectGetHeight(frame);
toViewController.view.frame = frame;
[UIView animateWithDuration:[self transitionDuration:transitionContext]
animations:^
{
toViewController.view.frame = toFrame;
} completion:^(BOOL finished)
{
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
测试项目在这里。