Класс размеров для портретного и ландшафтного режимов iPad

97

Я в основном хочу, чтобы мои подвиды располагались по-разному в зависимости от ориентации iPad (книжная или альбомная) с использованием классов размеров, представленных в xcode 6. Я нашел множество руководств, объясняющих, как разные классы размеров доступны для Iphone в портретной и альбомной ориентации на IB. но, тем не менее, похоже, что нет ни одного, охватывающего отдельные ландшафтные или портретные режимы для iPad на IB. Кто-нибудь может помочь?

neelIVP
источник
Похоже, не существует непрограммного решения, которое потребовалось бы для экрана по умолчанию
SwiftArchitect

Ответы:

174

Похоже, что Apple намерена рассматривать обе ориентации iPad как одну и ту же, но, как некоторые из нас находят, есть вполне законные причины дизайна, чтобы захотеть изменить макет пользовательского интерфейса для iPad Portrait и iPad Landscape.

К сожалению, текущая ОС, похоже, не поддерживает это различие ... это означает, что мы вернулись к манипулированию ограничениями автоматической компоновки в коде или аналогичными обходными путями для достижения того, что мы в идеале должны иметь возможность получить бесплатно с помощью адаптивного пользовательского интерфейса. .

Не изящное решение.

Разве нет способа использовать магию, которую Apple уже встроила в IB и UIKit, чтобы использовать размерный класс по нашему выбору для данной ориентации?

~

Обдумывая проблему в более общем плане, я понял, что «классы размера» - это просто способы обратиться к нескольким макетам, хранящимся в IB, чтобы их можно было вызывать по мере необходимости во время выполнения.

Фактически, «размерный класс» - это просто пара значений перечисления. Из UIInterface.h:

typedef NS_ENUM(NSInteger, UIUserInterfaceSizeClass) {
    UIUserInterfaceSizeClassUnspecified = 0,
    UIUserInterfaceSizeClassCompact     = 1,
    UIUserInterfaceSizeClassRegular     = 2,
} NS_ENUM_AVAILABLE_IOS(8_0);

Итак, независимо от того, как Apple решила назвать эти разные варианты, по сути, это просто пара целых чисел, используемых в качестве своего рода уникального идентификатора, чтобы отличить один макет от другого, хранящегося в IB.

Теперь предположим, что мы создаем альтернативный макет (с использованием неиспользуемого класса размера) в IB - скажем, для iPad Portrait ... есть ли способ заставить устройство использовать наш выбор класса размера (макет пользовательского интерфейса) по мере необходимости во время выполнения ?

Попробовав несколько различных (менее элегантных) подходов к проблеме, я подозревал, что есть способ программно переопределить класс размера по умолчанию. И есть (в UIViewController.h):

// Call to modify the trait collection for child view controllers.
- (void)setOverrideTraitCollection:(UITraitCollection *)collection forChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS(8_0);
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS(8_0);

Таким образом, если вы можете упаковать иерархию контроллера представления как «дочерний» контроллер представления и добавить его к родительскому контроллеру представления верхнего уровня ... тогда вы можете условно переопределить дочерний элемент, думая, что это класс другого размера, чем класс по умолчанию из ОС.

Вот пример реализации, которая делает это в «родительском» контроллере представления:

@interface RDTraitCollectionOverrideViewController : UIViewController {
    BOOL _willTransitionToPortrait;
    UITraitCollection *_traitCollection_CompactRegular;
    UITraitCollection *_traitCollection_AnyAny;
}
@end

@implementation RDTraitCollectionOverrideViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setUpReferenceSizeClasses];
}

- (void)setUpReferenceSizeClasses {
    UITraitCollection *traitCollection_hCompact = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
    UITraitCollection *traitCollection_vRegular = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassRegular];
    _traitCollection_CompactRegular = [UITraitCollection traitCollectionWithTraitsFromCollections:@[traitCollection_hCompact, traitCollection_vRegular]];

    UITraitCollection *traitCollection_hAny = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassUnspecified];
    UITraitCollection *traitCollection_vAny = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassUnspecified];
    _traitCollection_AnyAny = [UITraitCollection traitCollectionWithTraitsFromCollections:@[traitCollection_hAny, traitCollection_vAny]];
}

