如何使用故事板呈现启动/登录视图控制器

4

我看到过各种形式的这个问题,但没有明确的答案。我将在此提问并回答。我的应用程序需要在启动时进行一些工作...初始化、几个网络调用、登录等等。在这些工作完成之前,我不希望我的主视图控制器开始工作。有什么好的模式可以解决这个问题?

要求:

  • iOS5+、storyboards、ARC。
  • 在启动工作完成之前,主vc的视图不能出现。
  • 想要在启动工作完成后选择转换样式到主vc。
  • 尽可能多地在storyboard中完成布局。
  • 代码应该清晰地包含在一个明显的位置。
2个回答

13

编辑,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;
  1. 创建一个AppStartupViewController,并在应用程序的故事板中布置它。
  2. 不要将任何segue拖到它上面,也不要使它成为起始vc,只需将其浮动在某个位置即可。
  3. 在storyboard中的vc属性检查器中,将其标识符设置为“AppStartupViewController”。

在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
    }
}

我希望这对你有所帮助。


如果你不介意的话,我尝试了你提供的解决方案,但我遇到了你提到的问题。您介意解释一下“视图将显示的内容,我之前无条件执行过的内容”的语句是什么意思吗? - chaitanya.varanasi
当然 - 在我提供的解决方案中,您的应用程序的主要视图控制器将会获得两次viewWillAppear:的调用 - 首先在AppStartup视图控制器呈现之前,第二次是在主要视图控制器真正要出现之前。问题在于第一个。我希望我的主要视图控制器在收到任何视图消息之前都能假定一切都准备就绪,但是在我运行启动逻辑之前,第一个(我认为苹果是错误的)viewWillAppear:消息发生了。 (它不会出现,那么为什么要收到那个消息?)继续... - danh
因此,我在那里添加了readyToRun条件。 "the view will appear stuff ..."是您计划在该方法中执行的任何操作,任何只有在应用程序准备就绪时才运行的操作。这很难表达。将考虑如何编辑我的解决方案以获得更多的清晰度。 - danh
谢谢Danh,这帮了很多! - chaitanya.varanasi
@cleverbit - 自从写这篇文章以来,我已经改变了我的方法,完全在主要编辑中描述,并且与您最近的一个答案共享一个关键相似之处--替换根窗口视图。(不确定为什么您的答案被投票否决,但我通过加一来纠正了这一点)。 我的新做法将所有这些工作交给了一个创业公司。 这有几个好处,我在编辑中进行了描述。 - danh
显示剩余3条评论

0

使用导航控制器时的另一种解决方案。

  1. 将你的导航控制器设置为初始视图控制器,将主控制器设置为导航控制器根视图。

  2. 在故事板中添加你的加载控制器,并使用模态样式添加一个命名segue。

  3. 在主控制器的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错误。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接