Лучший способ переключаться между UISplitViewController и другими контроллерами представления?
Я создаю приложение для iPad. Один из экранов в приложении идеально подходит для использования UISplitViewController. Однако верхний уровень приложения - это главное меню, для которого я не хочу использовать UISplitViewController. Это представляет проблему, потому что Apple заявляет, что:
UISplitViewController
должен быть контроллером представления верхнего уровня в приложении, т.е. его представление должно быть добавлено как подпредставлениеUIWindow
если используется,
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):
- создать новый подкласс UIViewController (например, LoginViewController)
- добавьте новый контроллер представления в раскадровку, установите его в качестве начального контроллера представления (вместо UISplitViewController) и подключите его к LoginViewController
- добавьте UIButton к LoginViewController и создайте модальный переход от этой кнопки к UISplitViewController
- переместить стандартный код установки для UISplitViewController из AppDelegate's
didFinishLaunchingWithOptions
в LoginViewController'sprepareForSegue
Это почти сработало. Я говорю почти, потому что после того, как приложение запущено с LoginViewController и вы нажимаете кнопку и переходите к UISplitViewController, происходит странная ошибка: показ и скрытие главного контроллера вида при изменении ориентации больше не анимируется.
Спустя некоторое время, пытаясь справиться с этой проблемой и без реального решения, я подумал, что это как-то связано с тем странным правилом, согласно которому UISplitViewController должен быть rootViewController (а в данном случае это не так, LoginViewController), поэтому я отказался от этого не столь идеального решения.,
Вторая попытка (модальный переход от UISplitViewController):
- создать новый подкласс UIViewController (например, LoginViewController)
- добавьте новый контроллер представления в раскадровку и подключите его к LoginViewController (но на этот раз оставьте UISplitViewController в качестве начального контроллера представления)
- создать модальный переход от UISplitViewController к LoginViewController
- добавьте 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
Оба эти примера приводят к нескольким плохим вещам:
- консольные выходы:
Unbalanced calls to begin/end appearance transitions for <UISplitViewController: 0x7fc8e872fc00>
- UISplitViewController должен отображаться первым, прежде чем LoginViewController будет модально выделен (я бы предпочел представить только форму входа, чтобы пользователь не видел UISplitViewController до входа в систему)
- Раскрутка segue не вызывается (это совершенно другая ошибка, и я не буду вдаваться в эту историю сейчас)
Решение (обновить rootViewController)
Единственный способ, который я нашел, который работает правильно, это если вы измените rootViewController окна на лету:
- Определите Storyboard ID для LoginViewController и UISplitViewController и добавьте какое-либо свойство loggedIn в AppDelegate.
- Основываясь на этом свойстве, создайте экземпляр соответствующего контроллера представления и после этого установите его как rootViewController.
- Сделать это без анимации в
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, сталкивающихся с той же проблемой: вот еще один ответ и объяснения. Вы должны сделать его корневым контроллером. Если это не так, наложите модальное.
Просто столкнулся с этой проблемой в проекте и подумал, что поделюсь своим решением. В нашем случае (для 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, вы должны использовать пользовательский переход.
Прочитайте эту ссылку (переведите ее с японского)
Добавляя к ответу @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