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


97

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


Здається, не існує програмного рішення, яке було б потрібно для екрана за замовчуванням
SwiftArchitect

Відповіді:


174

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

На жаль, нинішня ОС не забезпечує підтримку цього розрізнення ... це означає, що ми знову маніпулюємо обмеженнями автоматичної компонування в коді чи подібними способами вирішення, щоб досягти того, що ми в ідеалі повинні отримати безкоштовно за допомогою адаптивного інтерфейсу користувача .

Не елегантне рішення.

Чи не існує способу використовувати магію, яку Apple вже вбудував у IB та UIKit, щоб використовувати клас розмірів, обраний нами для заданої орієнтації?

~

Розмірковуючи про проблему більш загально, я зрозумів, що "класи розмірів" - це просто способи вирішення кількох макетів, які зберігаються в ІБ, щоб їх можна було викликати за потреби під час виконання.

Насправді "клас розмірів" - це просто пара значень перерахувань. З UIInterface.h:

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

Тож незалежно від того, що Apple вирішила назвати ці різні варіанти, по суті, вони є лише парою цілих чисел, які використовуються як унікальний різновид ідентифікаторів, щоб відрізнити один макет від іншого, що зберігається в ІБ.

Тепер, припустимо, що ми створимо альтернативну компоновку (використовуючи невикористаний клас розмірів) в 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 зробить це непотрібним у наступній версії ОС. Тим часом це може бути більш елегантним і масштабованим підходом, ніж програмно возитися з обмеженнями автоматичного розміщення або робити інші маніпуляції в коді.

~

EDIT (6/4/15): Будь ласка, майте на увазі, що наведений вище зразок коду є фактично доказом концепції для демонстрації методики. Не соромтеся адаптуватися за потребою для вашої власної програми.

~

EDIT (24.07.2015): Приємно, що вищезазначене пояснення допомагає демістифікувати проблему. Поки я не перевіряв його, код mohamede1945 [нижче] виглядає як корисна оптимізація для практичних цілей. Не соромтесь перевірити це і дайте нам знати, що ви думаєте. (В інтересах повноти я залиште зразок коду вище таким, як є.)


1
Чудовий пост @RonDiamond!
амергін

3
Apple застосовує аналогічний підхід до перегляду класу розмірів ширини в Safari, тож ви можете бути впевнені, що це більш-менш підтримуваний підхід. Вам не слід справді потребувати зайвих івар; UITraitCollectionдостатньо оптимізований і overrideTraitCollectionForChildViewControllerвикликається досить рідко, що не повинно виникнути проблем робити перевірку ширини та створювати її потім.
zwaldowski

1
@zwaldowski Спасибі Зразок коду тут лише для того, щоб продемонструвати техніку, і, очевидно, її можна оптимізувати різними способами за бажанням. (Як правило, коли об’єкт багаторазово використовується знову і знову [наприклад, коли пристрій змінює орієнтації], я не думаю, що це погана ідея триматися за об'єкт; але, як ви зазначаєте, різниця в продуктивності тут може бути мінімальним.)
RonDiamond

1
@Rashmi: Як я нагадав у поясненні, це в кінцевому рахунку не має значення, який ви обираєте - ви просто відображаєте макет (и) на пару значень enum. Таким чином, ви можете реально використовувати те, що "клас розмірів" має сенс для вас. Я б просто переконався, що він не суперечить якомусь іншому (законному) класу розмірів, який може бути успадкований десь за замовчуванням.
RonDiamond

1
Що стосується контролера дитячого перегляду, я не думаю, що це має значення. Ви повинні мати можливість інстанціювати це програмно або з ручки, доки він належним чином додається як дочірній контролер до контейнера.
RonDiamond

41

Як підсумок дуже довгої відповіді RonDiamond. Все, що вам потрібно зробити - це у вашому кореневому контролері перегляду.

Ціль-с

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

Швидкий:

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

Тоді в сюжетній роботі використовуйте компактну ширину для «Портрет» та «Звичайна ширина» для пейзажу.


Мені цікаво, як це вийде з новими режимами розділеного екрана ... один спосіб дізнатися!
mm2001

UITraitCollection призначений лише для iOS
8.0+

Для мене це не працює. У мене є перегляд з двома оглядами контейнерів, їх потрібно відображати в іншому місці залежно від орієнтації. Я створив усі необхідні класи розмірів, все це працює для iPhone. Я спробував використовувати ваш код, щоб розділити орієнтацію iPad. Однак це буде просто дивно впливати на погляди. Це не змінює його так, як належить. Будь-яка підказка, що може йти?
NoSixties

1
Це працювало для мене, коли я замінював - (UITraitCollection *)traitCollectionзамість цього overrideTraitCollectionForChildViewController. Крім того, обмеження повинні відповідати характеристикам колекції, тому wC (hAny).
Х. де Йонге

1
@Apollo Я хотів би, але це не відповіло б на власне питання. Подивіться тут на зразок Apple, як використовувати setOverrideTraitCollection github.com/ios8/AdaptivePhotosAnAdaptiveApplication/blob/master/…
зловмисник

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 в Xcode's Interface Builder, який не знає про налаштування коду.

Пам’ятайте, що Split View на iPad може використовувати риси різного розміру від звичайної повноекранної роботи.

Ця відповідь ґрунтується на підході в цій публікації блогу , з деякими вдосконаленнями.

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


Apple вказує, що traitCollectionвластивість може бути лише для читання: developer.apple.com/documentation/uikit/uitraitenvironment/…
розумний шлях

4

Довга і корисна відповідь від RonDiamond - це гарний початок для розуміння принципів, проте код, який працював на мене (iOS 8+), заснований на переважаючому методі (UITraitCollection *)traitCollection

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

Щоб переключити обмеження в код на основі поточного розміру контролера перегляду, просто додайте наступне у свій клас 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]];
    }
}

0

Наскільки відрізняється ваш пейзажний режим, ніж ваш портретний режим? Якщо його дуже відрізняється, може бути хорошою ідеєю створити інший контролер перегляду та завантажити його, коли пристрій знаходиться в ландшафті

Наприклад

    if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) 
    //load landscape view controller here

Так, це один варіант. Але я не вважаю його найбільш оптимальним. Моя думка полягає в тому, що якщо є можливість використовувати різні ознаки класу розмірів для портретних і ландшафтних режимів для iphone в iios8, то чому б не однаково для iPad?
neelIVP

Перед Xcode 6 ми можемо використовувати різні розклади для різної орієнтації. Це не дуже ефективно, якщо більшість контролерів перегляду однакові. Але це дуже зручно для різних макетів. У Xcode 6 немає ніякого способу зробити це. Можливо, створення різних контролерів перегляду для різної орієнтації - єдине рішення.
Bagusflyer

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
}}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.