Sejak Xcode 8 dan iOS10, tampilan tidak diukur dengan benar di viewDidLayoutSubviews

95

Tampaknya dengan Xcode 8, pada viewDidLoad, semua subview viewcontroller memiliki ukuran yang sama yaitu 1000x1000. Hal yang aneh, tapi oke, viewDidLoadbelum pernah menjadi tempat yang lebih baik untuk mengukur tampilan dengan benar.

Tapi viewDidLayoutSubviewsapakah!

Dan pada proyek saya saat ini, saya mencoba mencetak ukuran tombol:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    NSLog(@"%@", self.myButton);
}

Log menunjukkan ukuran (1000x1000) untuk myButton! Kemudian jika saya masuk klik tombol, misalnya, log menunjukkan ukuran normal.

Saya menggunakan autolayout.

Apakah ini bug?

Martin
sumber
1
Memiliki masalah yang sama dengan UIImageView - ketika saya mencetak saya mendapatkan frame aneh = (0 0; 1000 1000) ;. Saya berada di dalam UITableViewCell, dan begitu saya menyegarkan tampilan tabel, bingkai itu seperti yang saya harapkan (juga saat sel keluar dari viewport dan kembali lagi). Adakah yang tahu mengapa ini terjadi (bingkai aneh secara default)?
Eugen Dimboiu
4
Saya pikir (0, 0, 1000, 1000)inisialisasi terikat adalah cara baru Xcode instanciates views dari IB. Sebelum Xcode8, tampilan dibuat dengan ukuran yang dikonfigurasi di xib, kemudian diubah ukurannya sesuai layar setelahnya. Tetapi sekarang, tidak ada ukuran yang dikonfigurasi dalam dokumen IB karena ukurannya tergantung pada pilihan perangkat Anda (di bagian bawah layar). Jadi pertanyaan sebenarnya adalah: adakah tempat yang dapat diandalkan di mana ukuran akhir tampilan dapat diperiksa?
Martin
4
apakah Anda menggunakan sudut membulat untuk tombol Anda? Coba panggil layoutIfNeeded () sebelumnya.
Eugen Dimboiu
Menarik. Saya memang menggunakan bingkai tampilan untuk menghitung batas bulat. Sekalipun tidak menjawab pertanyaan, itu berhasil. Ini tip yang bagus untuk diingat. Terima kasih!
Martin
Saya rasa saya mengalami masalah serupa saat menyiapkan tombol gambar di dalam tampilan kanan bidang uitext. Saya ingin menyetel tinggi dan lebar tombol gambar ke tinggi bidang teks sehingga rasio aspeknya dipertahankan dan keluar dari wadahnya.
atlantach_james

Jawaban:

98

Sekarang, Interface Builder memungkinkan pengguna mengubah ukuran setiap pengontrol tampilan di storyboard secara dinamis, untuk mensimulasikan ukuran perangkat tertentu.

Sebelum fungsionalitas ini, pengguna harus mengatur secara manual setiap ukuran pengontrol tampilan. Jadi pengontrol tampilan disimpan dengan ukuran tertentu, yang digunakan initWithCoderuntuk mengatur frame awal.

Sekarang, tampaknya initWithCodertidak menggunakan ukuran yang ditentukan di storyboard, dan menentukan ukuran 1000x1000 px untuk tampilan viewcontroller & semua subviewnya.

Ini bukan masalah, karena tampilan harus selalu menggunakan salah satu dari solusi tata letak berikut:

  • autolayout, dan semua batasan akan mengatur tampilan Anda dengan benar

  • autoresizingMask, yang akan mengatur tata letak setiap tampilan yang tidak memiliki batasan apa pun yang dilampirkan ( catatan autolayout dan batasan margin sekarang kompatibel dalam tampilan yang sama \ o /! )

Tapi ini dia masalah untuk semua hal tata letak yang terkait dengan lapisan tampilan, seperti cornerRadius, karena tata letak otomatis maupun topeng otomatis tidak berlaku untuk properti lapisan.

