不使用导航控制器堆栈、子视图或模态控制器,如何实现视图控制器的动画切换?

142

导航控制器有视图控制器堆栈来管理,并且有限的动画过渡。

将一个视图控制器作为子视图添加到现有视图控制器中需要将事件传递给子视图控制器,这很难管理,充满了小烦恼,通常在实现时感觉像是一种糟糕的 hack(苹果也建议不要这样做)。

呈现模态视图控制器会再次将一个视图控制器放在另一个视图控制器之上,虽然它没有上述事件传递问题,但它并没有真正“交换”视图控制器,而是将其堆叠在一起。

故事板仅限于iOS 5,并且几乎是理想的,但不能在所有项目中使用。

可以有人提供一个SOLID CODE EXAMPLE,展示一种无需上述限制并允许它们之间进行动画过渡的更改视图控制器的方法吗?

一个近似的例子,但没有动画: 如何在没有导航控制器的情况下使用多个iOS自定义视图控制器

编辑:使用导航控制器是可以的,但需要有动画过渡样式(不仅仅是滑动效果),展示的视图控制器需要完全地进行交换(而不是堆叠)。如果第二个视图控制器必须从堆栈中移除另一个视图控制器,则它的封装性就不够好。

编辑2:iOS 4应该是这个问题的基本操作系统,当提到故事板时,我应该澄清这一点。


1
你可以使用导航控制器进行自定义动画转换。如果您接受这种方法,请在问题中去除该限制,我会发布一个代码示例。 - Richard Brightwell
@Richard 如果它能避免繁琐的堆栈管理,并且能够适应不同的视图控制器之间的动画过渡样式,那么使用导航控制器是可以的! - TigerCoding
好的,我有些不耐烦,已经发布了代码。试试看吧。对我来说有效。 - Richard Brightwell
@RichardBrightwell,您在此处提到可以使用导航控制器在视图控制器之间进行自定义动画转换...怎么做?您能发一个例子吗?谢谢。 - Duck
6个回答

107

编辑:新的答案适用于任何方向。 原始答案仅在界面为纵向时有效。这是因为用不同的视图替换视图的视图转换动画必须发生在至少低于添加到窗口的第一个视图的级别的视图上(例如window.rootViewController.view.anotherView)。

我实现了一个简单的容器类,我称之为TransitionController。您可以在https://gist.github.com/1394947找到它。

另外,我更喜欢在一个单独的类中实现,因为它更容易重用。如果您不想这样做,您可以直接在应用程序委托中实现相同的逻辑,从而消除了TransitionController类的需要。但是,您需要的逻辑将是相同的。

使用方法如下:

在您的应用程序委托中

// add a property for the TransitionController

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    MyViewController *vc = [[MyViewContoller alloc] init...];
    self.transitionController = [[TransitionController alloc] initWithViewController:vc];
    self.window.rootViewController = self.transitionController;
    [self.window makeKeyAndVisible];
    return YES;
}

从任何视图控制器转换到新的视图控制器:
- (IBAction)flipToView
{
    anotherViewController *vc = [[AnotherViewController alloc] init...];
    MyAppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
    [appDelegate.transitionController transitionToViewController:vc withOptions:UIViewAnimationOptionTransitionFlipFromRight];
}

编辑:原始答案如下 - 仅适用于纵向方向

