Kelas ukuran untuk mode potret dan lanskap iPad

97

Saya pada dasarnya ingin subview saya diposisikan secara berbeda tergantung pada orientasi iPad (Portrait atau Landscape) menggunakan Sizing Classes yang diperkenalkan di xcode 6. Saya telah menemukan banyak tutorial yang menjelaskan bagaimana berbagai kelas sizing tersedia untuk iPhone dalam portrait dan landscape di IB tetapi bagaimanapun tampaknya tidak ada yang mencakup mode lanskap atau potret individu untuk iPad di IB. Adakah yang bisa membantu?

neelIVP
sumber
Tampaknya tidak ada solusi non-programatik, yang akan dibutuhkan untuk layar Default
SwiftArchitect

Jawaban:

174

Tampaknya niat Apple untuk memperlakukan kedua orientasi iPad sebagai hal yang sama - tetapi seperti yang banyak dari kita temukan, ada alasan desain yang sangat sah untuk ingin memvariasikan tata letak UI untuk iPad Portrait vs iPad Landscape.

Sayangnya, OS saat ini tampaknya tidak memberikan dukungan untuk perbedaan ini ... yang berarti bahwa kami kembali memanipulasi batasan tata letak otomatis dalam kode atau solusi serupa untuk mencapai apa yang idealnya kami dapatkan secara gratis menggunakan Adaptive UI .

Bukan solusi yang elegan.

Tidakkah ada cara untuk memanfaatkan keajaiban yang telah dibangun Apple ke dalam IB dan UIKit untuk menggunakan kelas ukuran yang kita pilih untuk orientasi tertentu?

~

Dalam memikirkan masalah secara lebih umum, saya menyadari bahwa 'kelas ukuran' hanyalah cara untuk menangani beberapa tata letak yang disimpan di IB, sehingga dapat dipanggil sesuai kebutuhan pada waktu proses.

Faktanya, 'kelas ukuran' sebenarnya hanyalah sepasang nilai enum. Dari UIInterface.h:

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

Jadi terlepas dari apa yang Apple putuskan untuk memberi nama variasi yang berbeda ini, pada dasarnya, mereka hanyalah sepasang bilangan bulat yang digunakan sebagai pengenal unik, untuk membedakan satu tata letak dari yang lain, disimpan di IB.

Sekarang, misalkan kita membuat tata letak alternatif (menggunakan kelas ukuran yang tidak terpakai) di IB - katakanlah, untuk Potret iPad ... adakah cara agar perangkat menggunakan pilihan kelas ukuran (tata letak UI) sesuai kebutuhan saat runtime ?

Setelah mencoba beberapa pendekatan berbeda (kurang elegan) untuk masalah ini, saya menduga mungkin ada cara untuk menimpa kelas ukuran default secara terprogram. Dan ada (di 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);

Jadi, jika Anda dapat mengemas hierarki pengontrol tampilan sebagai pengontrol tampilan 'anak', dan menambahkannya ke pengontrol tampilan induk tingkat atas ... Anda dapat mengganti secara bersyarat anak tersebut dengan berpikir bahwa itu adalah kelas ukuran yang berbeda dari default dari OS.

Berikut adalah contoh implementasi yang melakukan ini, di pengontrol tampilan 'induk':

@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

Sebagai demo cepat untuk melihat apakah itu berhasil, saya menambahkan label khusus secara khusus ke versi 'Reguler / Reguler' dan 'Ringkas / Reguler' dari tata letak pengontrol anak di IB:

masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini

Dan inilah yang tampak seperti berjalan, saat iPad berada dalam kedua orientasi: masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini

Voila! Konfigurasi kelas ukuran khusus pada waktu proses.

Semoga Apple akan membuat ini tidak perlu di OS versi berikutnya. Sementara itu, ini mungkin pendekatan yang lebih elegan dan skalabel daripada mengotak-atik batasan tata letak otomatis atau melakukan manipulasi lain dalam kode secara terprogram.

~

EDIT (6/4/15): Harap diingat bahwa contoh kode di atas pada dasarnya adalah bukti konsep untuk mendemonstrasikan teknik tersebut. Jangan ragu untuk beradaptasi sesuai kebutuhan untuk aplikasi spesifik Anda sendiri.