Untuk menjawab masalah ini, cara yang umum adalah menggunakan viewDidLayoutSubviewsjika Anda berada di controller, atau layoutSubviewjika Anda dalam tampilan. Pada titik ini (jangan lupa untuk memanggil supermetode relatifnya), Anda cukup yakin bahwa semua tata letak telah selesai!

Cukup yakin? Hum ... tidak sepenuhnya, saya telah berkomentar, dan itulah mengapa saya menanyakan pertanyaan ini, dalam beberapa kasus tampilan masih memiliki ukuran 1000x1000 pada metode ini. Saya pikir tidak ada jawaban untuk pertanyaan saya sendiri. Untuk memberikan informasi yang maksimal tentangnya:

1- Itu terjadi hanya saat meletakkan sel! Dalam UITableViewCell& UICollectionViewCellsubkelas,layoutSubview tidak akan dipanggil setelah tampilan ditata dengan benar.

2- Seperti yang dikatakan @EugenDimboiu (tolong beri suara positif pada jawabannya jika berguna untuk Anda), menelepon [myView layoutIfNeeded] subview yang tidak ditata akan mengaturnya dengan benar tepat pada waktunya.

- (void)layoutSubviews {
    [super layoutSubviews];
    NSLog (self.myLabel); // 1000x1000 size 
    [self.myLabel layoutIfNeeded];
    NSLog (self.myLabel); // normal size
}

3- Menurut saya, ini pasti bug. Saya telah mengirimkannya ke radar (id 28562874).

PS: Saya bukan penutur asli bahasa Inggris, jadi silakan mengedit posting saya jika tata bahasa saya harus diperbaiki;)

PS2: Jika ada solusi yang lebih baik, jangan ragu untuk menulis jawaban lain. Saya akan memindahkan jawaban yang diterima.

Martin
sumber
3
thx, tetapi saya tidak dapat menemukan radar dengan id 28562874, dapatkah saya mendapatkan URL radar?
Joey
Bekerja seperti pesona bagi saya, juga untuk lapisan tampilan!
hlynbech
@Joey, saya tidak tahu apakah saya bisa mendapatkan tautan dari bug saya. Saya tidak menemukan URL langsung apa pun, dan tampaknya pengguna lain tidak dapat melihat laporan saya. Menurut jawaban SO stackoverflow.com/a/145223/127493 ini , tampaknya cara terbaik untuk meningkatkan prioritas bug adalah dengan membuat duplikat.
Martin
Ini sangat bodoh di pihak Apple. Saya memiliki sepenuhnya ditentukan oleh pengontrol tampilan tata letak otomatis, dan beberapa bidang teks dan UIView semuanya memiliki bingkai 0,0,1000,1000 bodoh ini. Tapi tidak semua. Bagaimana ini bisa lolos dari QA? Saya kira saya dapat mengajukan Radar lain yang tidak akan mereka baca.
ahwulf
3
Saya ingin berterima kasih kepada Anda dan semua orang atas wawasan dan saran Anda. Saya menghabiskan sepanjang hari mencabut rambut saya karena bagian UIStackViewdalam a UICollectionViewCelltidak mengembalikan tinggi yang benar selama viewDidLayoutSubviews. Menelepon layoutIfNeededsegera memperbaiki masalah.
Ruiz
40

Apakah Anda menggunakan sudut membulat untuk tombol Anda? Coba telepon layoutIfNeeded()sebelumnya.

Eugen Dimboiu
sumber
1
ahah, mencoba mendapatkan lebih banyak reputasi? Seperti yang saya katakan dalam komentar saya, ini tidak menjawab pertanyaan itu. Namun, itu membantu saya sehingga Anda mendapatkan +1 :)
Martin
4
Ini dapat membantu seseorang di masa depan, dan lebih mudah dikenali dibandingkan dengan komentar
Eugen Dimboiu
1
Bantu aku sekarang!
daidai
@daidai senang mendengarnya!
Eugen Dimboiu
ini bekerja! tapi kenapa? juga tetapi apa solusi yang tepat untuk mendapatkan ukuran bingkai yang tepat?
Crashalot
25