我为这个例子做出了以下假设:

  1. 您已经将一个视图控制器分配为窗口的rootViewController

  2. 当您切换到新视图时,您希望使用拥有新视图的视图控制器替换当前的视图控制器。任何时候,只有当前的视图控制器是活动的(例如alloc'ed)。

代码可以很容易地修改以实现不同的工作方式,关键点是动画转换和单个视图控制器。确保您不会在分配给window.rootViewController之外的任何地方保留视图控制器。

代码以动画形式在应用程序委托中进行过渡

- (void)transitionToViewController:(UIViewController *)viewController
                    withTransition:(UIViewAnimationOptions)transition
{
    [UIView transitionFromView:self.window.rootViewController.view
                        toView:viewController.view
                      duration:0.65f
                       options:transition
                    completion:^(BOOL finished){
                        self.window.rootViewController = viewController;
                    }];
}

在视图控制器中的使用示例

- (IBAction)flipToNextView
{
    AnotherViewController *anotherVC = [[AnotherVC alloc] init...];
    MyAppDelegate *appDelegate = (MyAppDelegate *)[UIApplication sharedApplication].delegate;
    [appDelegate transitionToViewController:anotherVC
                             withTransition:UIViewAnimationOptionTransitionFlipFromRight];
}

1
非常好!它不仅起到了作用,而且是一个非常简单和清晰的代码示例。非常感谢! - TigerCoding
这会不会给你的布局带来问题?另外,这会触发viewControllers的willAppear和didAppear方法吗? - Felix Lamouroux
1
我知道出现的调用已经被执行了,因为我记录了它们。我也不明白这会影响方向的改变。你能详细说明一下你为什么这样认为吗? - TigerCoding
1
@XJones 的解决方案非常好,在我的 iPad 应用程序中运行得非常好,除了一个问题我正在面临,即在第一次初始化之后 transVC=[TransitionController ... initWithViewController:aUINavCtrlObj]; window.rootVC=transVC;aUINavCtrlObj 中的视图从顶部填充 20px(即所有方向的屏幕(UIWindow)边界下方 20px 都超出了屏幕(UIWindow)范围),但是在我执行 [transVC transitionToViewController:anotherVC] 后,填充就消失了。 我尝试在 TransitionController 的 loadView 中设置 wantsFullScreenLayout=NO,它会在 statusBar 下方添加一个 20px 的黑色区域。 - Abduliam Rehmanius
1
@AbduliamRehmanius:我曾遇到过同样的问题。我通过将TransitionController.m第25行更改为UIView *view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];来解决它,但我只在最新版本的iOS上使用过,所以请仔细测试。 - Phil Calvin
显示剩余16条评论

67
您可以使用苹果的新视图控制器包含系统。要获取更深入的信息,请查看WWDC 2011会议视频“实现UIViewController包含”。
iOS5中引入了UIViewController包含功能,允许您有一个父视图控制器和多个嵌套其中的子视图控制器。这就是UISplitViewController的工作原理。通过这种方式,您可以将视图控制器堆叠在父控制器中,但是对于您特定的应用程序,您只需使用父控制器来管理从一个可见的视图控制器到另一个的过渡。这是苹果认可的做法,并且从一个子视图控制器到另一个的动画非常简单。此外,您还可以使用各种不同的UIViewAnimationOption转换!
此外,使用UIViewContainment时,除非您想要这样做,否则无需担心在方向事件期间管理子视图控制器的混乱。您可以简单地使用以下内容以确保您的父视图控制器将旋转事件转发给子视图控制器。
- (BOOL)automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers{
    return YES;
}

您可以在父视图控制器的viewDidLoad方法中执行以下或类似操作来设置第一个子视图控制器:

[self addChildViewController:self.currentViewController];
[self.view addSubview:self.currentViewController.view];
[self.currentViewController didMoveToParentViewController:self];
[self.currentViewController.swapViewControllerButton setTitle:@"Swap" forState:UIControlStateNormal];

那么当你需要更改子视图控制器时,你在父视图控制器中调用以下类似的内容:

-(void)swapViewControllers:(childViewController *)addChildViewController:aNewViewController{
     [self addChildViewController:aNewViewController];
     __weak __block ViewController *weakSelf=self;
     [self transitionFromViewController:self.currentViewController
                       toViewController:aNewViewController
                               duration:1.0
                                options:UIViewAnimationOptionTransitionCurlUp
                             animations:nil
                             completion:^(BOOL finished) {
                                   [aNewViewController didMoveToParentViewController:weakSelf];

                                   [weakSelf.currentViewController willMoveToParentViewController:nil];
                                   [weakSelf.currentViewController removeFromParentViewController];

                                   weakSelf.currentViewController=[aNewViewController autorelease];
                             }];
 }