~

EDIT (7/24/15): Sangat menyenangkan bahwa penjelasan di atas tampaknya membantu mengungkap masalah. Meskipun saya belum mengujinya, kode oleh mohamede1945 [di bawah] tampak seperti pengoptimalan yang berguna untuk tujuan praktis. Jangan ragu untuk mengujinya dan beri tahu kami pendapat Anda. (Demi kelengkapan, saya akan membiarkan kode contoh di atas apa adanya.)

RonDiamond
sumber
1
Pos hebat @RonDiamond!
amergin
3
Apple mengambil pendekatan serupa untuk mendefinisikan ulang kelas ukuran lebar di Safari, sehingga Anda dapat yakin bahwa ini adalah pendekatan yang kurang lebih didukung. Anda seharusnya tidak benar-benar membutuhkan ivar ekstra; UITraitCollectioncukup dioptimalkan dan overrideTraitCollectionForChildViewControllerdipanggil cukup jarang sehingga seharusnya tidak menjadi masalah untuk melakukan pemeriksaan lebar dan membuatnya kemudian.
zwaldowski
1
@zwaldows Terima kasih. Kode contoh di sini hanya untuk mendemonstrasikan teknik, dan jelas dapat dioptimalkan dengan berbagai cara sesuai keinginan. (Sebagai aturan umum, dengan objek yang digunakan berulang kali [misalnya, setiap kali perangkat mengubah orientasi], menurut saya bukan ide yang buruk untuk memegang sebuah objek; tetapi seperti yang Anda tunjukkan, perbedaan kinerja di sini mungkin minimal.)
RonDiamond
1
@Rashmi: Seperti yang saya singgung dalam penjelasannya, pada akhirnya tidak masalah mana yang Anda pilih - Anda hanya memetakan tata letak ke sepasang nilai enum. Jadi, Anda benar-benar dapat menggunakan "kelas ukuran" mana pun yang masuk akal bagi Anda. Saya hanya memastikan itu tidak bertentangan dengan beberapa kelas ukuran (sah) lain yang mungkin diwariskan di suatu tempat sebagai default.
RonDiamond
1
Untuk pengontrol tampilan anak, menurut saya itu tidak penting. Anda harus dapat membuat instance secara terprogram atau dari ujung pena, selama itu ditambahkan dengan benar sebagai pengontrol turunan ke penampung.
RonDiamond
41

Sebagai ringkasan dari jawaban yang sangat panjang oleh RonDiamond. Yang perlu Anda lakukan adalah di pengontrol tampilan root Anda.

Tujuan-c

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

Cepat:

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

Kemudian di storyborad gunakan compact width untuk Portrait dan Regular width untuk Landscape.

mohamede1945
sumber
Saya bertanya-tanya bagaimana hal itu akan berhasil dengan mode layar terbagi baru ... satu cara untuk mengetahuinya!
mm2001
UITraitCollection hanya untuk iOS 8.0+
mohamede1945
Tidak berfungsi untuk saya, saya memiliki tampilan dengan dua tampilan kontainer, ini perlu ditampilkan di lokasi yang berbeda tergantung pada orientasinya. Saya membuat semua kelas ukuran yang diperlukan, semuanya berfungsi untuk iPhone. Saya mencoba menggunakan kode Anda untuk memisahkan orientasi iPad juga. Namun itu hanya akan bertindak aneh pada pandangan. Itu tidak mengubahnya seperti yang seharusnya. Ada petunjuk apa yang mungkin terjadi?
NoSixties
1
Ini berhasil bagi saya ketika saya mengesampingkannya, - (UITraitCollection *)traitCollectionbukan overrideTraitCollectionForChildViewController. Juga kendala harus sesuai dengan koleksi sifat, jadi wC (hAny).
H. de Jonge
1
@Apollo Saya akan melakukannya, tetapi itu tidak akan menjawab pertanyaan yang sebenarnya. Lihat di sini untuk contoh Apple tentang cara menggunakan setOverrideTraitCollection github.com/ios8/AdaptivePhotosAnAdaptiveApplication/blob/master/…
malhal
5