Solusi: Bungkus semua yang ada viewDidLayoutSubviewsdi dalamnya DispatchQueue.main.async.

// swift 3

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    DispatchQueue.main.async {
        // do stuff here
    }
}
Derek Soike
sumber
1
ini bekerja bahkan jika dimasukkan ke dalam -viewDidLoad... solusi yang aneh
medvedNick
1
Mungkin karena ketika viewDidLayoutSubviews, sistem tidak memiliki waktu untuk melakukan apa yang perlu dilakukan. Calling dispatch membuat Anda melompat satu runloop, memungkinkan sistem menyelesaikan kata-katanya. Jika saya benar ketika Anda bisa mendapatkan perilaku yang sama dengan tidur beberapa milidetik
thibaut noah
karena semua tugas UI harus dilakukan dalam utas utama, terima kasih atas solusinya!
danywarner
18

Saya tahu ini bukan pertanyaan persis Anda, tetapi saya mengalami masalah serupa di mana pada pembaruan beberapa pandangan saya kacau meskipun memiliki ukuran bingkai yang benar di viewDidLayoutSubviews. Menurut catatan Rilis iOS 10:

"Mengirim layoutIfNeeded ke tampilan tidak diharapkan untuk memindahkan tampilan, tetapi dalam rilis sebelumnya, jika tampilan telah translatesAutoresizingMaskIntoConstraints NO, dan jika diposisikan oleh batasan, layoutIfNeeded akan memindahkan tampilan agar sesuai dengan mesin layout sebelum mengirim layout ke subtree. Perubahan ini memperbaiki perilaku ini, dan posisi penerima serta biasanya ukurannya tidak akan terpengaruh oleh layoutIfNeeded.

Beberapa kode yang ada mungkin mengandalkan perilaku salah ini yang sekarang diperbaiki. Tidak ada perubahan perilaku untuk biner yang ditautkan sebelum iOS 10, tetapi saat membangun di iOS 10 Anda mungkin perlu memperbaiki beberapa situasi dengan mengirim -layoutIfNeeded ke superview tampilan translatesAutoresizingMaskIntoConstraints yang merupakan penerima sebelumnya, atau memposisikan dan mengukurnya sebelumnya ( atau setelahnya, tergantung pada perilaku yang Anda inginkan) layoutIfNeeded.

Aplikasi pihak ketiga dengan subkelas UIView khusus menggunakan Tata Letak Otomatis yang menimpa layoutSubviews dan tata letak kotor pada dirinya sendiri sebelum memanggil super berisiko memicu loop umpan balik tata letak saat dibuat ulang di iOS 10. Saat dikirim dengan benar panggilan layoutSubviews berikutnya, mereka harus yakin untuk berhenti mengotori tata letak pada diri sendiri di beberapa titik (perhatikan bahwa panggilan ini dilewati pada rilis sebelum iOS 10). "

Pada dasarnya, Anda tidak bisa memanggil layoutIfNeeded pada objek turunan View jika Anda menggunakan translatesAutoresizingMaskIntoConstraints - sekarang memanggil layoutIfNeeded harus ada di superView, dan Anda masih bisa memanggilnya di viewDidLayoutSubviews.

HannahCarney
sumber
5

Jika frame tidak benar dalam layoutSubViews (padahal sebenarnya tidak), Anda dapat mengirimkan sedikit kode asinkron pada thread utama. Ini memberi sistem waktu untuk melakukan tata letak. Ketika blok yang Anda kirim dijalankan, bingkai memiliki ukuran yang sesuai.

Emiel
sumber
3

Ini memperbaiki masalah (sangat menjengkelkan) untuk saya:

- (void) viewDidLayoutSubviews {

    [super viewDidLayoutSubviews];

    self.view.frame = CGRectMake(0,0,[[UIScreen mainScreen] bounds].size.width,[[UIScreen mainScreen] bounds].size.height);

}