-(void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    _willTransitionToPortrait = self.view.frame.size.height > self.view.frame.size.width;
}

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]
    _willTransitionToPortrait = size.height > size.width;
}

-(UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController {
    UITraitCollection *traitCollectionForOverride = _willTransitionToPortrait ? _traitCollection_CompactRegular : _traitCollection_AnyAny;
    return traitCollectionForOverride;
}
@end

В качестве быстрой демонстрации, чтобы увидеть, работает ли это, я добавил настраиваемые метки специально для версий 'Regular / Regular' и 'Compact / Regular' макета дочернего контроллера в IB:

введите описание изображения здесь введите описание изображения здесь

А вот как это выглядит, когда iPad находится в обеих ориентациях: введите описание изображения здесь введите описание изображения здесь

Вуаля! Конфигурации классов нестандартного размера во время выполнения.

Надеюсь, Apple сделает это ненужным в следующей версии ОС. Между тем, это может быть более элегантный и масштабируемый подход, чем программная возня с ограничениями автоматической компоновки или выполнение других манипуляций в коде.

~

РЕДАКТИРОВАТЬ (6/4/15): имейте в виду, что приведенный выше образец кода по сути является доказательством концепции для демонстрации техники. Не стесняйтесь при необходимости адаптироваться к вашему конкретному приложению.

~

РЕДАКТИРОВАТЬ (7/24/15): отрадно, что приведенное выше объяснение, похоже, помогает прояснить проблему. Хотя я не тестировал его, код mohamede1945 [ниже] выглядит как полезная оптимизация для практических целей. Не стесняйтесь протестировать его и сообщить нам, что вы думаете. (Для полноты картины я оставлю приведенный выше пример кода как есть.)

Рон Даймонд
источник
1
Отличный пост @RonDiamond!
начало 06
3
Apple применяет аналогичный подход к переопределению класса размера ширины в Safari, поэтому вы можете быть уверены, что это более или менее поддерживаемый подход. Вам действительно не нужны дополнительные ivars; UITraitCollectionдостаточно оптимизирован и overrideTraitCollectionForChildViewControllerвызывается достаточно редко, поэтому выполнить проверку ширины и затем создать ее не должно быть проблемой.
zwaldowski
1
@zwaldowski Спасибо. Образец кода здесь предназначен только для демонстрации техники и, очевидно, может быть оптимизирован различными способами по желанию. (Как правило, когда объект используется многократно и многократно [например, всякий раз, когда устройство меняет ориентацию], я не думаю, что это плохая идея - удерживать объект; но, как вы указываете, разница в производительности здесь может быть минимальным.)
RonDiamond
1
@Rashmi: Как я уже упоминал в объяснении, в конечном итоге не имеет значения, что вы выберете - вы просто сопоставляете макет (ы) с парой значений перечисления. Так что вы действительно можете использовать любой «размерный класс», который вам подходит. Я бы просто убедился, что он не конфликтует с каким-либо другим (допустимым) классом размера, который может быть унаследован где-то по умолчанию.
RonDiamond
1
Что касается контроллера дочернего представления, я не думаю, что это имеет значение. Вы должны иметь возможность создать его программно или из пера, если он правильно добавлен в качестве дочернего контроллера в контейнер.
RonDiamond
41

Подводя итог очень длинному ответу Рона Даймонда. Все, что вам нужно сделать, это в вашем корневом контроллере представления.

Objective-c

- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController
{
    if (CGRectGetWidth(self.view.bounds) < CGRectGetHeight(self.view.bounds)) {
        return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
    } else {
        return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
    }
}

Swift:

override func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection! {
        if view.bounds.width < view.bounds.height {
            return UITraitCollection(horizontalSizeClass: .Compact)
        } else {
            return UITraitCollection(horizontalSizeClass: .Regular)
        }
    }

Затем в Storyborad используйте компактную ширину для портрета и обычную ширину для ландшафта.

мохамед1945
источник
Мне интересно, как это будет работать с новыми режимами разделенного экрана ... Один способ узнать!
mm2001
UITraitCollection предназначен только для iOS 8.0+
mohamede1945,
У меня не работает. У меня есть представление с двумя контейнерами, которые должны отображаться в другом месте в зависимости от ориентации. Я создал все необходимые классы размеров, все работает для iPhone. Я попытался использовать ваш код, чтобы разделить ориентацию iPad. Однако это будет просто странно вести себя на просмотрах. Это не меняет его так, как предполагалось. Есть подсказка, что может происходить?
NoSixties 05
1
Это сработало для меня, когда я - (UITraitCollection *)traitCollectionвместо overrideTraitCollectionForChildViewController. Также ограничения должны соответствовать коллекциям признаков, поэтому wC (hAny).
Х. де Йонге
1
@Apollo Я бы хотел, но он не ответил на настоящий вопрос. Взгляните на пример Apple, как использовать setOverrideTraitCollection github.com/ios8/AdaptivePhotosAnAdaptiveApplication/blob/master/…
malhal
5

IPad имеет «обычный» размер как по горизонтали, так и по вертикали, не делая различий между портретом и пейзажем.

Эти характеристики размера можно переопределить в пользовательском UIViewControllerкоде подкласса с помощью метода traitCollection, например:

- (UITraitCollection *)traitCollection {
    // Distinguish portrait and landscape size traits for iPad, similar to iPhone 7 Plus.
    // Be aware that `traitCollection` documentation advises against overriding it.
    UITraitCollection *superTraits = [super traitCollection];
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
        UITraitCollection *horizontalRegular = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
        UITraitCollection *verticalRegular = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassRegular];
        UITraitCollection *regular = [UITraitCollection traitCollectionWithTraitsFromCollections:@[horizontalRegular, verticalRegular]];

        if ([superTraits containsTraitsInCollection:regular]) {
            if (UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation])) {
                // iPad in portrait orientation
                UITraitCollection *horizontalCompact = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
                return [UITraitCollection traitCollectionWithTraitsFromCollections:@[superTraits, horizontalCompact, verticalRegular]];
            } else {
                // iPad in landscape orientation
                UITraitCollection *verticalCompact = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassCompact];
                return [UITraitCollection traitCollectionWithTraitsFromCollections:@[superTraits, horizontalRegular, verticalCompact]];
            }
        }
    }
    return superTraits;
}

