Лучший способ переключаться между UISplitViewController и другими контроллерами представления?

Я создаю приложение для iPad. Один из экранов в приложении идеально подходит для использования UISplitViewController. Однако верхний уровень приложения - это главное меню, для которого я не хочу использовать UISplitViewController. Это представляет проблему, потому что Apple заявляет, что:

  1. UISplitViewController должен быть контроллером представления верхнего уровня в приложении, т.е. его представление должно быть добавлено как подпредставление UIWindow

  2. если используется, UISplitViewController должно быть там на протяжении всего времени жизни приложения - т.е. не удаляйте его представление из UIWindow и не ставьте другое на место, или наоборот

Прочитав все вокруг и экспериментируя, кажется, что это единственный приемлемый вариант, чтобы удовлетворить требования Apple, а наша собственная - использовать модальные диалоги. Таким образом, наше приложение имеет UISplitViewController на корневом уровне (то есть его представление добавлено как подпредставление UIWindow), и чтобы показать наше главное меню, мы помещаем его как полноэкранный модальный диалог в UISplitViewController. Затем, закрыв модальное диалоговое окно контроллера представления главного меню, мы можем фактически показать наш разделенный вид.

Эта стратегия, кажется, работает нормально. Но напрашивается вопрос:

1) Есть ли лучший способ структурировать это, без модалов, который также отвечает всем упомянутым требованиям? Кажется немного странным, когда основной пользовательский интерфейс отображается в виде модального диалога. (Модалы должны быть предназначены для целевых пользовательских задач.)

2) Могу ли я отказаться от магазина приложений из-за моего подхода? Эта модальная стратегия, вероятно, "неправильно использует" модальные диалоги, согласно руководству Apple по человеческому интерфейсу. Но какой другой выбор они дали мне? Знают ли они, что я делаю это?

9 ответов

Решение

Touche! Подбежал к той же проблеме и решил ее так же, используя модалы. В моем случае это был вид входа в систему, а затем главное меню, которое должно отображаться перед разделенным просмотром. Я использовал ту же стратегию, что и придуманная вами. Я (как и несколько других знающих людей с iOS, с которыми я разговаривал) не мог найти лучшего выхода. У меня отлично работает. Пользователь никогда не замечает модальное в любом случае. Представь их так. И да, я также могу сказать вам, что в App Store есть немало приложений, которые делают то же самое под капюшоном.:) С другой стороны, дайте мне знать, если вы когда-нибудь придумаете какой-нибудь лучший выход:)

Я серьезно не верил, что эта концепция наличия некоторого UIViewController для отображения перед UISplitViewController (например, формы входа в систему) оказалась настолько сложной, что мне пришлось создавать высокоуровневые представления такого типа.

Мой пример основан на iOS 8 и XCode 6.0 (Swift), поэтому я не уверен, существовала ли ранее эта проблема аналогичным образом, или это связано с некоторыми новыми ошибками, появившимися в iOS 8, но из всех подобных вопросов, которые я обнаружил, я не видел полного "не очень хакерского" решения этой проблемы.

Я проведу вас через некоторые из вещей, которые я пробовал, прежде чем я нашел решение (в конце этого поста). Каждый пример основан на создании нового проекта из шаблона Master-Detail без включения CoreData.


Первая попытка (модальный переход к UISplitViewController):

  1. создать новый подкласс UIViewController (например, LoginViewController)
  2. добавьте новый контроллер представления в раскадровку, установите его в качестве начального контроллера представления (вместо UISplitViewController) и подключите его к LoginViewController
  3. добавьте UIButton к LoginViewController и создайте модальный переход от этой кнопки к UISplitViewController
  4. переместить стандартный код установки для UISplitViewController из AppDelegate's didFinishLaunchingWithOptions в LoginViewController's prepareForSegue

Это почти сработало. Я говорю почти, потому что после того, как приложение запущено с LoginViewController и вы нажимаете кнопку и переходите к UISplitViewController, происходит странная ошибка: показ и скрытие главного контроллера вида при изменении ориентации больше не анимируется.

