我已经以各种形式看到了这个问题,但没有明确的答案。我要在这里提问和回答。我的应用程序需要在启动时工作......初始化、一些网络调用、登录等。我不希望我的主视图控制器在完成之前工作。这样做的好模式是什么?
要求:
- iOS5+,故事板,ARC。
- 在启动工作完成之前,主 vc 的视图无法出现。
- 想要在启动工作完成时选择到主 vc 的过渡样式。
- 想在故事板中做尽可能多的布局。
- 代码应该干净地包含在明显的地方。
我已经以各种形式看到了这个问题,但没有明确的答案。我要在这里提问和回答。我的应用程序需要在启动时工作......初始化、一些网络调用、登录等。我不希望我的主视图控制器在完成之前工作。这样做的好模式是什么?
要求:
编辑,2017 年 7 月 自第一次写作以来,我已经将我的做法改为将启动任务交给他们自己的视图控制器。在那个 VC 中,我检查启动条件,在需要时显示“忙碌”的 UI,等等。根据启动时的状态,我设置窗口的根 VC。
除了解决 OP 问题之外,这种方法还有其他好处,可以更好地控制启动 UI 和 UI 转换。这是如何做到的:
在您的主故事板中,添加一个新的 VC,称为LaunchViewController
并使其成为应用程序的初始 vc。为您的应用程序的“真实”初始 vc 提供一个标识符,如“AppUI”(标识符位于 IB 的“身份”选项卡上)。
识别作为主要 UI 流程开始的其他 vcs(例如注册/登录、教程等),并提供这些描述性标识符。(有些人更喜欢将每个流程保留在自己的故事板中。这是一个很好的做法,IMO)。
另一个不错的可选想法:也为您的应用程序的启动故事板的 vc 提供一个标识符(如“LaunchVC”),以便您可以在启动期间抓住它并使用它的视图。这将为用户在启动期间和执行启动任务时提供无缝体验。
这是我的LaunchViewController
样子......
@implementation LaunchViewController
- (void)viewDidLoad {
[super viewDidLoad];
// optional, but I really like this:
// cover my view with my launch screen's view for a seamless start
UIStoryboard *storyboard = [self.class storyboardWithKey:@"UILaunchStoryboardName"];
UINavigationController *vc = [storyboard instantiateViewControllerWithIdentifier:@"LaunchVC"];
[self.view addSubview:vc.view];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self hideBusyUI];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self showBusyUI];
// start your startup logic here:
// let's say you need to do a network transaction...
//
[someNetworkCallingObject doSomeNetworkCallCompletion:^(id result, NSError *error) {
if (/* some condition */) [self.class presentUI:@"AppUI"];
else if (/* some condition */) [self.class presentUI:@"LoginUI"];
// etc.
}];
}
#pragma mark - Busy UI
// optional, but maybe you want a spinner or something while getting started
- (void)showBusyUI {
// in one app, I add a spinner on my launch storyboard vc
// give it a tag, and give the logo image a tag, too
// here in animation, I fade out the logo and fade in a spinner
UIImageView *logo = (UIImageView *)[self.view viewWithTag:32];
UIActivityIndicatorView *aiv = (UIActivityIndicatorView *)[self.view viewWithTag:33];
[UIView animateWithDuration:0.5 animations:^{
logo.alpha = 0.0;
aiv.alpha = 1.0;
}];
}
- (void)hideBusyUI {
// an animation that reverses the showBusyUI
}
#pragma mark - Present UI
+ (void)presentUI:(NSString *)identifier {
UIStoryboard *storyboard = [self storyboardWithKey:@"UIMainStoryboardFile"];
UINavigationController *vc = [storyboard instantiateViewControllerWithIdentifier:identifier];
UIWindow *window = [UIApplication sharedApplication].delegate.window;
window.rootViewController = vc;
// another bonus of this approach: any VC transition you like to
// any of the app's main flows
[UIView transitionWithView:window
duration:0.3
options:UIViewAnimationOptionTransitionCrossDissolve
animations:nil
completion:nil];
}
+ (UIStoryboard *)storyboardWithKey:(NSString *)key {
NSBundle *bundle = [NSBundle mainBundle];
NSString *storyboardName = [bundle objectForInfoDictionaryKey:key];
return [UIStoryboard storyboardWithName:storyboardName bundle:bundle];
}
@end
下面的原始答案,虽然我更喜欢我目前的方法
让我们用一个布尔值来表示应用程序准备好运行主 vc,例如:
BOOL readyToRun = startupWorkIsDone && userIsLoggedIn;
在 AppStartupViewController.m 中,当 readyToRun 条件满足时,它可以自行关闭:
self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; // your choice here from UIModalTransitionStyle
[self dismissViewControllerAnimated:YES completion:nil];
现在,只要应用程序处于活动状态,它就可以检查是否准备好运行,并在需要时显示 AppStartupViewController。在 AppDelegate.h
- (void)applicationDidBecomeActive:(UIApplication *)application {
BOOL readyToRun = startupWorkIsDone && userIsLoggedIn;
if (!readyToRun) {
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];
AppStartupViewController *startupVC = [storyboard instantiateViewControllerWithIdentifier:@"AppStartupViewController"];
[self.window.rootViewController presentViewController:startupVC animated:NO completion:nil];
// animate = NO because we don't want to see the mainVC's view
}
}
这主要是答案,但有一个障碍。不幸的是,在 AppStartupViewController 出现之前,主 vc 被加载(没关系)并获得 viewWillAppear: 消息(不好)。这意味着我们必须在 MainViewController.m 中传播一些额外的启动逻辑,如下所示:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (readyToRun) {
// the view will appear stuff i would have done unconditionally before
}
}
我希望这是有帮助的。
使用导航控制器时的另一种解决方案。
您的导航控制器是初始视图控制器,将您的主控制器设置为导航控制器根的视图。
将您的加载控制器添加到情节提要中,并与命名的 segue of style 模态链接。
在您的主控制器的 viewWillAppear 中触发 segue(每个应用程序运行一次)。
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if(_isFirstRun)
{
_isFirstRun = NO;
[self performSegueWithIdentifier:@"segueLoading" sender:nil];
}
}
如果您在 viewDidLoad 中触发 segue,它将不起作用,可能是因为导航控制器的动画尚未完成,您将收到一个unbalanced calls to begin/end appearance transitions