Edit / Catatan: Ini untuk ViewController layar penuh.

Nerdhappy
sumber
Menggunakan batas mainScreen bukanlah solusi yang baik karena dalam banyak kasus viewController tidak mengambil seluruh ruang layar. Selain itu, Anda harus memanggil [super viewDidLayoutSubviews];metode ini karena banyak hal tata letak otomatis dilakukan oleh tampilan itu sendiri
Martin
@Martin apa yang Anda rekomendasikan? Setuju ini sepertinya tidak ideal.
Crashalot
@Crashalot seperti yang saya katakan dalam jawaban saya, menggunakan autolayout atau autoresizingMask akan mengatur tata letak Anda dengan benar UIView. Tetapi jika Anda harus melakukan perhitungan khusus pada bingkai tampilan tertentu, jawaban Eugen berfungsi: panggil layoutIfNeeded. Saya merasa ini bukan solusi terbaik, tetapi saya masih belum menemukan yang lebih baik.
Martin
2

Sebenarnya viewDidLayoutSubviewsjuga bukan tempat terbaik untuk mengatur bingkai pandangan Anda. Sejauh yang saya mengerti, mulai sekarang satu-satunya tempat yang harus dilakukan adalah layoutSubviewsmetode dalam kode tampilan aktual. Saya berharap saya tidak benar, seseorang tolong koreksi saya jika itu tidak benar!

alex_roudique
sumber
Terima kasih atas jawaban anda. Dokumentasi Apple tentang viewDidLayoutSubviewscukup ambigu. Kalimat kedua dalam "diskusi" memiliki kontradiksi dengan kalimat terakhir. developer.apple.com/reference/uikit/uiviewcontroller/…
Martin
0

Saya sudah melaporkan masalah ini ke Apple, masalah ini sudah ada sejak lama, ketika Anda menginisialisasi UIViewController dari Xib, tetapi saya menemukan solusi yang cukup bagus. Selain itu saya menemukan masalah itu dalam beberapa kasus ketika layoutIfNeeded di UICollectionView dan UITableView ketika sumber data tidak disetel di saat awal, dan diperlukan juga swizzle.

extension UIViewController {
    open override class func initialize() {
        if self !== UIViewController.self {
            return
        }
        DispatchQueue.once(token: "io.inspace.uiviewcontroller.swizzle") {
            ins_applyFixToViewFrameWhenLoadingFromNib()
        }
    }

    @objc func ins_setView(view: UIView!) {
        // View is loaded from xib file
        if nibBundle != nil && storyboard == nil && !view.frame.equalTo(UIScreen.main.bounds) {
            view.frame = UIScreen.main.bounds
            view.layoutIfNeeded()
        }
        ins_setView(view: view)
    }

