Seit Xcode 8 und iOS10 sind Ansichten in viewDidLayoutSubviews nicht richtig dimensioniert

94

Es scheint, dass mit viewDidLoadaktiviertem Xcode 8 alle Viewcontroller-Unteransichten dieselbe Größe von 1000 x 1000 haben. Seltsame Sache, aber okay, war viewDidLoadnoch nie der bessere Ort, um die Ansichten richtig zu dimensionieren.

Aber viewDidLayoutSubviewsist!

Bei meinem aktuellen Projekt versuche ich, die Größe einer Schaltfläche zu drucken:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

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

Das Protokoll zeigt eine Größe von (1000x1000) für myButton! Wenn ich mich dann zum Beispiel mit einer Schaltfläche anmelde, zeigt das Protokoll eine normale Größe an.

Ich benutze Autolayout.

Ist es ein Fehler?

Martin
quelle
1
Ich habe das gleiche Problem mit UIImageView - wenn ich drucke, bekomme ich einen seltsamen Frame = (0 0; 1000 1000);. Ich bin in einer UITableViewCell und sobald ich die Tabellenansicht aktualisiere, ist der Rahmen so, wie ich es erwartet habe (auch wenn die Zelle das Ansichtsfenster verlässt und wieder zurückkommt). Hat jemand eine Idee, warum dies passiert (standardmäßig seltsamer Rahmen)?
Eugen Dimboiu
4
Ich denke, die (0, 0, 1000, 1000)gebundene Initialisierung ist die neue Art und Weise, wie Xcode Ansichten von IB instanziiert. Vor Xcode8 wurden Ansichten mit ihrer konfigurierten Größe in der XIB erstellt und anschließend direkt nach dem Bildschirm in der Größe geändert. Jetzt ist im IB-Dokument jedoch keine Größe konfiguriert, da die Größe von Ihrer Geräteauswahl abhängt (am unteren Bildschirmrand). Die eigentliche Frage lautet also: Gibt es einen zuverlässigen Ort, an dem die endgültige Größe der Ansichten überprüft werden kann?
Martin
4
Verwenden Sie abgerundete Ecken für Ihren Knopf? Versuchen Sie vorher, layoutIfNeeded () aufzurufen.
Eugen Dimboiu
Interessant. Ich habe tatsächlich den Ansichtsrahmen verwendet, um einen runden Rand zu berechnen. Auch wenn es die Frage nicht beantwortet, funktioniert es. Es ist ein guter Tipp zu beachten. Vielen Dank!
Martin
Ich glaube, ich habe ähnliche Probleme beim Einrichten einer Bildschaltfläche in der rechten Ansicht eines Uitextfelds. Ich wollte die Höhe und Breite der Bildschaltfläche auf die Höhe des Textfelds einstellen, damit es sein Seitenverhältnis beibehält und aus dem Container herausfällt.
atlantach_james

Antworten:

98

Mit Interface Builder kann der Benutzer jetzt die Größe aller View Controller im Storyboard dynamisch ändern, um die Größe eines bestimmten Geräts zu simulieren.

Vor dieser Funktion sollte der Benutzer jede Größe des Ansichtscontrollers manuell festlegen. Daher wurde der Ansichts-Controller mit einer bestimmten Größe gespeichert, mit initWithCoderder der anfängliche Frame festgelegt wurde.

Nun scheint es, dass initWithCoderSie nicht die im Storyboard definierte Größe verwenden und eine Größe von 1000 x 1000 Pixel für die Viewcontroller-Ansicht und alle ihre Unteransichten definieren.

Dies ist kein Problem, da Ansichten immer eine der folgenden Layoutlösungen verwenden sollten:

  • Autolayout und alle Einschränkungen ordnen Ihre Ansichten korrekt an

  • autoresizingMask, mit der jede Ansicht, an die keine Einschränkung gebunden ist, mit einem Layout versehen wird ( beachten Sie, dass die Einschränkungen für Autolayout und Rand jetzt in derselben Ansicht kompatibel sind \ o /! ).

