Animować zmianę kontrolerów widoku bez korzystania ze stosu kontrolerów nawigacji, widoków podrzędnych lub kontrolerów modalnych?

142

NavigationControllers mają stosy ViewController do zarządzania i ograniczone przejścia animacji.

Dodanie kontrolera widoku jako widoku podrzędnego do istniejącego kontrolera widoku wymaga przekazywania zdarzeń do kontrolera widoku podrzędnego, co jest trudne w zarządzaniu, obarczone niewielkimi irytacjami i generalnie wydaje się być złym hackiem podczas implementacji (Apple odradza również robiąc to).

Przedstawienie modalnego kontrolera widoku ponownie umieszcza kontroler widoku na drugim i chociaż nie ma problemów z przekazywaniem zdarzeń opisanych powyżej, tak naprawdę nie zamienia kontrolera widoku, ale układa go w stos.

Scenorysy są ograniczone do iOS 5 i są prawie idealne, ale nie można ich używać we wszystkich projektach.

Czy ktoś może przedstawić SOLID CODE EXAMPLE na sposobie zmiany kontrolerów widoku bez powyższych ograniczeń i pozwala na animowane przejścia między nimi?

Bliski przykład, ale bez animacji: jak używać wielu kontrolerów widoku niestandardowego systemu iOS bez kontrolera nawigacji

Edycja: użycie kontrolera nawigacji jest w porządku, ale muszą istnieć animowane style przejść (nie tylko efekty slajdów), a wyświetlany kontroler widoku musi zostać całkowicie zamieniony (nie ułożony w stos). Jeśli drugi kontroler widoku musi usunąć inny kontroler widoku ze stosu, oznacza to, że nie jest wystarczająco hermetyzowany.

Edycja 2: iOS 4 powinien być podstawowym systemem operacyjnym dla tego pytania, powinienem był to wyjaśnić, wspominając o scenorysach (powyżej).

TigerCoding
źródło
1
Za pomocą kontrolera nawigacyjnego można wykonywać niestandardowe przejścia animacji. Jeśli byłoby to do przyjęcia, usuń to ograniczenie ze swojego pytania, a prześlę przykładowy kod.
Richard Brightwell
@Richard, jeśli pominie kłopoty z zarządzaniem stosem i dostosuje różne animowane style przejść między kontrolerami widoku, użycie kontrolera nawigacji będzie w porządku!
TigerCoding
Ok dobrze. Zniecierpliwiłem się i wysłałem kod. Spróbuj. Pracuje dla mnie.
Richard Brightwell
@RichardBrightwell Powiedziałeś tutaj, że można wykonać niestandardowe przejścia animacji między kontrolerami widoku za pomocą kontrolera nawigacji ... jak? Czy możesz zamieścić przykład? dzięki.
Duck

Odpowiedzi:

108

EDYCJA: Nowa odpowiedź, która działa w każdej orientacji. Oryginalna odpowiedź działa tylko wtedy, gdy interfejs jest w orientacji pionowej. Są to animacje przejścia widoku b / c, które zastępują widok z innym widokiem, muszą występować z widokami co najmniej o poziom poniżej pierwszego widoku dodanego do okna (npwindow.rootViewController.view.anotherView.).

Zaimplementowałem prostą klasę kontenera, którą nazwałem TransitionController. Możesz go znaleźć na https://gist.github.com/1394947 .

Na marginesie wolę implementację w oddzielnej klasie b / c, która jest łatwiejsza do ponownego wykorzystania. Jeśli tego nie chcesz, możesz po prostu zaimplementować tę samą logikę bezpośrednio w delegacie aplikacji, eliminując potrzebę stosowania TransitionControllerklasy. Logika, której potrzebujesz, byłaby jednak taka sama.

Użyj go w następujący sposób:

W delegacie aplikacji

// 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;
}

Aby przejść do nowego kontrolera widoku z dowolnego kontrolera widoku