Спустя некоторое время, пытаясь справиться с этой проблемой и без реального решения, я подумал, что это как-то связано с тем странным правилом, согласно которому UISplitViewController должен быть rootViewController (а в данном случае это не так, LoginViewController), поэтому я отказался от этого не столь идеального решения.,


Вторая попытка (модальный переход от UISplitViewController):

  1. создать новый подкласс UIViewController (например, LoginViewController)
  2. добавьте новый контроллер представления в раскадровку и подключите его к LoginViewController (но на этот раз оставьте UISplitViewController в качестве начального контроллера представления)
  3. создать модальный переход от UISplitViewController к LoginViewController
  4. добавьте UIButton в LoginViewController и создайте раскрутку с помощью этой кнопки

Наконец, добавьте этот код в AppDelegate's didFinishLaunchingWithOptions после стандартного кода для настройки UISplitViewController:

window?.makeKeyAndVisible()
splitViewController.performSegueWithIdentifier("segueToLogin", sender: self)
return true

или попробуйте этот код:

window?.makeKeyAndVisible()
let loginViewController = splitViewController.storyboard?.instantiateViewControllerWithIdentifier("LoginVC") as LoginViewController
splitViewController.presentViewController(loginViewController, animated: false, completion: nil)
return true

Оба эти примера приводят к нескольким плохим вещам:

  1. консольные выходы: Unbalanced calls to begin/end appearance transitions for <UISplitViewController: 0x7fc8e872fc00>
  2. UISplitViewController должен отображаться первым, прежде чем LoginViewController будет модально выделен (я бы предпочел представить только форму входа, чтобы пользователь не видел UISplitViewController до входа в систему)
  3. Раскрутка segue не вызывается (это совершенно другая ошибка, и я не буду вдаваться в эту историю сейчас)

Решение (обновить rootViewController)

Единственный способ, который я нашел, который работает правильно, это если вы измените rootViewController окна на лету:

  1. Определите Storyboard ID для LoginViewController и UISplitViewController и добавьте какое-либо свойство loggedIn в AppDelegate.
  2. Основываясь на этом свойстве, создайте экземпляр соответствующего контроллера представления и после этого установите его как rootViewController.
  3. Сделать это без анимации в didFinishLaunchingWithOptions но анимированные при вызове из пользовательского интерфейса.

Вот пример кода из AppDelegate:

var loggedIn = false

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    setupRootViewController(false)
    return true
}

func setupRootViewController(animated: Bool) {
    if let window = self.window {
        var newRootViewController: UIViewController? = nil
        var transition: UIViewAnimationOptions

        // create and setup appropriate rootViewController
        if !loggedIn {
            let loginViewController = window.rootViewController?.storyboard?.instantiateViewControllerWithIdentifier("LoginVC") as LoginViewController
            newRootViewController = loginViewController
            transition = .TransitionFlipFromLeft

        } else {
            let splitViewController = window.rootViewController?.storyboard?.instantiateViewControllerWithIdentifier("SplitVC") as UISplitViewController
            let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as UINavigationController
            navigationController.topViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem()
            splitViewController.delegate = self

            let masterNavigationController = splitViewController.viewControllers[0] as UINavigationController
            let controller = masterNavigationController.topViewController as MasterViewController

            newRootViewController = splitViewController
            transition = .TransitionFlipFromRight
        }

        // update app's rootViewController
        if let rootVC = newRootViewController {
            if animated {
                UIView.transitionWithView(window, duration: 0.5, options: transition, animations: { () -> Void in
                    window.rootViewController = rootVC
                    }, completion: nil)
            } else {
                window.rootViewController = rootVC
            }
        }
    }
}

И это пример кода из LoginViewController:

@IBAction func login(sender: UIButton) {
    let delegate = UIApplication.sharedApplication().delegate as AppDelegate
    delegate.loggedIn = true
    delegate.setupRootViewController(true)
}