Aber das ist ein Problem für alle Sachen Layout der Ansicht Schicht bezogen, wie cornerRadius, da weder automatisches Layout noch die automatische Maske auf Schichteigenschaften gilt.

Um dieses Problem zu lösen, verwenden viewDidLayoutSubviewsSie üblicherweise, wenn Sie sich in der Steuerung befinden oder layoutSubviewwenn Sie sich in einer Ansicht befinden. An diesem Punkt (vergessen Sie nicht, die superrelativen Methoden aufzurufen ) sind Sie sich ziemlich sicher, dass alle Layout-Aufgaben erledigt wurden!

Ziemlich sicher? Hum ... nicht ganz, habe ich bemerkt, und deshalb habe ich diese Frage gestellt. In einigen Fällen hat die Ansicht bei dieser Methode immer noch die Größe 1000x1000. Ich denke, es gibt keine Antwort auf meine eigene Frage. Um die maximale Information darüber zu geben:

1- es passiert nur beim Auslegen von Zellen! In UITableViewCell& UICollectionViewCellUnterklassen layoutSubviewwird nicht aufgerufen, nachdem die Unteransichten korrekt angeordnet wurden.

2- Wie @EugenDimboiu bemerkte (bitte stimmen Sie seiner Antwort zu, wenn dies für Sie nützlich ist), wird das Aufrufen [myView layoutIfNeeded]der nicht angelegten Unteransicht gerade rechtzeitig korrekt angeordnet.

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

3- Meiner Meinung nach ist dies definitiv ein Fehler. Ich habe es beim Radar eingereicht (ID 28562874).

PS: Ich bin kein englischer Muttersprachler, also zögern Sie nicht, meinen Beitrag zu bearbeiten, wenn meine Grammatik korrigiert werden sollte;)

PS2: Wenn Sie eine bessere Lösung haben, schreiben Sie keine weitere Antwort. Ich werde die akzeptierte Antwort verschieben.

Martin
quelle
3
Danke, aber ich konnte das Radar mit der ID 28562874 nicht finden. Kann ich die Radar-URL haben?
Joey
Arbeitete wie ein Zauber für mich, auch für die Ebenen der Aussicht!
Hlynbech
@ Joey, ich weiß nicht, ob ich einen Link zu meinem Fehler bekommen kann. Ich habe keine direkte URL gefunden und es scheint, dass andere Benutzer meine Berichte nicht sehen können. Laut dieser SO-Antwort stackoverflow.com/a/145223/127493 scheint es der beste Weg zu sein, die Priorität eines Fehlers zu erhöhen, ein Duplikat zu erstellen .
Martin
Dies ist für Apple mehr als dumm. Ich habe einen vollständig von der automatischen Layoutansicht festgelegten Controller, und einige der Textfelder und eine UIView haben alle diesen dummen 0,0,1000,1000-Frame. Aber nicht alles. Wie kann dies der Qualitätssicherung entkommen? Ich nehme an, ich kann noch ein Radar ablegen, das sie nicht lesen werden.
Ahwulf
3
Ich möchte Ihnen und allen anderen für Ihre Erkenntnisse und Vorschläge danken. Ich verbrachte den ganzen Tag damit, mir die Haare auszureißen, weil eine UIStackViewInnenseite von a UICollectionViewCellwährenddessen nicht die richtige Höhe zurückgab viewDidLayoutSubviews. Durch einen layoutIfNeededsofortigen Anruf wurde das Problem behoben.
Ruiz
40

Verwenden Sie abgerundete Ecken für Ihren Knopf? Versuchen Sie layoutIfNeeded()vorher anzurufen .

Eugen Dimboiu
quelle
1
ahah, versuchst du mehr rep zu bekommen? Wie ich in meinem Kommentar sagte, beantwortet dies die Frage nicht. Es hat mir jedoch geholfen, so dass Sie eine +1 verdient haben :)
Martin
4
Es kann jemandem in der Zukunft helfen, und es ist einfacher, im Vergleich zu Kommentaren zu erkennen
Eugen Dimboiu
1
Hat mir gerade geholfen!
Daidai
@daidai froh das zu hören!
Eugen Dimboiu
das funktioniert! aber wieso? auch aber was ist die richtige Lösung, um die richtige Rahmengröße zu erhalten?
Crashalot
24