- (IBAction)flipToView
{
    anotherViewController *vc = [[AnotherViewController alloc] init...];
    MyAppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
    [appDelegate.transitionController transitionToViewController:vc withOptions:UIViewAnimationOptionTransitionFlipFromRight];
}

EDYCJA: Oryginalna odpowiedź poniżej - działa tylko w orientacji pionowej

Przyjąłem następujące założenia dla tego przykładu:

  1. Masz przypisany kontroler widoku jako rootViewControllerokno

  2. Po przełączeniu do nowego widoku chcesz zamienić bieżący viewController na viewController będący właścicielem nowego widoku. W dowolnym momencie aktywny jest tylko bieżący kontroler viewController (np. Przydzielony).

Kod można łatwo zmodyfikować, aby działał inaczej, kluczową kwestią jest animowane przejście i kontroler pojedynczego widoku. Upewnij się, że nie zachowujesz kontrolera widoku poza przypisaniem go do window.rootViewController.

Kod do animacji przejścia w delegacie aplikacji

- (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;
                    }];
}

Przykładowe użycie w kontrolerze widoku

- (IBAction)flipToNextView
{
    AnotherViewController *anotherVC = [[AnotherVC alloc] init...];
    MyAppDelegate *appDelegate = (MyAppDelegate *)[UIApplication sharedApplication].delegate;
    [appDelegate transitionToViewController:anotherVC
                             withTransition:UIViewAnimationOptionTransitionFlipFromRight];
}
XJones
źródło
1
Tak, bardzo ładnie! Nie tylko załatwia sprawę, ale jest to bardzo prosty i przejrzysty przykład kodu. Wielkie dzięki!
TigerCoding
Czy on nie powoduje problemów w krajobrazie? Ponadto: czy to wywołuje metody willAppear i didAppear w kontrolerach viewControllers?
Felix Lamouroux
1
Wiem, że połączenia pozorne są wykonywane, ponieważ je zarejestrowałem. Nie rozumiem też, dlaczego miałoby to wpływać na zmiany orientacji. Czy możesz wyjaśnić, dlaczego myślisz, że tak?
TigerCoding
1
@XJones świetne rozwiązanie, działa całkiem dobrze w mojej aplikacji na iPada z wyjątkiem jednego problemu, z którym mam do czynienia, po pierwszej inicjalizacji transVC = [TransitionController ... initWithViewController: aUINavCtrlObj]; window.rootVC = transVC; widok w aUINavCtrlObj jest dopełniany 20 pikseli od góry (tj. dolne 20 pikseli jest poza granicami ekranu (UIWindow) we wszystkich orientacjach), ale po wykonaniu [transVC przejścieToViewController: anotherVC] wypełnienie zniknęło. Próbowałem wantFullScreenLayout = NO w loadView TransitionControllera, dodaje czarny obszar o rozmiarze 20 pikseli tuż pod paskiem statusu.
Abduliam Rehmanius,
1
@AbduliamRehmanius: Miałem ten sam problem. Naprawiłem to, zmieniając wiersz 25 z TransitionController.mna UIView *view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];, ale używałem tego tylko w najnowszej wersji iOS, więc przetestuj uważnie.
Phil Calvin
67

Możesz użyć nowego systemu powstrzymywania ViewController firmy Apple. Aby uzyskać bardziej szczegółowe informacje, obejrzyj film z sesji WWDC 2011 „Implementing UIViewControllerContainment”.

Nowość w systemie iOS5, UIViewControllerContainment umożliwia posiadanie elementu nadrzędnego viewController i wielu zawartych w nim kontrolerów podrzędnych. Tak działa UISplitViewController. W ten sposób możesz ułożyć kontrolery widoku w nadrzędnym, ale dla twojej konkretnej aplikacji używasz tylko rodzica do zarządzania przejściem z jednego widocznego viewController do drugiego. Jest to zatwierdzony przez Apple sposób robienia rzeczy i animowania z jednego widoku dziecka. Kontroler jest bezbolesny. Dodatkowo możesz używać wszystkich różnych UIViewAnimationOptionprzejść!