我在这里发布了一个完整的示例项目:https://github.com/toolmanGitHub/stackedViewControllers。这个其他项目展示了如何在一些不占据整个屏幕的视图控制器类型上使用UIViewController容器。

祝好运

1
一个很好的例子。感谢您抽出时间发布代码。另一方面,它仅限于iOS 5。如问题中所述:“[Storyboards] 仅限于iOS 5,并且几乎是理想的,但不能在所有项目中使用。”考虑到大约40%的客户仍在使用iOS 4,目标是提供适用于iOS 4及更高版本的东西。 - TigerCoding
在转场之前,难道不应该调用 [self.currentViewController willMoveToParentViewController:nil]; 吗? - Felix Lamouroux
@FelixLam - 根据UIViewController容器的文档,只有在覆盖addChildViewController方法时才需要调用willMoveToParentViewController。在这个例子中,我正在调用它但没有覆盖该方法。 - timthetoolman
2
在WWDC的演示中,我记得他们在调用转换之前称其为“before calling”,因为转换并不意味着当前VC将移动到nil。对于UITabBarController,转换不会更改任何vc的父级。从父级中删除会调用didMoveToParentViewController:nil,但没有will...调用。在我看来。 - Felix Lamouroux

7

好的,我知道问题说不使用导航控制器,但没有理由不使用。 OP 没有及时回复评论让我去睡觉。请不要给我投反对票。:)

以下是如何使用导航控制器从当前视图控制器弹出并翻转到新视图控制器:

UINavigationController *myNavigationController = self.navigationController;
[[self retain] autorelease];

[myNavigationController popViewControllerAnimated:NO];

PreferencesViewController *controller = [[PreferencesViewController alloc] initWithNibName:nil bundle:nil];

[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration: 0.65];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:myNavigationController.view cache:YES];
[myNavigationController pushViewController:controller animated:NO];
[UIView commitAnimations];

[controller release];

这不会堆叠视图控制器吗? - TigerCoding
1
是的,因为我们正在使用导航控制器。然而,它可以绕过您可以执行哪种过渡的限制,这我认为是您问题的核心所在。 - Richard Brightwell
这个可以工作!但是,我很好奇...当视图被弹出时,为什么它不会立即消失?这是因为在动画开始时它仍然在UI线程中吗? - TigerCoding
我现在必须去睡觉了。已经过了睡觉时间,否则我很乐意继续帮助你。这里有一些告别的想法:ARC - 从未尝试过开启ARC。堆栈计数 - 也许你可以迭代并查看堆栈上的VCs。希望这能帮助到你。Zzzzzzzzz - Richard Brightwell
@RubberDuck,回应你上面的评论 - 我把它发布在这个答案中。 - Richard Brightwell
显示剩余7条评论

3

我刚好遇到了这个问题,并尝试在所有现有的答案上进行变化,取得了有限的成功。以下是我最终解决它的方法:

正如在这篇关于自定义segue的文章中所述,制作自定义segue实际上非常容易。在界面生成器中,它们也很容易连接,可以保持IB中的关系可见,并且不需要segue的源/目标视图控制器提供太多支持。

上面链接的文章提供了IOS 4代码,用于使用从顶部滑入的动画替换导航控制器堆栈上的当前顶部视图控制器。

在我的情况下,我想要类似的替换segue发生,但使用FlipFromLeft转换。我只需要支持iOS 5+。 代码:

RAFlipReplaceSegue.h文件:

#import <UIKit/UIKit.h>

@interface RAFlipReplaceSegue : UIStoryboardSegue
@end

来自RAFlipReplaceSegue.m:

#import "RAFlipReplaceSegue.h"

