我看到过各种形式的这个问题,但没有明确的答案。我将在此提问并回答。我的应用程序需要在启动时进行一些工作...初始化、几个网络调用、登录等等。在这些工作完成之前,我不希望我的主视图控制器开始工作。有什么好的模式可以解决这个问题?
要求:
- iOS5+、storyboards、ARC。
- 在启动工作完成之前,主vc的视图不能出现。
- 想要在启动工作完成后选择转换样式到主vc。
- 尽可能多地在storyboard中完成布局。
- 代码应该清晰地包含在一个明显的位置。
我看到过各种形式的这个问题,但没有明确的答案。我将在此提问并回答。我的应用程序需要在启动时进行一些工作...初始化、几个网络调用、登录等等。在这些工作完成之前,我不希望我的主视图控制器开始工作。有什么好的模式可以解决这个问题?
要求:
编辑,2017年7月 自第一次写作以来,我已经改变了我的做法,将启动任务交给它们自己的视图控制器。在那个VC中,我检查启动条件,如果需要,呈现“繁忙”UI等。根据启动时的状态,我设置窗口的根视图控制器。
除了解决OP问题外,这种方法还具有提供更好的启动UI和UI转换控制的额外好处。以下是如何实现:
在您的主storyboard中,添加一个名为LaunchViewController
的新VC,并使其成为应用程序的初始VC。给您的应用程序的“真正的”初始VC分配一个标识符,例如“AppUI”(标识符位于IB的Identity选项卡上)。
识别其他作为主要UI流程起点的VC(例如注册/登录、教程等),并为这些VC也分配描述性标识符。(有些人喜欢将每个流程保留在自己的storyboard中。在我看来,这是一个很好的做法)。
另一个不错的可选想法:也为您的应用程序启动storyboard的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
以下是原始答案,但我更喜欢我的当前方法
让我们使用布尔值来表达应用程序准备好运行主视图控制器的状态,例如:
BOOL readyToRun = startupWorkIsDone && userIsLoggedIn;
在AppStartupViewController.m文件中,当readyToRun条件满足时,可以使其自己dismiss:
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。
在主控制器的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
错误。