- (BOOL)prefersStatusBarHidden {
    // Override to negate this documented special case, and avoid erratic hiding of status bar in conjunction with `traitCollection` override:
    // For apps linked against iOS 8 or later, this method returns true if the view controller is in a vertically compact environment.
    return NO;
}

Это дает iPad такие же габариты, как и iPhone 7 Plus. Обратите внимание, что другие модели iPhone обычно имеют свойство «компактной ширины» (а не обычной ширины) независимо от ориентации.

Подобная имитация iPhone 7 Plus позволяет использовать эту модель в качестве замены iPad в Interface Builder Xcode, который не знает о настройках в коде.

Имейте в виду, что режим Split View на iPad может отличаться по размеру от обычного полноэкранного режима.

Этот ответ основан на подходе, принятом в этом сообщении в блоге , с некоторыми улучшениями.

Обновление 2019-01-02: Обновлено, чтобы исправить периодически возникающую скрытую строку состояния в ландшафте iPad и потенциальное попирание (новых) черт в UITraitCollection. Также отмечено, что документация Apple не рекомендует переопределять traitCollection, поэтому в будущем могут возникнуть проблемы с этим методом.

Jedwidz
источник
Apple указывает, что traitCollectionсвойство должно быть
доступно
4

Длинный и полезный ответ RonDiamond - хорошее начало для понимания принципов, однако код, который работал у меня (iOS 8+), основан на методе переопределения (UITraitCollection *)traitCollection

Итак, добавьте ограничения в InterfaceBuilder с вариациями для Width - Compact, например, для свойства ограничения Installed. Таким образом, Ширина - Любая будет допустима для альбомной ориентации, Ширина - Компактная - для Книжной.