    private class func ins_applyFixToViewFrameWhenLoadingFromNib() {
        UIViewController.swizzle(originalSelector: #selector(setter: UIViewController.view),
                                 with: #selector(UIViewController.ins_setView(view:)))
        UICollectionView.swizzle(originalSelector: #selector(UICollectionView.layoutSubviews),
                                 with: #selector(UICollectionView.ins_layoutSubviews))
        UITableView.swizzle(originalSelector: #selector(UITableView.layoutSubviews),
                                 with: #selector(UITableView.ins_layoutSubviews))
     }
}

extension UITableView {
    @objc fileprivate func ins_layoutSubviews() {
        if dataSource == nil {
            super.layoutSubviews()
        } else {
            ins_layoutSubviews()
        }
    }
}

extension UICollectionView {
    @objc fileprivate func ins_layoutSubviews() {
        if dataSource == nil {
            super.layoutSubviews()
        } else {
            ins_layoutSubviews()
        }
    }
}

Pengiriman setelah perpanjangan:

extension DispatchQueue {

    private static var _onceTracker = [String]()

    /**
     Executes a block of code, associated with a unique token, only once.  The code is thread safe and will
     only execute the code once even in the presence of multithreaded calls.

     - parameter token: A unique reverse DNS style name such as com.vectorform.<name> or a GUID
     - parameter block: Block to execute once
     */
    public class func once(token: String, block: (Void) -> Void) {
        objc_sync_enter(self); defer { objc_sync_exit(self) }

        if _onceTracker.contains(token) {
            return
        }

        _onceTracker.append(token)
        block()
    }
}

Ekstensi Swizzle:

extension NSObject {
    @discardableResult
    class func swizzle(originalSelector: Selector, with selector: Selector) -> Bool {

        var originalMethod: Method?
        var swizzledMethod: Method?

        originalMethod = class_getInstanceMethod(self, originalSelector)
        swizzledMethod = class_getInstanceMethod(self, selector)

        if originalMethod != nil && swizzledMethod != nil {
            method_exchangeImplementations(originalMethod!, swizzledMethod!)
            return true
        }
        return false
    }
}
Michal Zaborowski
sumber
0

Masalah saya diselesaikan dengan mengubah penggunaan dari

-(void)viewDidLayoutSubviews{
    [super viewDidLayoutSubviews];
    self.viewLoginMailTop.constant = -self.viewLoginMail.bounds.size.height;
}

untuk

-(void)viewWillLayoutSubviews{
    [super viewWillLayoutSubviews];
    self.viewLoginMailTop.constant = -self.viewLoginMail.bounds.size.height;
}

Jadi, dari Did to Will

Sangat aneh

Jan
sumber
0

Solusi yang lebih baik untuk saya.

protocol LayoutComplementProtocol {
    func didLayoutSubviews(with targetView_: UIView)
}

private class LayoutCaptureView: UIView {
    var targetView: UIView!
    var layoutComplements: [LayoutComplementProtocol] = []

    override func layoutSubviews() {
        super.layoutSubviews()

        for layoutComplement in self.layoutComplements {
            layoutComplement.didLayoutSubviews(with: self.targetView)
        }
    }
}

extension UIView {
    func add(layoutComplement layoutComplement_: LayoutComplementProtocol) {
        func findLayoutCapture() -> LayoutCaptureView {
            for subView in self.subviews {
                if subView is LayoutCaptureView {
                    return subView as? LayoutCaptureView
                }
            }
            let layoutCapture = LayoutCaptureView(frame: CGRect(x: -100, y: -100, width: 10, height: 10)) // not want to show, want to have size
            layoutCapture.targetView = self
            self.addSubview(layoutCapture)
            return layoutCapture
        }

        let layoutCapture = findLayoutCapture()
        layoutCapture.layoutComplements.append(layoutComplement_)
    }
}

Menggunakan

class CircleShapeComplement: LayoutComplementProtocol {
    func didLayoutSubviews(with targetView_: UIView) {
        targetView_.layer.cornerRadius = targetView_.frame.size.height / 2
    }
}

myButton.add(layoutComplement: CircleShapeComplement())
Bizhara
sumber
0

Ganti layoutSublayers (dari layer: CALayer) alih-alih layoutSubviews di subview sel untuk memiliki bingkai yang benar

Entro
sumber
0

Jika Anda perlu melakukan sesuatu berdasarkan bingkai tampilan Anda - timpa layoutSubviews dan panggil layoutIfNeeded

    override func layoutSubviews() {
    super.layoutSubviews()

    yourView.layoutIfNeeded()
    setGradientForYourView()
}

Saya memiliki masalah dengan viewDidLayoutSubviews mengembalikan bingkai yang salah untuk tampilan saya, yang mana saya perlu menambahkan gradien. Dan hanya layoutIfNeeded yang melakukan hal yang benar :)

Vitya Shurapov
sumber
-1

Sesuai pembaruan baru di ios ini sebenarnya adalah bug tetapi kami dapat mengurangi ini dengan menggunakan -

Jika Anda menggunakan xib dengan autolayout dalam proyek Anda, maka Anda hanya perlu memperbarui frame dalam pengaturan autolayout, cari gambar untuk ini.masukkan deskripsi gambar di sini

neha mishra
sumber