iPhone 6+ 主分割视图中带有标签栏的状态恢复

iPhone 6+ state restoration with tab bar in master split view

我在 iPhone 6+ 中支持状态恢复时遇到问题。

这是我的层次结构:

问题是当状态恢复发生在纵向时,然后,稍后,状态解码尝试在横向时进行。

说明: 在纵向模式下,只有 TabBar 作为主视图(实际上,不存在详细视图),因此将其推入 TabBar 导航的控制器。

然后,稍后,当应用程序尝试执行横向状态恢复时,我的详细信息视图被推送到主导航控制器(当它应该是详细信息时)。

由于自定义层次结构,我相应地实现了 UISplitViewControllerDelegate 方法,它们工作正常。 UISplitViewControllerDelegate 方法还确保状态恢复在以下情况下有效:

Landscape -> Landscape
Landscape -> Portrait
 Portrait -> Portrait

不起作用的是:肖像 -> 风景,因为正如我所说,在未折叠状态下不会调用委托方法,因此视图层次结构不知道如何从主视图中拆分细节并将其嵌入进入详细导航控制器。

不确定我是否完全理解您的问题,因为 iPhone 6+ 应该从折叠状态变为拆分状态,因此应该调用此委托:

- (UIViewController *)splitViewController:(UISplitViewController *)splitViewController separateSecondaryViewControllerFromPrimaryViewController:(UIViewController *)primaryViewController

但是你似乎暗示这没有被调用并且在肖像中它处于非折叠状态?

在我使用的拆分视图控制器上,我发现最好让控制器自行解决问题。如果您对详细视图使用 showDetail segues,它应该会为您处理拆分。您确定使用 showDetail segues 从您的 Controller1->4 推送 DetailController 而不是显示 segues 吗?

因此假设您对细节使用了正确的 segues,只是 return 委托中的 nil 让拆分视图控制器自行解决?

到目前为止,我是这样解决的:在 viewWillAppear 中,所有细节控制器都是从标签栏的导航控制器中删除的。 存储状态时,设备方向被保存并且 SplitViewController.viewControllers(对我来说它不会像在 iOS7 上那样自动存储它们)。

- (void) encodeRestorableStateWithCoder:(NSCoder*)coder
{
    [super encodeRestorableStateWithCoder:coder];
    [coder encodeObject:self.viewControllers forKey:@"viewControllers"];
    [coder encodeInteger:[UIApplication sharedApplication].statusBarOrientation forKey:@"orientation"];
}

- (void) decodeRestorableStateWithCoder:(NSCoder*)coder
{
    [super decodeRestorableStateWithCoder:coder];
    NSArray* viewControllers = [coder decodeObjectForKey:@"viewControllers"];
    if (viewControllers.count > 0)
    {
        self.viewControllers = viewControllers;
    }
    restoredOrientation = (UIInterfaceOrientation) [coder decodeIntegerForKey:@"orientation"];
}

这里是 viewWillAppear 的实现:

- (void) viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    // iPhone6+: if state restoration happened while in portrait orientation and app is launched while in landscape,
    // then all detail views should be cut from master view and split details view is set appropriately
    if (firstLoad &&
            [UIScreen mainScreen].scale > 2.9 && // assure it's iPhone 6+
            UIInterfaceOrientationIsPortrait(restoredOrientation) &&
            UIInterfaceOrientationIsLandscape(UIApplication.sharedApplication.statusBarOrientation))
    {
        UITabBarController* tbc = self.viewControllers[0];
        NSArray* detachedControllers = [tbc cutControllersFrom:DocumentViewController.class];
        if (detachedControllers.count > 0)
        {
            UINavigationController* documentNavigation = [self.storyboard
                    instantiateViewControllerWithIdentifier:@"NavigationController"];
            documentNavigation.viewControllers = detachedControllers;
            self.viewControllers = @[ self.viewControllers[0], documentNavigation ];
        }
        else // place some default no-selection controller in detail
        {
            UINavigationController* noSelectionNavigation = [self.storyboard
                    instantiateViewControllerWithIdentifier:@"NoSelectionSID"];
            self.viewControllers = @[ self.viewControllers.firstObject, noSelectionNavigation ];
        }
    }
    firstLoad = NO;
}