Чтобы переключить ограничения в коде в зависимости от текущего размера контроллера представления, просто добавьте в свой класс UIViewController следующее:

- (UITraitCollection *)traitCollection
{
    UITraitCollection *verticalRegular = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassRegular];

    if (self.view.bounds.size.width < self.view.bounds.size.height) {
        // wCompact, hRegular
        return [UITraitCollection traitCollectionWithTraitsFromCollections:
                @[[UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact],
                  verticalRegular]];
    } else {
        // wRegular, hRegular
        return [UITraitCollection traitCollectionWithTraitsFromCollections:
                @[[UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular],
                  verticalRegular]];
    }
}
Jadamec
источник
0

Насколько ваш альбомный режим будет отличаться от портретного? Если он сильно отличается, было бы неплохо создать другой контроллер представления и загрузить его, когда устройство находится в альбомной ориентации.

Например

    if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) 
    //load landscape view controller here
MendyK
источник
Да, это один из вариантов. Но я не считаю его самым оптимальным. Я хочу сказать, что если есть возможность использовать разные черты класса размеров для портретного и ландшафтного режимов для iphone в ios8, то почему не то же самое для iPad?
neelIVP
До Xcode 6 мы могли использовать разные раскадровки для разной ориентации. Это не очень эффективно, если большинство контроллеров представления одинаковы. Но это очень удобно для разных раскладок. В Xcode 6 это невозможно. Возможно, создание другого контроллера представления для другой ориентации - единственное решение.
Bagusflyer 01
2
Загрузка разных viewController каждый раз кажется очень неэффективной, особенно если что-то происходит на экране. Гораздо лучше использовать одно и то же представление и либо манипулировать ограничением автоматического размещения, либо положением элементов в коде. Или использовать упомянутый выше прием, пока Apple не решит эту проблему.
Пахнев
0

Версия Swift 5. Работает нормально.

override func overrideTraitCollection(forChild childViewController: UIViewController) -> UITraitCollection? {
    if UIScreen.main.bounds.width > UIScreen.main.bounds.height {
        let collections = [UITraitCollection(horizontalSizeClass: .regular),
                           UITraitCollection(verticalSizeClass: .compact)]
        return UITraitCollection(traitsFrom: collections)
    }
    return super.overrideTraitCollection(forChild: childViewController)
}
Ли Цзинь
источник
-3

Код Swift 3.0 для решения @RonDiamond

class Test : UIViewController {


var _willTransitionToPortrait: Bool?
var _traitCollection_CompactRegular: UITraitCollection?
var _traitCollection_AnyAny: UITraitCollection?

func viewDidLoad() {
    super.viewDidLoad()
    self.upReferenceSizeClasses = null
}

func setUpReferenceSizeClasses() {
    var traitCollection_hCompact: UITraitCollection = UITraitCollection(horizontalSizeClass: UIUserInterfaceSizeClassCompact)
    var traitCollection_vRegular: UITraitCollection = UITraitCollection(verticalSizeClass: UIUserInterfaceSizeClassRegular)
    _traitCollection_CompactRegular = UITraitCollection(traitsFromCollections: [traitCollection_hCompact,traitCollection_vRegular])
    var traitCollection_hAny: UITraitCollection = UITraitCollection(horizontalSizeClass: UIUserInterfaceSizeClassUnspecified)
    var traitCollection_vAny: UITraitCollection = UITraitCollection(verticalSizeClass: UIUserInterfaceSizeClassUnspecified)
    _traitCollection_AnyAny = UITraitCollection(traitsFromCollections: [traitCollection_hAny,traitCollection_vAny])
}

func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    _willTransitionToPortrait = self.view.frame.size.height > self.view.frame.size.width
}

func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
    _willTransitionToPortrait = size.height > size.width
}

func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection {
    var traitCollectionForOverride: UITraitCollection = _willTransitionToPortrait ? _traitCollection_CompactRegular : _traitCollection_AnyAny
    return traitCollectionForOverride
}}
Zeeshan
источник