Depuis Xcode 8 et iOS10, les vues ne sont pas dimensionnées correctement sur viewDidLayoutSubviews

94

Il semble qu'avec Xcode 8, viewDidLoadtoutes les sous-vues du contrôleur de vue aient la même taille de 1000x1000. Chose étrange, mais d'accord, viewDidLoadn'a jamais été le meilleur endroit pour dimensionner correctement les vues.

Mais viewDidLayoutSubviewsest-ce!

Et sur mon projet actuel, j'essaye d'imprimer la taille d'un bouton:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

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

Le journal affiche une taille de (1000x1000) pour myButton! Ensuite, si je me connecte sur un bouton, par exemple, le journal affiche une taille normale.

J'utilise la mise en page automatique.

Est-ce un bug?

Martin
la source
1
Ayant le même problème avec UIImageView - lorsque j'imprime, j'obtiens un cadre étrange = (0 0; 1000 1000);. Je suis à l'intérieur d'un UITableViewCell, et une fois que j'ai actualisé la vue de la table, le cadre est ce que je m'attends à ce qu'il soit (également lorsque la cellule sort de la fenêtre et revient). Quelqu'un a-t-il une idée de pourquoi cela se produit (cadre étrange par défaut)?
Eugen Dimboiu le
4
Je pense que l' (0, 0, 1000, 1000)initialisation liée est la nouvelle façon dont Xcode instancie les vues d'IB. Avant Xcode8, les vues étaient créées avec leur taille configurée dans le xib, puis redimensionnées en fonction de l'écran juste après. Mais maintenant, il n'y a pas de taille configurée dans le document IB car la taille dépend de la sélection de votre appareil (en bas de l'écran). La vraie question est donc: y a-t-il un endroit fiable où la taille finale des vues pourrait être vérifiée?
Martin le
4
utilisez-vous des coins arrondis pour votre bouton? Essayez d'appeler layoutIfNeeded () avant.
Eugen Dimboiu
Intéressant. J'utilisais en effet le cadre de vue pour calculer une bordure ronde. Même si cela ne répond pas à la question, cela fonctionne. C'est un bon conseil à garder à l'esprit. Merci!
Martin le
Je pense que j'ai des problèmes similaires lors de la configuration d'un bouton d'image dans la vue de droite d'un uitextfield. Je voulais définir la hauteur et la largeur du bouton d'image sur la hauteur du champ de texte afin qu'il conserve son rapport hauteur / largeur et les retombées du conteneur.
atlantach_james

Réponses:

98

Désormais, Interface Builder permet à l'utilisateur de modifier dynamiquement la taille de chaque contrôleur de vue dans le storyboard, pour simuler la taille d'un certain appareil.

Avant cette fonctionnalité, l'utilisateur doit définir manuellement chaque taille de contrôleur de vue. Ainsi, le contrôleur de vue a été enregistré avec une certaine taille, qui a été utilisée initWithCoderpour définir l'image initiale.

Maintenant, il semble que initWithCodern'utilisez pas la taille définie dans le storyboard et définissez une taille de 1000x1000 px pour la vue du contrôleur de vue et toutes ses sous-vues.

Ce n'est pas un problème, car les vues doivent toujours utiliser l'une de ces solutions de mise en page:

  • mise en page automatique, et toutes les contraintes mettront correctement en page vos vues

  • autoresizingMask, qui mettra en page chaque vue à laquelle aucune contrainte n'est attachée ( notez que les contraintes de mise en page automatique et de marge sont désormais compatibles dans la même vue \ o /! )

Mais c'est un problème pour tous les éléments de mise en page liés au calque de vue, comme cornerRadius, car ni la mise en page automatique ni le masque de redimensionnement automatique ne s'appliquent aux propriétés du calque.

Pour répondre à ce problème, la méthode courante consiste à utiliser viewDidLayoutSubviewssi vous êtes dans le contrôleur, ou layoutSubviewsi vous êtes dans une vue. À ce stade (n'oubliez pas d'appeler leurs superméthodes relatives), vous êtes à peu près sûr que tout le travail de mise en page a été fait!

Assez sûr? Hum ... pas totalement, j'ai remarqué, et c'est pourquoi j'ai posé cette question, dans certains cas, la vue a toujours sa taille 1000x1000 sur cette méthode. Je pense qu'il n'y a pas de réponse à ma propre question. Pour donner le maximum d'informations à ce sujet:

1- cela n'arrive que lors de la disposition des cellules! Dans les UITableViewCell& UICollectionViewCellsous-classes, layoutSubviewne sera pas appelé après que les sous- vues soient correctement disposées.

2- Comme l'a fait remarquer @EugenDimboiu (veuillez voter pour sa réponse si cela est utile pour vous), appeler la [myView layoutIfNeeded]sous-vue non disposée la mettra en page correctement juste à temps.

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

3- A mon avis, c'est définitivement un bug. Je l'ai soumis au radar (id 28562874).

PS: je ne suis pas anglophone, alors n'hésitez pas à modifier mon message si ma grammaire doit être corrigée;)

PS2: Si vous avez une meilleure solution, n'hésitez pas à écrire une autre réponse. Je vais proposer la réponse acceptée.

Martin
la source
3
thx, mais je n'ai pas pu trouver le radar avec l'ID 28562874, puis-je avoir l'URL du radar?
Joey
Cela a fonctionné comme un charme pour moi, aussi pour les couches de la vue!
hlynbech
@Joey, je ne sais pas si je peux obtenir un lien de mon bug. Je n'ai trouvé aucune URL directe et il semble que les autres utilisateurs ne puissent pas voir mes rapports. D'après cette réponse SO stackoverflow.com/a/145223/127493 , il semble que le meilleur moyen d'augmenter la priorité d'un bogue soit de faire un doublon.
Martin
C'est plus que stupide de la part d'Apple. J'ai un contrôleur de vue de mise en page entièrement spécifié par auto, et plusieurs des champs de texte et un UIView ont tous ce stupide cadre 0,0,1000,1000. Mais pas tout. Comment cela peut-il échapper à l'assurance qualité? Je suppose que je peux déposer un autre radar qu'ils ne liront pas.
ahwulf
3
Je tiens à vous remercier, vous et tous les autres, pour vos idées et suggestions. J'ai passé toute la journée à m'arracher les cheveux parce que l' UIStackViewintérieur d'un UICollectionViewCellne retournait pas une hauteur correcte pendant viewDidLayoutSubviews. L'appel a layoutIfNeededimmédiatement corrigé le problème.
Ruiz
40

Utilisez-vous des coins arrondis pour votre bouton? Essayez d'appeler layoutIfNeeded()avant.

Eugen Dimboiu
la source
1
ahah, essayer d'obtenir plus de représentants? Comme je l'ai dit dans mon commentaire, cela ne répond pas à la question. Cependant, cela m'a aidé, donc vous avez gagné un +1 :)
Martin
4
Cela peut aider quelqu'un à l'avenir, et il est plus facile de le repérer par rapport aux commentaires
Eugen Dimboiu
1
M'a aidé tout à l'heure!
daidai
@daidai heureux d'entendre ça!
Eugen Dimboiu le
cela marche! mais pourquoi? mais quelle est la bonne solution pour obtenir la bonne taille de cadre?
Crashalot le
24

Solution: Wrap tout à l' intérieur viewDidLayoutSubviewsde DispatchQueue.main.async.

// swift 3

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    DispatchQueue.main.async {
        // do stuff here
    }
}
Derek Soike
la source
1
cela fonctionne même si mis dans -viewDidLoad... une solution de contournement aussi étrange
medvedNick
1
Probablement parce que lorsque viewDidLayoutSubviews, le système n'avait pas eu le temps de faire ce qui devait être fait. L'appel de dispatch vous fait sauter une boucle, permettant au système de finir sa parole. Si j'ai raison, quand vous pourriez avoir le même comportement avec un sommeil de quelques millisecondes
thibaut noah
car toutes les tâches de l'interface utilisateur doivent être effectuées dans le thread principal, merci pour votre solution!
danywarner
18