IPad memiliki ciri ukuran 'biasa' untuk dimensi horizontal dan vertikal, tidak memberikan perbedaan antara potret dan lanskap.

Sifat ukuran ini dapat diganti dalam UIViewControllerkode subkelas khusus Anda , melalui metode traitCollection, misalnya:

- (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;
}

Ini memberi iPad ciri ukuran yang sama dengan iPhone 7 Plus. Perhatikan bahwa model iPhone lainnya umumnya memiliki sifat 'lebar ringkas' (bukan lebar reguler) terlepas dari orientasinya.

Meniru iPhone 7 Plus dengan cara ini memungkinkan model tersebut digunakan sebagai stand-in untuk iPad dalam Pembuat Antarmuka Xcode, yang tidak menyadari adanya penyesuaian dalam kode.

Ketahuilah bahwa Split View di iPad mungkin menggunakan ciri ukuran yang berbeda dari operasi layar penuh normal.

Jawaban ini didasarkan pada pendekatan yang diambil dalam posting blog ini , dengan beberapa perbaikan.

Pembaruan 2019-01-02: Diperbarui untuk memperbaiki bilah status tersembunyi yang terputus-putus di lanskap iPad, dan potensi menginjak-injak sifat (lebih baru) di UITraitCollection. Juga dicatat bahwa dokumentasi Apple sebenarnya merekomendasikan untuk tidak menimpa traitCollection, jadi di masa depan mungkin ada masalah dengan teknik ini.

jedwidz
sumber
Apple menetapkan bahwa traitCollectionpropertinya menjadi hanya baca: developer.apple.com/documentation/uikit/uitraitenvironment/…
cleverbit
4

Jawaban panjang dan bermanfaat oleh RonDiamond adalah awal yang baik untuk memahami prinsip-prinsip tersebut, namun kode yang berhasil untuk saya (iOS 8+) didasarkan pada metode utama (UITraitCollection *)traitCollection

Jadi, tambahkan batasan di InterfaceBuilder dengan variasi untuk Lebar - Kompak, misalnya untuk properti batasan Terpasang. Jadi Lebar - Apa pun akan valid untuk lanskap, Lebar - Ringkas untuk Potret.

Untuk mengganti batasan dalam kode berdasarkan ukuran pengontrol tampilan saat ini, cukup tambahkan yang berikut ini ke kelas UIViewController Anda:

- (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
sumber
0

Seberapa jauh perbedaan mode lanskap Anda dengan mode potret Anda? Jika sangat berbeda, mungkin ada baiknya untuk membuat pengontrol tampilan lain dan memuatnya saat perangkat dalam lanskap

Sebagai contoh

    if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) 
    //load landscape view controller here
MendyK
sumber
Ya ini salah satu pilihan. Tapi kurasa itu bukan yang paling optimal. Maksud saya adalah jika ada opsi untuk menggunakan ciri kelas ukuran yang berbeda untuk mode potret dan lansekap untuk iphone di ios8 lalu mengapa tidak sama untuk iPad?
neelIVP
Sebelum Xcode 6, kita dapat menggunakan storyboard yang berbeda untuk orientasi yang berbeda. Itu tidak terlalu efisien jika sebagian besar pengontrol tampilan sama. Tetapi sangat nyaman untuk tata letak yang berbeda. Di Xcode 6, tidak ada cara untuk melakukannya. Mungkin membuat pengontrol tampilan berbeda untuk orientasi berbeda adalah satu-satunya solusi.
Bagusflyer
2
Memuat viewController yang berbeda setiap kali tampaknya sangat tidak efisien, terutama jika ada sesuatu yang terjadi di layar. Jauh lebih baik menggunakan tampilan yang sama dan memanipulasi batasan tata letak otomatis atau posisi elemen dalam kode. Atau menggunakan peretasan yang disebutkan di atas, hingga apple mengatasi masalah ini.
Pahnev
0

Versi Swift 5. Ini bekerja dengan baik.

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)
}
Li Jin
sumber
-3

Kode Swift 3.0 untuk solusi @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
sumber