Я также хотел бы услышать, есть ли какой-нибудь лучший / более чистый способ, чтобы это работало должным образом в iOS 8.

А кто сказал, что у тебя может быть только одно окно?:)

Посмотрите, может ли помочь мой ответ на этот похожий вопрос.

Этот подход работает очень хорошо для меня. Пока вам не нужно беспокоиться о нескольких дисплеях или восстановлении состояния, этого связанного кода должно быть достаточно для того, чтобы делать то, что вам нужно: вам не нужно заставлять свою логику смотреть назад или переписывать существующий код, и вы все равно можете воспользоваться UISplitView на более глубоком уровне в вашем приложении - без (AFAIK) нарушения правил Apple.

Для будущих разработчиков iOS, сталкивающихся с той же проблемой: вот еще один ответ и объяснения. Вы должны сделать его корневым контроллером. Если это не так, наложите модальное.

UISplitviewcontroller не является контроллером root view

Просто столкнулся с этой проблемой в проекте и подумал, что поделюсь своим решением. В нашем случае (для iPad) мы хотели начать с UISplitViewController с видимыми обоими контроллерами представления (используя preferredDisplayMode = .allVisible). В какой-то момент в иерархии деталей (справа) (у нас тоже был контроллер навигации для этой стороны) мы хотели поместить новый контроллер представления поверх всего контроллера разделенного представления (не использовать модальный переход).

На iPhone такое поведение предоставляется бесплатно - в любой момент времени виден только один контроллер представления. Но на iPad нам пришлось придумать что-то еще. Мы закончили тем, что выбрали контроллер представления корневого контейнера, который добавляет контроллер разделенного представления к нему как дочерний контроллер представления. Этот корневой контроллер представления встроен в контроллер навигации. Когда контроллер подробного вида в контроллере разделенного вида хочет протолкнуть новый контроллер по всему контроллеру разделенного вида, корневой контроллер представления толкает этот новый контроллер представления со своим контроллером навигации.

Другой вариант: в контроллере подробного представления я отображаю модальный контроллер представления:

let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
if (!appDelegate.loggedIn) {
    // display the login form
    let storyboard = UIStoryboard(name: "Storyboard", bundle: nil)
    let login = storyboard.instantiateViewControllerWithIdentifier("LoginViewController") as UIViewController
    self.presentViewController(login, animated: false, completion: { () -> Void in
       // user logged in and is valid now
       self.updateDisplay()
    })
} else {
    updateDisplay()
}

Не отклоняйте контроллер входа, не установив флаг входа. Обратите внимание, что в IP-телефонах главный контроллер представления будет первым, поэтому очень похожий код должен быть на главном контроллере представления.

Я сделал UISplitView в качестве начального представления, затем он переходит к модальному полноэкранному UIView и обратно к UISplitView. Если вам нужно вернуться к SplitView, вы должны использовать пользовательский переход.

Прочитайте эту ссылку (переведите ее с японского)

UIViewController для UISplitViewController

Добавляя к ответу @tadija я нахожусь в похожей ситуации:

Мое приложение было только для телефонов, и я добавляю планшетный интерфейс. Я решил сделать это в Swift в том же приложении - и в конечном итоге перенести все приложения на одну и ту же раскадровку (когда я чувствую, что версия IPad стабильна, использование ее для телефонов должно быть тривиально с новыми классами из XCode6).

В моей сцене еще не определены сегменты, и это все еще работает.

Мой код в моем делегате приложения находится в ObjectiveC, и немного отличается - но использует ту же идею. Обратите внимание, что я использую контроллер вида по умолчанию со сцены, в отличие от предыдущих примеров. Я чувствую, что это также будет работать на IOS7/IPhone, в котором среда выполнения будет генерировать регулярный UINavigationController вместо UISplitViewController, Я мог бы даже добавить новый код, который будет выдвигать контроллер вида входа в систему на IPhones вместо изменения rootVC.