Lösung: Wrap alles innen viewDidLayoutSubviewsin DispatchQueue.main.async.

// swift 3

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    DispatchQueue.main.async {
        // do stuff here
    }
}
Derek Soike
quelle
1
Dies funktioniert auch, wenn es in -viewDidLoad... eine so seltsame
Problemumgehung gesteckt wird
1
Wahrscheinlich, weil das System bei viewDidLayoutSubviews nicht die Zeit hatte, das zu tun, was getan werden musste. Wenn Sie den Versand anrufen, springen Sie einen Runloop, sodass das System sein Wort beenden kann. Wenn ich recht habe, wenn Sie das gleiche Verhalten mit einem Schlaf von ein paar Millisekunden bekommen könnten
Thibaut Noah
Da alle UI-Aufgaben innerhalb des Hauptthreads ausgeführt werden müssen, danke für Ihre Lösung!
Danywarner
18

Ich weiß, dass dies nicht Ihre genaue Frage war, aber ich bin auf ein ähnliches Problem gestoßen, bei dem einige meiner Ansichten wie beim Update durcheinander gebracht wurden, obwohl in viewDidLayoutSubviews die richtige Frame-Größe vorhanden war. Laut iOS 10 Versionshinweise:

"Es wird nicht erwartet, dass durch das Senden von layoutIfNeeded an eine Ansicht die Ansicht verschoben wird. In früheren Versionen hat layoutIfNeeded die Ansicht vor dem Senden des Layouts so verschoben, dass sie mit der Layout-Engine übereinstimmt, wenn in der Ansicht" ÜbersetzenAutoresizingMaskIntoConstraints "auf" NO "gesetzt wurde und wenn sie durch Einschränkungen positioniert wurde Diese Änderungen korrigieren dieses Verhalten, und die Position des Empfängers und normalerweise seine Größe werden von layoutIfNeeded nicht beeinflusst.

Einige vorhandene Codes stützen sich möglicherweise auf dieses falsche Verhalten, das jetzt korrigiert wird. Es gibt keine Verhaltensänderung für Binärdateien, die vor iOS 10 verknüpft wurden. Wenn Sie jedoch auf iOS 10 aufbauen, müssen Sie möglicherweise einige Situationen korrigieren, indem Sie -layoutIfNeeded an eine Übersicht der Ansicht translatesAutoresizingMaskIntoConstraints senden, die der vorherige Empfänger war, oder sie zuvor positionieren und dimensionieren ( oder danach, je nach gewünschtem Verhalten) layoutIfNeeded.

Bei Apps von Drittanbietern mit benutzerdefinierten UIView-Unterklassen, die das automatische Layout verwenden und layoutSubviews und das schmutzige Layout vor dem Aufruf von super überschreiben, besteht die Gefahr, dass beim Neuerstellen unter iOS 10 eine Layout-Rückkopplungsschleife ausgelöst wird. Wenn sie nachfolgende LayoutSubviews-Aufrufe korrekt gesendet werden, müssen sie dies unbedingt tun Hören Sie irgendwann auf, das Layout auf sich selbst zu verschmutzen (beachten Sie, dass dieser Aufruf in der Version vor iOS 10 übersprungen wurde). "

Grundsätzlich können Sie layoutIfNeeded nicht für ein untergeordnetes Objekt der Ansicht aufrufen, wenn Sie translatesAutoresizingMaskIntoConstraints verwenden. Jetzt muss der Aufruf von layoutIfNeeded in der SuperView erfolgen, und Sie können dies weiterhin in viewDidLayoutSubviews aufrufen.

HannahCarney
quelle
5

Wenn die Frames in layoutSubViews nicht korrekt sind (was sie nicht sind), können Sie ein bisschen Code im Hauptthread asynchron versenden. Dies gibt dem System einige Zeit, um das Layout zu erstellen. Wenn der von Ihnen versendete Block ausgeführt wird, haben die Frames die richtige Größe.

Emiel
quelle
3

