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

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

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

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

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

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

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

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

Ответы [ 9 ]

19 голосов
/ 22 сентября 2014

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

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

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


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

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

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

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


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

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

Наконец, добавьте этот код в didFinishLaunchingWithOptions AppDelegate после стандартного кода для настройки 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. Unwind segue не вызывается (это совершенно другая ошибка, и я не буду вдаваться в подробности)эта история сейчас)

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

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

  1. Определение идентификатора раскадровки для 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.

6 голосов
/ 18 ноября 2010

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

3 голосов
/ 14 ноября 2013

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

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

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

1 голос
/ 08 марта 2018

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

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

1 голос
/ 19 декабря 2010

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

UISplitviewcontroller не как контроллер rootview

0 голосов
/ 22 декабря 2014

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

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-телефонах главный контроллер представления будет первым, поэтому очень похожий код должен быть на главном контроллере представления.

0 голосов
/ 22 декабря 2014

Добавление к ответу @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;
    }
}
0 голосов
/ 12 ноября 2014

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

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

UIViewController to UISplitViewController

0 голосов
/ 30 октября 2014

Я хотел бы поделиться своим подходом к представлению 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...