Je sais que ce n'était pas votre question exacte, mais j'ai rencontré un problème similaire où, comme lors de la mise à jour, certaines de mes vues ont été faussées malgré la taille d'image correcte dans viewDidLayoutSubviews. Selon les notes de version d'iOS 10:

"L'envoi de layoutIfNeeded à une vue ne devrait pas déplacer la vue, mais dans les versions précédentes, si la vue avait translatesAutoresizingMaskIntoConstraints défini sur NO, et si elle était positionnée par des contraintes, layoutIfNeeded déplacerait la vue pour qu'elle corresponde au moteur de mise en page avant d'envoyer la mise en page Ces modifications corrigent ce comportement, et la position du récepteur et généralement sa taille ne seront pas affectées par layoutIfNeeded.

Certains codes existants peuvent s'appuyer sur ce comportement incorrect qui est maintenant corrigé. Il n'y a pas de changement de comportement pour les binaires liés avant iOS 10, mais lors de la construction sur iOS 10, vous devrez peut-être corriger certaines situations en envoyant -layoutIfNeeded à un superview de la vue translatesAutoresizingMaskIntoConstraints qui était le récepteur précédent, ou bien en le positionnant et en le dimensionnant avant ( ou après, selon votre comportement souhaité) layoutIfNeeded.