其中 cutControllersFrom 方法是 UITabBarController 上的一个类别:

- (NSArray*) cutControllersFrom:(Class)controllerClass
{
    NSArray* ret;
    for (UIViewController* vc in self.viewControllers)
    {
        if (![vc isKindOfClass:UINavigationController.class])
        {
            continue;
        }

        UINavigationController* nc = (UINavigationController*) vc;
        NSArray* removed = [nc cutFrom:controllerClass];
        if (vc == self.selectedViewController)
        {
            ret = removed;
        }
    }

    return ret;
}

它调用 cutFrom: 方法,它是 UINavigationController 上的一个类别:

- (NSArray*) cutFrom:(Class)controllerClass
{
    NSMutableArray* toRemove = [NSMutableArray array];
    BOOL startRemoving = NO;
    UIViewController* endingViewController;

    for (NSUInteger i = 0; i < self.viewControllers.count; i++)
    {
        UIViewController* vc = self.viewControllers[i];
        if ([vc isKindOfClass:controllerClass])
        {
            startRemoving = YES;
            endingViewController = self.viewControllers[i - 1];
        }

        if (startRemoving)
        {
            [toRemove addObject:vc];
        }
    }
    if (endingViewController)
    {
        [self popToViewController:endingViewController animated:NO];
    }

    return toRemove;
}

当状态保留为纵向时,恢复标识符路径将不同于预期的横向,这是使用拆分视图控制器时的默认初始状态。即细节在主要而不是次要。因此,当应用程序重新启动时,它将无法重新创建以前的层次结构,并将求助于在它们的单独配置中重新创建这些控制器(About the UI Restoration Process 中的第 4 步),这意味着 separateSecondaryViewControllerFromPrimaryViewController 不会调用是因为控件已经分离。您可能会注意到在启动时创建了两个细节控制器,第一个可能设置了默认属性的控制器被丢弃了,或者您甚至可能会看到两个细节控制器被推送到导航堆栈上。我不确定您为什么设法让 Portrait->Portrait 正常工作,因为它应该会遇到同样的问题。

要解决这些问题,您可以实现 application:viewControllerWithRestorationIdentifierPath:coder 将使用纵向层次结构调用(在纵向->横向场景中),您可以 return 通过搜索现有的视图控制器在从情节提要中加载的层次结构中。恢复视图控制器后,它将检测到从保留的肖像到当前风景的方向发生了变化,并且 separateSecondaryViewControllerFromPrimaryViewController 将被调用。但是,我还不确定这是正确的方法,因为拆分控制器中有一些状态没有保存,例如 _preservedDetailController (这是拆分视图控制器可以自动分离控制器的方式在之前的崩溃之后)所以如果在方向改变后重新启动,我们有可能只是扔掉状态我还不能 100% 确定。

这是我的测试应用程序的示例:

- (UIViewController *)application:(UIApplication *)application viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder{
    if([identifierComponents.lastObject isEqualToString:@"DetailViewController"]){
        UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
        UINavigationController *secondaryNavigationController = splitViewController.viewControllers.lastObject;;
        DetailViewController *detail = (DetailViewController *)secondaryNavigationController.viewControllers.firstObject;
        NSURL *objectURI = [coder decodeObjectForKey:@"object"];
        if(objectURI){
            NSManagedObjectContext *moc = self.persistentContainer.viewContext;
            NSManagedObjectID *objectID = [moc.persistentStoreCoordinator managedObjectIDForURIRepresentation:objectURI];
            NSManagedObject *object = [moc objectWithID:objectID];
            detail.object = object;
        }
        // attempt to workaround a bug for _preservedDetailController not being restored.
        [splitViewController _willShowCollapsedDetailViewController:secondaryNavigationController inTargetController:nil];
        return detail;
    }
    else if([identifierComponents.lastObject isEqualToString:@"DetailNavigationController"]){
        UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
        UINavigationController *secondaryNavigationController = splitViewController.viewControllers.lastObject;
        return secondaryNavigationController;
    }
    return nil;
}