A partire da Xcode 8 e iOS10, le visualizzazioni non vengono ridimensionate correttamente su viewDidLayoutSubviews


94

Sembra che con Xcode 8, viewDidLoadtutte le sottoview del viewcontroller abbiano la stessa dimensione di 1000x1000. Cosa strana, ma va bene, viewDidLoadnon è mai stato il posto migliore per dimensionare correttamente le viste.

Ma lo viewDidLayoutSubviewsè!

E nel mio progetto attuale, provo a stampare le dimensioni di un pulsante:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

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

Il registro mostra una dimensione di (1000x1000) per myButton! Quindi se accedo a un pulsante clic, ad esempio, il registro mostra una dimensione normale.

Sto usando il layout automatico.

E 'un errore?


1
Avendo lo stesso problema con UIImageView - quando stampo ottengo un frame strano = (0 0; 1000 1000) ;. Sono all'interno di un UITableViewCell, e una volta aggiornato il tableview il frame è quello che mi aspetto che sia (anche quando la cella esce dal viewport e torna di nuovo). Qualcuno ha idea del motivo per cui questo sta accadendo (frame strano per impostazione predefinita)?
Eugen Dimboiu

4
Penso che l' (0, 0, 1000, 1000)inizializzazione associata sia il nuovo modo in cui Xcode instancia le viste da IB. Prima di Xcode8, le viste venivano create con le dimensioni configurate in xib, quindi ridimensionate in base allo schermo subito dopo. Ma ora, non ci sono dimensioni configurate nel documento IB poiché le dimensioni dipendono dalla selezione del dispositivo (nella parte inferiore dello schermo). Quindi la vera domanda è: esiste un luogo affidabile in cui è possibile controllare la dimensione finale delle viste?
Martin,

4
stai usando angoli arrotondati per il tuo pulsante? Prova a chiamare layoutIfNeeded () prima.
Eugen Dimboiu

Interessante. In effetti stavo usando la cornice della vista per calcolare un bordo rotondo. Anche se non risponde alla domanda, funziona. È un buon consiglio da tenere a mente. Grazie!
Martin

Penso di avere problemi simili impostando un pulsante immagine all'interno della vista destra di un uitextfield. Volevo impostare l'altezza e la larghezza del pulsante dell'immagine all'altezza del campo di testo in modo da mantenere le sue proporzioni e ricadere fuori dal contenitore.
atlantach_james

Risposte:


98

Ora, Interface Builder consente all'utente di modificare dinamicamente la dimensione di ogni controller di visualizzazione nello storyboard, per simulare le dimensioni di un determinato dispositivo.

Prima di questa funzionalità, l'utente deve impostare manualmente ogni dimensione del controller di visualizzazione. Quindi il controller della vista è stato salvato con una certa dimensione, che è stata utilizzata initWithCoderper impostare il frame iniziale.

Ora, sembra che initWithCodernon usi la dimensione definita nello storyboard e definisci una dimensione di 1000x1000 px per la vista del viewcontroller e tutte le sue sottoview.

Questo non è un problema, perché le viste dovrebbero sempre utilizzare una di queste soluzioni di layout:

  • autolayout, e tutti i vincoli imposteranno correttamente le tue viste

  • autoresizingMask, che imposterà ogni vista a cui non è associato alcun vincolo ( nota che il layout automatico e i vincoli di margine sono ora compatibili nella stessa vista \ o /! )

Ma questo è un problema per tutte le cose di layout relative al livello di visualizzazione, come cornerRadius, poiché né il layout automatico né la maschera di ridimensionamento automatico si applicano alle proprietà del livello.

Per rispondere a questo problema, il modo comune è usare viewDidLayoutSubviewsse sei nel controller o layoutSubviewse sei in una vista. A questo punto (non dimenticare di chiamare i loro supermetodi relativi), sei abbastanza sicuro che tutte le cose di layout siano state fatte!

Abbastanza sicuro? Hum ... non del tutto, ho osservato, ed è per questo che ho posto questa domanda, in alcuni casi la vista ha ancora la sua dimensione 1000x1000 su questo metodo. Penso che non ci sia risposta alla mia domanda. Per dare il massimo delle informazioni a riguardo:

1- succede solo quando disponi le celle! Nelle UITableViewCell& UICollectionViewCellsottoclassi, layoutSubviewnon verrà chiamato dopo che le sottoviste sarebbero state disposte correttamente.

