webentwicklung-frage-antwort-db.com.de

Seit Xcode 8 und iOS10 haben Ansichten in viewDidLayoutSubviews keine korrekte Größe

Es scheint, dass mit Xcode 8 auf viewDidLoad alle Viewcontroller-Subviews dieselbe Größe von 1000x1000 haben. Seltsame Sache, aber okay, viewDidLoad war noch nie der bessere Ort, um die Ansichten korrekt zu skalieren.

Aber viewDidLayoutSubviews ist! 

Und 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 beispielsweise bei einem Klick auf eine Schaltfläche anmelde, zeigt das Protokoll eine normale Größe.

Ich verwende Autolayout.

Ist es ein Fehler?

87
Martin

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

Vor dieser Funktionalität muss der Benutzer die Größe der einzelnen View-Controller manuell einstellen. Der View Controller wurde also mit einer bestimmten Größe gespeichert, die in initWithCoder verwendet wurde, um den Anfangsrahmen festzulegen.

Nun scheint es, dass initWithCoder nicht die im Storyboard definierte Größe verwendet und eine Größe von 1000x1000 px für die Viewcontroller-Ansicht und alle ihre Unteransichten definiert.

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

  • autolayout, und alle Einschränkungen werden Ihre Ansichten korrekt anordnen

  • autoresizingMask, die für jede Ansicht, für die keine Einschränkung festgelegt ist, ein Layout erstellt (note Autolayout und Randbedingungen sind jetzt in derselben Ansicht kompatibel\o /!)

Dieses ist ein Problem für alle Layout-Elemente, die sich auf die Ansichtsebene beziehen, wie cornerRadius, da weder autolayout noch autoresizing mask für Layer-Eigenschaften gelten.

Um dieses Problem zu lösen, verwenden Sie normalerweise viewDidLayoutSubviews, wenn Sie sich im Controller befinden, oder layoutSubview, wenn Sie sich in einer Ansicht befinden. An diesem Punkt (vergessen Sie nicht, ihre relativen Methoden super zu nennen), sind Sie ziemlich sicher, dass das gesamte Layout erledigt wurde!

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

1- passiert nur beim Auslegen von Zellen! In UITableViewCell & UICollectionViewCell-Unterklassen wird layoutSubview nicht genannt, nachdem Subviews korrekt angeordnet wurden.

2- Wie @EugenDimboiu bemerkte (bitte antworten Sie mit einer Antwort, falls dies für Sie nützlich ist), wird das Aufrufen von [myView layoutIfNeeded] in der nicht aufgelegten Unteransicht das Layout genau zur richtigen Zeit erstellen.

- (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 an Radar übermittelt (ID 28562874).

PS: Ich bin kein Engländer, also kann ich meinen Beitrag bearbeiten, falls meine Grammatik korrigiert werden sollte;)

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

93
Martin

Verwenden Sie abgerundete Ecken für Ihren Knopf? Rufen Sie vorher layoutIfNeeded() auf. 

39
Eugen Dimboiu

Lösung: Alles in viewDidLayoutSubviews in DispatchQueue.main.async einwickeln. 

// Swift 3

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    DispatchQueue.main.async {
        // do stuff here
    }
}
18
Derek Soike

Ich weiß, dass dies nicht Ihre genaue Frage war, aber ich hatte ein ähnliches Problem, da beim Update einige meiner Ansichten durcheinander geraten waren, obwohl in viewDidLayoutSubviews die richtige Frame-Größe verwendet wurde. Gemäß den Versionshinweisen zu iOS 10:

Msgstr "" "Das Senden von layoutIfNeeded an eine Ansicht wird nicht erwartet, um die Ansicht zu verschieben, Wenn jedoch in früheren Versionen Hätte. TranslatesAutoresizingMaskIntoConstraints auf NO gesetzt werden, und wenn Durch Einschränkungen positioniert war, würde layoutIfNeeded den view to passen Sie die Layout-Engine an, bevor Sie das Layout an die Teilstruktur senden. Diese -Änderungen korrigieren dieses Verhalten. Die Position des Empfängers und in der Regel seine Größe werden von layoutIfNeeded nicht beeinflusst.

Einige vorhandene Codes stützen sich möglicherweise auf dieses falsche Verhalten, nämlich jetzt korrigiert Es gibt keine Verhaltensänderung für vor .__ verknüpfte Binärdateien. iOS 10, aber unter iOS 10 müssen Sie möglicherweise einige .__ korrigieren. Situationen durch Senden von -layoutIfNeeded an einen Überblick über die translatesAutoresizingMaskIntoConstraints-Ansicht, die die vorherige .__ war. Empfänger, oder positionieren und dimensionieren Sie es vor (oder nach, je nach gewünschtem Verhalten) layoutIfNeeded.

Apps von Drittanbietern mit benutzerdefinierten UIView-Unterklassen, die das automatische Layout verwenden, dass Überschreiben Sie layoutSubviews und verschmutztes Layout vor dem Aufruf von super sind Gefahr laufen, eine Layout-Rückkopplungsschleife auszulösen, wenn sie auf .__ neu erstellt werden. iOS 10. Wenn sie richtig gesendet werden, werden nachfolgende Aufrufe von layoutSubviews Sie müssen sicher sein, dass Sie das schmutzige Layout auf Ihrem eigenen Rechner irgendwann beenden (beachten Sie, dass dieser Aufruf in der Version vor iOS 10 übersprungen wurde). "

Grundsätzlich können Sie nicht layoutIfNeeded für ein untergeordnetes Objekt der View aufrufen, wenn Sie translatesAutoresizingMaskIntoConstraints verwenden. Der Aufruf von layoutIfNeeded muss sich auf dem SuperView befinden, und Sie können dies immer noch in viewDidLayoutSubviews aufrufen.

18

Damit wurde 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.

3
Nerdhappy

Wenn die Frames in layoutSubViews nicht korrekt sind (was sie nicht sind), können Sie async ein wenig Code im Hauptthread ausgeben. Dies gibt dem System etwas Zeit für das Layout. Wenn der gesendete Block ausgeführt wird, haben die Frames ihre richtige Größe.

3
Emiel

Tatsächlich ist viewDidLayoutSubviews auch nicht der beste Ort, um den Rahmen Ihrer Ansicht festzulegen. Soweit ich verstanden habe, sollte ab jetzt nur noch die layoutSubviews-Methode im Code der aktuellen Ansicht verwendet werden. Ich wünschte ich wäre nicht richtig, jemand korrigiert mich bitte wenn es nicht stimmt!

2
alex_roudique

Mein Problem wurde durch Ändern der Verwendung von behoben

-(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 bis Will

Super komisch

0
Jan

Ich habe dieses Problem bereits an Apple gemeldet. Dieses Problem gibt es schon seit langer Zeit, als Sie UIViewController von Xib initialisieren. Ich fand jedoch eine ziemlich gute Lösung. Darüber hinaus fand ich dieses Problem in einigen Fällen, wenn layoutIfNeeded für UICollectionView und UITableView verwendet wurde, wenn die Datenquelle nicht im Anfangszeitpunkt festgelegt ist, und es auch erforderlich war, sie zu wechseln.

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

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
    }
}
0
mientus

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

0
Entro

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

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

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