- (void) setupRootViewController:(BOOL) animated {
    UIViewController *newController = nil;
    UIStoryboard *board = [UIStoryboard storyboardWithName:@"Storyboard" bundle:nil];
    UIViewAnimationOptions transition = UIViewAnimationOptionTransitionCrossDissolve;

    if (!loggedIn) {
        newController = [board instantiateViewControllerWithIdentifier:@"LoginViewController"];
    } else {
        newController = [board instantiateInitialViewController];
    }

    if (animated) {
        [UIView transitionWithView: self.window duration:0.5 options:transition animations:^{
            self.window.rootViewController = newController;
            NSLog(@"setup root view controller animated");
        } completion:^(BOOL finished) {
            NSLog(@"setup root view controller finished");
        }];
    } else {
        self.window.rootViewController = newController;
    }
}

Я хотел бы поделиться своим подходом к представлению UISplitViewController, как вы могли бы через -presentViewController:animated:completion: (мы все знаем, что это не сработает). Я создал подкласс UISplitViewController, который отвечает на:

-presentAsRootViewController
-returnToPreviousViewController

Класс, который, как и другие успешные подходы, устанавливает UISplitViewController в качестве rootViewController окна, но делает это с анимацией, аналогичной той, которую вы получаете (по умолчанию) с -presentViewController:animated:completion:

PresentableSplitViewController.h

#import <UIKit/UIKit.h>    
@interface PresentableSplitViewController : UISplitViewController    
- (void) presentAsRootViewController;
@end

PresentableSplitViewController.m

#import "PresentableSplitViewController.h"

@interface PresentableSplitViewController ()
@property (nonatomic, strong) UIViewController *previousViewController;
@end

@implementation PresentableSplitViewController

- (void) presentAsRootViewController {

    UIWindow *window=[[[UIApplication sharedApplication] delegate] window];
    _previousViewController=window.rootViewController;

    UIView *windowSnapShot = [window snapshotViewAfterScreenUpdates:YES];
    window.rootViewController = self;

    [window insertSubview:windowSnapShot atIndex:0];

    CGRect dstFrame=self.view.frame;

    CGSize offset=CGSizeApplyAffineTransform(CGSizeMake(0, 1), window.rootViewController.view.transform);
    offset.width*=self.view.frame.size.width;
    offset.height*=self.view.frame.size.height;
    self.view.frame=CGRectOffset(self.view.frame, offset.width, offset.height);

    [UIView animateWithDuration:0.5
                          delay:0.0
         usingSpringWithDamping:1.0
          initialSpringVelocity:0.0
                        options:UIViewAnimationOptionCurveEaseInOut
                     animations:^{
                         self.view.frame=dstFrame;
                     } completion:^(BOOL finished) {
                         [windowSnapShot removeFromSuperview];
                     }];
}

- (void) returnToPreviousViewController {
    if(_previousViewController) {

        UIWindow *window=[[[UIApplication sharedApplication] delegate] window];

        UIView *windowSnapShot = [window snapshotViewAfterScreenUpdates:YES];
        window.rootViewController = _previousViewController;

        [window addSubview:windowSnapShot];

        CGSize offset=CGSizeApplyAffineTransform(CGSizeMake(0, 1), window.rootViewController.view.transform);
        offset.width*=windowSnapShot.frame.size.width;
        offset.height*=windowSnapShot.frame.size.height;

        CGRect dstFrame=CGRectOffset(windowSnapShot.frame, offset.width, offset.height);

        [UIView animateWithDuration:0.5
                              delay:0.0
             usingSpringWithDamping:1.0
              initialSpringVelocity:0.0
                            options:UIViewAnimationOptionCurveEaseInOut
                         animations:^{
                             windowSnapShot.frame=dstFrame;
                         } completion:^(BOOL finished) {
                             [windowSnapShot removeFromSuperview];
                             _previousViewController=nil;
                         }];
    }
}

@end
Другие вопросы по тегам