2- Come ha osservato @EugenDimboiu (si prega di votare la sua risposta se utile per te), invocando [myView layoutIfNeeded]la sottoview non strutturata la impaginerà correttamente appena in tempo.

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

3- A mio parere, questo è sicuramente un bug. L'ho inviato al radar (id 28562874).

PS: non sono di madrelingua inglese, quindi sentiti libero di modificare il mio post se la mia grammatica deve essere corretta;)

PS2: Se hai una soluzione migliore, sentiti libero di non scrivere un'altra risposta. Sposterò la risposta accettata.


3
grazie, ma non sono riuscito a trovare il radar con l'id 28562874, posso avere l'URL del radar?
Joey

Ha funzionato come un fascino per me, anche per gli strati della vista!
hlynbech

@ Joey, non so se riesco a ottenere un link del mio bug. Non ho trovato alcun URL diretto e sembra che gli altri utenti non possano vedere i miei rapporti. Secondo questa risposta SO stackoverflow.com/a/145223/127493 , sembra che il modo migliore per aumentare la priorità di un bug sia creare un duplicato.
Martin

Questo è più che stupido da parte di Apple. Ho un controller di visualizzazione layout automatico completamente specificato e molti dei campi di testo e un UIView hanno tutti questo stupido frame 0,0,1000,1000. Ma non tutto. Come può sfuggire al controllo di qualità? Suppongo di poter archiviare ancora un altro Radar che non leggeranno.
ahwulf

3
Vorrei ringraziare te e tutti gli altri per i tuoi suggerimenti e suggerimenti. Ho passato tutto il giorno a strapparmi i capelli perché l' UIStackViewinterno di a UICollectionViewCellnon tornava all'altezza corretta durante viewDidLayoutSubviews. Chiamare layoutIfNeededimmediatamente ha risolto il problema.
Ruiz

40

Stai usando angoli arrotondati per il tuo pulsante? Prova a chiamare layoutIfNeeded()prima.


1
ahah, stai cercando di ottenere più ripetizioni? Come ho detto nel mio commento, questo non risponde alla domanda. Tuttavia, mi ha aiutato quindi hai guadagnato un +1 :)
Martin

4
Potrebbe aiutare qualcuno in futuro, ed è più facile individuare il confronto con i commenti
Eugen Dimboiu,

1
Mi hai aiutato solo ora!
daidai

@daidai felice di sentirlo!
Eugen Dimboiu

questo funziona! ma perché? inoltre ma qual è la soluzione giusta per ottenere le dimensioni corrette del telaio?
Crashalot

24

Soluzione: Wrap tutto dentro viewDidLayoutSubviewsa DispatchQueue.main.async.

// swift 3

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    DispatchQueue.main.async {
        // do stuff here
    }
}

1
questo funziona anche se messo in -viewDidLoad... una soluzione così strana
medvedNick

1
Probabilmente perché quando viewDidLayoutSubviews il sistema non aveva avuto il tempo di fare ciò che doveva essere fatto. Chiamare dispatch ti fa saltare un runloop, permettendo al sistema di finire la sua parola. Se ho ragione quando potresti ottenere lo stesso comportamento con un sonno di pochi millisecondi
thibaut noah

perché tutte le attività dell'interfaccia utente devono essere eseguite all'interno del thread principale, grazie per la tua soluzione!
danywarner

18

So che questa non era la tua domanda esatta, ma mi sono imbattuto in un problema simile in cui, durante l'aggiornamento, alcune delle mie visualizzazioni erano incasinate nonostante avessero la dimensione del frame corretta in viewDidLayoutSubviews. Secondo le note di rilascio di iOS 10:

"L'invio di layoutIfNeeded a una vista non dovrebbe spostare la vista, ma nelle versioni precedenti, se la vista aveva translatesAutoresizingMaskIntoConstraints impostata su NO e se era posizionata da vincoli, layoutIfNeeded sposterebbe la vista in modo che corrisponda al motore di layout prima di inviare il layout Questi cambiamenti correggono questo comportamento e la posizione del ricevitore e di solito le sue dimensioni non saranno influenzate da layoutIfNeeded.

È possibile che del codice esistente si basi su questo comportamento non corretto che ora viene corretto. Non vi è alcun cambiamento di comportamento per i binari collegati prima di iOS 10, ma quando si costruisce su iOS 10 potrebbe essere necessario correggere alcune situazioni inviando -layoutIfNeeded a una superview della vista translatesAutoresizingMaskIntoConstraints che era il ricevitore precedente, oppure posizionandolo e dimensionandolo prima ( o dopo, a seconda del comportamento desiderato) layoutIfNeeded.