@implementation RAFlipReplaceSegue

-(void) perform
{
    UIViewController *destVC = self.destinationViewController;
    UIViewController *sourceVC = self.sourceViewController;
    [destVC viewWillAppear:YES];

    destVC.view.frame = sourceVC.view.frame;

    [UIView transitionFromView:sourceVC.view
                        toView:destVC.view
                      duration:0.7
                       options:UIViewAnimationOptionTransitionFlipFromLeft
                    completion:^(BOOL finished)
                    {
                        [destVC viewDidAppear:YES];

                        UINavigationController *nav = sourceVC.navigationController;
                        [nav popViewControllerAnimated:NO];
                        [nav pushViewController:destVC animated:NO];
                    }
     ];
}

@end

现在,控制拖动以建立任何其他类型的segue,然后将其设置为自定义segue,并键入自定义segue类的名称,这样就完成了!

有没有一种编程方式可以在没有Storyboard的情况下实现这个功能? - zakdances
据我所知,要使用segue,您必须在storyboard中定义它并给它一个标识符。您可以使用UIViewController的-performSegueWithIdentifier:sender:方法在代码中调用segue。 - Ryan Artecona
1
你不应该直接调用viewDidXXX和viewWillXXX方法。 - pronebird

2

我曾经花了很长时间苦苦思索这个问题,其中一个问题在这里有列出,我不确定你是否也遇到过这个问题。但是如果必须要在iOS 4上运行,这是我的建议。

首先,创建一个新的NavigationController类。这是我们将要做所有“肮脏工作”的地方 - 其他类将能够“干净地”调用实例方法,例如pushViewController:等。在您的.h文件中:

@interface NavigationController : UIViewController {
    NSMutableArray *childViewControllers;
    UIViewController *currentViewController;
}

- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL))completion;
- (void)addChildViewController:(UIViewController *)childController;
- (void)removeChildViewController:(UIViewController *)childController;

子视图控制器数组将作为我们堆栈中所有视图控制器的存储。我们会自动将所有旋转和调整大小的代码从NavigationController的视图转发到currentController

现在,在我们的实现中:

- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL))completion
{
    currentViewController = [toViewController retain];
    // Put any auto- and manual-resizing handling code here

    [UIView animateWithDuration:duration animations:animations completion:completion];

    [fromViewController.view removeFromSuperview];
}

- (void)addChildViewController:(UIViewController *)childController {
    [childViewControllers addObject:childController];
}

- (void)removeChildViewController:(UIViewController *)childController {
    [childViewControllers removeObject:childController];
}

现在你可以使用这些方法调用来实现自己定制的pushViewController:popViewController等功能。
祝你好运,希望这能帮到你!

我们必须再次将视图控制器作为现有视图控制器的子视图添加。尽管现有的导航控制器就是这样做的,但这意味着我们基本上必须重写它及其所有方法。实际上,我们应该避免分发viewWillAppear和类似的方法。我开始觉得没有干净的方法来解决这个问题。不过,我感谢您花费时间和精力! - TigerCoding
我认为,通过根据需要添加和删除视图控制器的这种系统,这个解决方案可以避免你不得不转发那些方法。 - aopsfan
你确定吗?我以前也是用类似的方式交换视图控制器,但我总是需要转发消息。你能确认一下吗? - TigerCoding
不,我不确定,但我认为只要在视图控制器的视图消失时删除它们,并在它们出现时添加它们,那么这应该会自动触发viewWillAppearviewDidAppear等方法。 - aopsfan

-1
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
UINavigationController *viewController = (UINavigationController *)[storyboard instantiateViewControllerWithIdentifier:@"storyBoardIdentifier"];
viewController.modalTransitionStyle = UIModalTransitionStylePartialCurl;
[self presentViewController:viewController animated:YES completion:nil];

试试这段代码。


这段代码实现将一个视图控制器从一个带有导航控制器的视图控制器转换到另一个视图控制器的过渡。

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