Dies hat das (lächerlich nervige) Problem für mich behoben:

- (void) viewDidLayoutSubviews {

    [super viewDidLayoutSubviews];

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

}

Bearbeiten / Hinweis: Dies ist für einen ViewController im Vollbildmodus.

Nerdhappy
quelle
Die Verwendung von mainScreen-Grenzen ist keine gute Lösung, da der viewController in vielen Fällen nicht den gesamten Bildschirmbereich einnimmt. Darüber hinaus sollten Sie [super viewDidLayoutSubviews];diese Methode aufrufen , da viele Autolayout-Aufgaben von der Ansicht selbst ausgeführt werden
Martin
@ Martin was empfehlen Sie? Einverstanden, dass dies nicht ideal erscheint.
Crashalot
@Crashalot, wie ich in meiner Antwort sage, würde die Verwendung von Autolayout oder AutoresizingMask Ihr UIViews korrekt gestalten . Wenn Sie jedoch eine spezielle Berechnung für einen bestimmten Ansichtsrahmen durchführen müssen, funktioniert Eugens Antwort: Rufen layoutIfNeededSie ihn auf. Ich habe das Gefühl, dass dies nicht die beste Lösung ist, aber ich habe immer noch keine bessere gefunden.
Martin
2

Eigentlich viewDidLayoutSubviewsist auch nicht der beste Ort, um den Rahmen Ihrer Ansicht festzulegen. Soweit ich verstanden habe, sollte von nun an nur noch die layoutSubviewsMethode im Code der eigentlichen Ansicht durchgeführt werden. Ich wünschte ich hätte nicht recht, jemand korrigiert mich bitte wenn es nicht wahr ist!

alex_roudique
quelle
Danke für deine Antwort. Die Apple-Dokumentation viewDidLayoutSubviewsist ziemlich zweideutig. Der zweite Satz in "Diskussionen" widerspricht in gewisser Weise dem letzten. developer.apple.com/reference/uikit/uiviewcontroller/…
Martin
0

Ich habe dieses Problem bereits an Apple gemeldet. Dieses Problem besteht seit langer Zeit, als Sie UIViewController von Xib aus initialisieren, aber ich habe eine gute Lösung gefunden. Darüber hinaus habe ich dieses Problem in einigen Fällen festgestellt, wenn layoutIfNeeded in UICollectionView und UITableView verwendet wurde, wenn die Datenquelle nicht im ersten Moment festgelegt wurde, und es auch benötigt wurde, um es zu ändern.

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

Versand einmalige Verlängerung:

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

Swizzle-Erweiterung:

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
quelle
0

Mein Problem wurde durch Ändern der Verwendung von gelöst

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

zu

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

Also von Did zu Will

Super komisch

Jan.
quelle
0

Bessere Lösung für mich.

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_)
    }
}

Verwenden von

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

myButton.add(layoutComplement: CircleShapeComplement())
bizhara
quelle
0

Überschreiben Sie layoutSublayers (von Layer: CALayer) anstelle von layoutSubviews in der Zellenunteransicht, um korrekte Frames zu erhalten

Entro
quelle
0

Wenn Sie etwas basierend auf dem Rahmen Ihrer Ansicht tun müssen, überschreiben Sie layoutSubviews und rufen Sie layoutIfNeeded auf

    override func layoutSubviews() {
    super.layoutSubviews()

    yourView.layoutIfNeeded()
    setGradientForYourView()
}

Ich hatte das Problem, dass viewDidLayoutSubviews den falschen Frame für meine Ansicht zurückgab , für den ich einen Farbverlauf hinzufügen musste. Und nur layoutIfNeeded hat das Richtige getan :)

Vitya Shurapov
quelle
-1

Laut neuem Update in ios ist dies tatsächlich ein Fehler, aber wir können dies reduzieren, indem wir -

Wenn Sie xib mit Autolayout in Ihrem Projekt verwenden, müssen Sie nur den Frame in der Autolayout-Einstellung aktualisieren. Bitte finden Sie ein Bild dafür.Geben Sie hier die Bildbeschreibung ein

Neha Mishra
quelle