Ponadto dzięki UIViewContainment nie musisz się martwić o bałagan związany z zarządzaniem podrzędnymi kontrolerami widoku podczas wydarzeń orientacyjnych. Możesz po prostu użyć poniższego, aby upewnić się, że parentViewController przekazuje zdarzenia rotacji do elementu podrzędnego viewControllers.

- (BOOL)automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers{
    return YES;
}

Możesz wykonać następujące lub podobne czynności w metodzie viewDidLoad swojego rodzica, aby skonfigurować pierwszy element childViewController:

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

wtedy, gdy trzeba zmienić element podrzędny viewController, wywołujesz coś w następujący sposób w ramach elementu nadrzędnego viewController:

-(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];
                             }];
 }

Tutaj zamieściłem pełny przykładowy projekt: https://github.com/toolmanGitHub/stackedViewControllers . Ten inny projekt pokazuje, jak używać funkcji UIViewControllerContainment na niektórych różnych typach kontrolerów danych wejściowych, które nie zajmują całego ekranu. Powodzenia

timthetoolman
źródło
1
Świetny przykład. Dziękujemy za poświęcenie czasu na wysłanie kodu. Z drugiej strony jest ograniczony tylko do iOS 5. Jak wspomniano w pytaniu: „[Storyboardy] są ograniczone do iOS 5 i są prawie idealne, ale nie można ich używać we wszystkich projektach”. Biorąc pod uwagę duży odsetek (około 40%?) Klientów nadal korzysta z iOS 4, celem jest zapewnienie czegoś, co działa w iOS 4 i nowszych.
TigerCoding
Czy nie powinieneś zadzwonić [self.currentViewController willMoveToParentViewController:nil];przed przejściem?
Felix Lamouroux
@FelixLam - zgodnie z dokumentami zawartymi w UIViewController, wywołujesz willMoveToParentViewController tylko wtedy, gdy zastąpisz metodę addChildViewController. W tym przykładzie nazywam to, ale nie zastępuję.
timthetoolman
2
Wydaje się, że w demie na WWDC pamiętam, że nazwali to przed wywołaniem rozpoczęcia przejścia, ponieważ przejście nie oznacza, że ​​currentVC przejdzie do zera. W przypadku UITabBarController przejście nie zmieni żadnego elementu nadrzędnego vc. Usunięcie z rodzica wywołuje didMoveToParentViewController: nil, ale nie ma wywołania will ... IMHO
Felix Lamouroux
7

OK, wiem, że pytanie brzmi bez użycia kontrolera nawigacyjnego, ale nie ma powodu, aby tego nie robić. OP nie odpowiadał na komentarze na czas, abym położył się spać. Nie głosuj na mnie. :)

Oto jak otworzyć bieżący kontroler widoku i przejść do nowego kontrolera widoku za pomocą kontrolera nawigacji:

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];
Richard Brightwell
źródło
Czy to nie jest kontrolery widoku stosu?
TigerCoding
1
Tak, ponieważ używamy kontrolera nawigacyjnego. Jednak pozwala to obejść ograniczenia dotyczące tego, jakiego rodzaju przejść można wykonać, co moim zdaniem było sednem twojego pytania.
Richard Brightwell
Blisko, ale jednym z poważnych problemów jest to, że na stosie można zarządzać wieloma kontrolerami widoku. Szukam sposobu na całkowitą zmianę kontrolerów widoku. =)
TigerCoding
Ach. Mogę też coś takiego mieć ... daj mi chwilę. Jeśli nie, usunę tę odpowiedź.
Richard Brightwell
Ok, poskładałem razem dwa różne fragmenty kodu. Myślę, że to zrobi, co chcesz.
Richard Brightwell
3

Ponieważ właśnie natknąłem się na ten konkretny problem i wypróbowałem różne warianty wszystkich wcześniej istniejących odpowiedzi na ograniczony sukces, opublikuję, jak ostatecznie go rozwiązałem:

Jak opisano w tym poście na temat niestandardowych segmentów , tworzenie niestandardowych segmentów jest naprawdę łatwe. Są również bardzo łatwe do podłączenia w Interface Builder, utrzymują relacje w IB widoczne i nie wymagają dużego wsparcia ze strony kontrolerów widoku źródła / miejsca docelowego.

W powyższym poście znajduje się kod iOS 4, który zastępuje bieżący kontroler widoku z góry w stosie navigationController na nowy, używając animacji wsuwania od góry.

W moim przypadku chciałem, aby nastąpiła podobna zmiana, ale z FlipFromLeftprzejściem. Potrzebowałem też tylko wsparcia dla iOS 5+. Kod:

Z RAFlipReplaceSegue.h:

#import <UIKit/UIKit.h>

@interface RAFlipReplaceSegue : UIStoryboardSegue
@end

Z 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

Teraz, przytrzymaj klawisz Ctrl i przeciągnij, aby ustawić dowolny inny rodzaj płynności, a następnie utwórz z niego niestandardowe przejście i wpisz nazwę niestandardowej klasy płynnej, et voilà!

Ryan Artecona
źródło
Czy istnieje sposób na to programowo bez scenorysu?
zakdances
O ile wiem, aby użyć płynności, musisz ją zdefiniować i nadać jej identyfikator w storyboardzie. Możesz wywołać segue w kodzie za pomocą –performSegueWithIdentifier:sender:metody UIViewController .
Ryan Artecona
1
Nigdy nie powinieneś bezpośrednio wywoływać viewDidXXX i viewWillXXX.
Ben Sinclair
2

Walczyłem z tym przez długi czas i jeden z moich problemów jest tutaj wymieniony , nie jestem pewien, czy miałeś ten problem. Ale oto, co poleciłbym, jeśli musi działać z iOS 4.

Najpierw utwórz nową NavigationControllerklasę. Tutaj wykonamy całą brudną robotę - inne klasy będą mogły „czysto” wywoływać metody instancji, takie jak pushViewController:i takie. W twoim .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;

Tablica kontrolerów widoku podrzędnego będzie służyć jako magazyn dla wszystkich kontrolerów widoku w naszym stosie. Automatycznie przekazalibyśmy dalej cały obrót i zmianę rozmiaru kodu z NavigationControllerwidoku do pliku currentController.

Teraz w naszej realizacji:

- (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];
}

Teraz można wdrożyć swój własny pushViewController:, popViewControlleri takie, używając tych wywołań metod.

Powodzenia i mam nadzieję, że to pomoże!

aopsfan
źródło
Ponownie musimy uciec się do dodania kontrolerów widoku jako widoków podrzędnych istniejących kontrolerów widoku. To prawda, właśnie to robi istniejący kontroler nawigacji, ale oznacza to, że w zasadzie musimy go przepisać i wszystkie jego metody. W rzeczywistości powinniśmy unikać rozpowszechniania metody viewWillAppear i podobnych metod. Zaczynam myśleć, że nie ma na to czystego sposobu. Jednak dziękuję za poświęcony czas i wysiłek!
TigerCoding
Myślę, że dzięki temu systemowi dodawania i usuwania kontrolerów widoku w razie potrzeby to rozwiązanie zapobiega konieczności przekazywania tych metod.
aopsfan
Jesteś pewny? Wcześniej zamieniałem kontrolery widoku w podobny sposób i zawsze musiałem przekazywać wiadomości. Czy możesz potwierdzić inaczej?
TigerCoding
Nie, nie jestem pewien, ale zakładam, że tak długo, jak usunąć widoki kontrolerów widzenia jak znikają, i dodać je jako one pojawiają, który powinien automatycznie spust viewWillAppear, viewDidAppeari takie.
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];

Wypróbuj ten kod.


Ten kod zapewnia przejście z kontrolera widoku do innego kontrolera widoku, który ma kontroler nawigacji.

Deepak Kumar Sahu
źródło