Le app di terze parti con sottoclassi UIView personalizzate che utilizzano il layout automatico che sovrascrivono layoutSottoviews e layout sporco su se stessi prima di chiamare super rischiano di attivare un ciclo di feedback del layout quando vengono ricostruiti su iOS 10. Quando vengono inviate correttamente, le chiamate successive di layoutSubviews devono assicurarsi di farlo smettere di sporcare il layout su se stessi ad un certo punto (si noti che questa chiamata è stata saltata nella versione precedente a iOS 10). "

Essenzialmente non puoi chiamare layoutIfNeeded su un oggetto figlio di View se stai usando translatesAutoresizingMaskIntoConstraints - ora la chiamata layoutIfNeeded deve essere su superView e puoi ancora chiamarla in viewDidLayoutSubviews.


5

Se i frame non sono corretti in layoutSubViews (cosa che non sono) puoi inviare un po 'di codice asincrono sul thread principale. Questo dà al sistema un po 'di tempo per fare il layout. Quando il blocco inviato viene eseguito, i frame hanno le dimensioni corrette.


3

Questo ha risolto il problema (ridicolmente fastidioso) per me:

- (void) viewDidLayoutSubviews {

    [super viewDidLayoutSubviews];

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

}

Modifica / Nota: questo è per un ViewController a schermo intero.


L'uso dei limiti mainScreen non è una buona soluzione perché in molti casi il viewController non occupa l'intero spazio dello schermo. Inoltre, dovresti chiamare [super viewDidLayoutSubviews];questo metodo a causa di molte cose di layout automatico fatte dalla vista stessa
Martin

@Martin cosa mi consigliate? D'accordo, questo non sembra l'ideale.
Crashalot

@Crashalot come dico nella mia risposta, usando autolayout o autoresizingMask imposterebbe correttamente i tuoi UIView. Ma se devi eseguire calcoli speciali su un determinato frame di visualizzazione, la risposta di Eugen funziona: chiamalo layoutIfNeeded. Ho la sensazione che questa non sia la soluzione migliore, ma non ho ancora trovato di meglio.
Martin

2

In realtà, viewDidLayoutSubviewsinoltre, non è il posto migliore per impostare la cornice della tua vista. Per quanto ho capito, da ora in poi l'unico posto in cui dovrebbe essere fatto è il layoutSubviewsmetodo nel codice della vista effettiva. Vorrei non aver ragione, qualcuno mi corregga per favore se non è vero!


grazie per la tua risposta. La documentazione di Apple viewDidLayoutSubviewsè piuttosto ambigua. La seconda frase in "discussioni" contraddice in qualche modo l'ultima. developer.apple.com/reference/uikit/uiviewcontroller/…
Martin,

0

Ho già segnalato questo problema a Apple, questo problema esiste da molto tempo, quando si inizializza UIViewController da Xib, ma ho trovato una soluzione alternativa piuttosto carina. In aggiunta a ciò, ho riscontrato questo problema in alcuni casi quando layoutIfNeeded su UICollectionView e UITableView quando l'origine dati non è impostata nel momento iniziale e necessario anche cambiarla.

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

Spedizione una volta estensione:

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

Estensione 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
    }
}

0

Il mio problema è stato risolto modificando l'utilizzo da

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

per

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

Quindi, da Did a Will

Super strano


0

Soluzione migliore per me.

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

Utilizzando

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

myButton.add(layoutComplement: CircleShapeComplement())

0

Sostituisci layoutSublayers (of layer: CALayer) invece di layoutSubviews nella sottoview della cella per avere frame corretti


0

Se hai bisogno di fare qualcosa in base alla cornice della tua vista, sovrascrivi layoutSubviews e chiama layoutSeNeeded

    override func layoutSubviews() {
    super.layoutSubviews()

    yourView.layoutIfNeeded()
    setGradientForYourView()
}

Ho avuto il problema con viewDidLayoutSubviews che restituiva il frame sbagliato per la mia vista, per cui avevo bisogno di aggiungere una sfumatura. E solo layoutIfNeeded ha fatto la cosa giusta :)


-1

Come per il nuovo aggiornamento in ios questo è in realtà un bug ma possiamo ridurlo usando -

Se stai usando xib con autolayout nel tuo progetto, devi solo aggiornare il frame nelle impostazioni di autolayout per favore trova l'immagine per questo.inserisci qui la descrizione dell'immagine

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.