Les applications tierces avec des sous-classes UIView personnalisées utilisant la mise en page automatique qui remplacent la mise en page Les sous-vues et la mise en page sale sur soi avant d'appeler super risquent de déclencher une boucle de retour de mise en page lorsqu'elles se reconstruisent sur iOS 10. Lorsqu'elles sont correctement envoyées, les appels de mise en page suivants, ils doivent être sûrs de arrêter de salir la mise en page sur soi à un moment donné (notez que cet appel a été ignoré dans la version antérieure à iOS 10). "

Essentiellement, vous ne pouvez pas appeler layoutIfNeeded sur un objet enfant de View si vous utilisez translatesAutoresizingMaskIntoConstraints - maintenant l'appel de layoutIfNeeded doit être sur le superView, et vous pouvez toujours l'appeler dans viewDidLayoutSubviews.

HannahCarney
la source
5

Si les cadres ne sont pas corrects dans layoutSubViews (ce qu'ils ne sont pas), vous pouvez envoyer un peu de code asynchrone sur le thread principal. Cela laisse au système le temps de faire la mise en page. Lorsque le bloc que vous envoyez est exécuté, les cadres ont leurs tailles appropriées.

Emiel
la source
3

Cela a résolu le problème (ridiculement ennuyeux) pour moi:

- (void) viewDidLayoutSubviews {

    [super viewDidLayoutSubviews];

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

}

Edit / Note: Ceci est pour un ViewController plein écran.

Nerdhappy
la source
L'utilisation des limites de mainScreen n'est pas une bonne solution car dans de nombreux cas, le viewController ne prend pas tout l'espace de l'écran. De plus, vous devriez appeler [super viewDidLayoutSubviews];cette méthode en raison de nombreuses tâches de mise en page automatique effectuées par la vue elle
Martin
@Martin que recommandez-vous? D'accord, cela ne semble pas idéal.
Crashalot le
@Crashalot comme je le dis dans ma réponse, l'utilisation de la mise en page automatique ou du masque autoresizing mettrait correctement en page vos UIViews. Mais si vous devez effectuer un calcul spécial sur un certain cadre de vue, la réponse d'Eugen fonctionne: appelez- layoutIfNeededle. J'ai le sentiment que ce n'est pas la meilleure solution, mais je n'ai toujours pas trouvé mieux.
Martin
2

En fait, ce viewDidLayoutSubviewsn'est pas non plus le meilleur endroit pour définir le cadre de votre vue. D'après ce que j'ai compris, à partir de maintenant, le seul endroit où cela devrait être fait est la layoutSubviewsméthode dans le code de la vue réelle. J'aurais aimé ne pas avoir raison, quelqu'un me corrige s'il vous plaît si ce n'est pas vrai!

alex_roudique
la source
Merci pour votre réponse. La documentation Apple sur viewDidLayoutSubviewsest assez ambiguë. La deuxième phrase des «discussions» contredit en quelque sorte la dernière. developer.apple.com/reference/uikit/uiviewcontroller/…
Martin
0

J'ai déjà signalé ce problème à Apple, ce problème existe depuis longtemps, lorsque vous initialisez UIViewController à partir de Xib, mais j'ai trouvé une solution de contournement assez intéressante. En plus de cela, j'ai trouvé ce problème dans certains cas lorsque layoutIfNeeded sur UICollectionView et UITableView lorsque la source de données n'est pas définie au moment initial et que je devais également la swizzler.

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

Envoi une fois l'extension:

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

Extension 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
la source
0

Mon problème a été résolu en modifiant l'utilisation de

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

à

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

Donc, de Did à Will

Super bizarre

Jan
la source
0

Meilleure solution pour moi.

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

En utilisant

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

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

Remplacer la mise en page Les éditeurs (du calque: CALayer) au lieu de la mise en page Les sous-vues dans la sous-vue de la cellule pour avoir des cadres corrects

Entro
la source
0

Si vous avez besoin de faire quelque chose en fonction du cadre de votre vue, remplacez layoutSubviews et appelez layoutIfNeeded

    override func layoutSubviews() {
    super.layoutSubviews()

    yourView.layoutIfNeeded()
    setGradientForYourView()
}

J'ai eu le problème avec viewDidLayoutSubviews renvoyant le mauvais cadre pour ma vue, pour laquelle je devais ajouter un dégradé. Et seul layoutIfNeeded a fait la bonne chose :)

Vitya Shurapov
la source
-1

Selon la nouvelle mise à jour d'ios, il s'agit en fait d'un bogue mais nous pouvons le réduire en utilisant -

Si vous utilisez xib avec la mise en page automatique dans votre projet, vous devez simplement mettre à jour le cadre dans le paramètre de mise en page automatique, veuillez trouver une image pour cela.entrez la description de l'image ici

neha mishra
la source