web-dev-qa-db-fra.com

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

Il semble qu'avec Xcode 8, sur viewDidLoad, toutes les sous-vues de viewcontroller aient la même taille que 1000x1000. Chose étrange, mais d'accord, viewDidLoad n'a jamais été le meilleur endroit pour dimensionner correctement les vues.

Mais viewDidLayoutSubviews l'est! 

Et sur mon projet actuel, j'essaie 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 à un clic de bouton, par exemple, le journal affiche une taille normale.

J'utilise autolayout.

Est-ce un bug?

87
Martin

À présent, Interface Builder permet à l'utilisateur de modifier de manière dynamique la taille de chaque contrôleur de vue dans le storyboard, afin de simuler la taille d'un périphérique donné.

Avant cette fonctionnalité, l'utilisateur doit définir manuellement la taille de chaque contrôleur de vue. Le contrôleur de vue a donc été enregistré avec une certaine taille, qui a été utilisée dans initWithCoder pour définir le cadre initial.

Maintenant, il semble que initWithCoder n'utilise pas la taille définie dans le storyboard et définisse une taille 1000x1000 px pour la vue viewcontroller et toutes ses sous-vues.

Ce n'est pas un problème, car les vues doivent toujours utiliser l'une de ces solutions de présentation:

  • autolayout, et toutes les contraintes organiseront correctement vos vues

  • autoresizingMask, qui mettra en page chaque vue pour laquelle aucune contrainte n'est attachée (les contraintes de marge et de marge de la note sont maintenant compatibles dans la même vue\o /!)

Mais ceci est est un problème pour tous les éléments de présentation liés au calque de vue, comme cornerRadius, puisque ni autolayout ni masque de redimensionnement automatique ne s'applique aux propriétés du calque.

Pour résoudre ce problème, la méthode habituelle consiste à utiliser viewDidLayoutSubviews si vous êtes dans le contrôleur ou layoutSubview si vous êtes dans une vue. À ce stade (n'oubliez pas d'appeler leurs méthodes relatives super), vous êtes pratiquement sûr que tout le travail de mise en page a été effectué!

Assez sûr? Hum ... pas tout à fait, 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 ne se produit que lors de la disposition des cellules! Dans les sous-classes UITableViewCell & UICollectionViewCell, layoutSubview ne sera pas appelé les sous-vues après _ seraient correctement présentées.

2- Comme @EugenDimboiu l'a fait remarquer (s'il vous plaît, demandez sa vote si cela vous est utile), appeler [myView layoutIfNeeded] dans la sous-vue non mise en forme le placera correctement à temps.

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

3- À mon avis, c'est vraiment un bug. Je l'ai soumis au radar (id 28562874).

PS: je ne suis pas anglais, alors n'hésitez pas à éditer mon post 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 passer la réponse acceptée.

93
Martin

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

39
Eugen Dimboiu

Solution: Tout envelopper dans viewDidLayoutSubviews dans DispatchQueue.main.async

// Swift 3

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

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

Je sais que ce n’était pas votre question exacte, mais j’ai rencontré un problème similaire: lors de la mise à jour, certaines de mes vues étaient brouillées malgré la taille correcte de la trame dans viewDidLayoutSubviews. Selon iOS 10 Notes de publication:

"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éfinie sur NO et si elle était positionnée par des contraintes, layoutIfNeeded déplacerait le faire correspondre le moteur de présentation avant d'envoyer la présentation à la sous-arborescence. Ces modifications de .__ corrigent ce comportement, ainsi que la position du destinataire et généralement sa taille ne sera pas affectée par layoutIfNeeded.

Certains codes existants peuvent s'appuyer sur ce comportement incorrect, à savoir maintenant corrigé. Il n'y a pas de changement de comportement pour les fichiers binaires liés avant iOS 10, mais si vous utilisez iOS 10, vous devrez peut-être en corriger situations en envoyant -layoutIfNeeded à un aperçu du traduit la vue AutodesizingMaskIntoConstraints qui était la précédente récepteur, ou bien le positionner et le dimensionner avant (ou après, selon le comportement souhaité) layoutIfNeeded.

Applications tierces avec des sous-classes UIView personnalisées utilisant la disposition automatique qui remplacez layoutSubviews et la disposition modifiée sur vous-même avant d'appeler super courent le risque de déclencher une boucle de rétroaction d’agencement lorsqu’ils reconstruisent le iOS 10. Quand ils sont correctement envoyés, les appels de layoutSubviews suivants ils doivent être sûrs de cesser de salir leur mise en page à un moment donné (remarque cet appel a été ignoré dans la version antérieure à iOS 10). "

Essentiellement, vous ne pouvez pas appeler layoutIfNeeded sur un objet enfant de la vue si vous utilisez translateatesAutoresizingMaskIntoConstraints - l'appel à présent de layoutIfNeeded doit se faire sur la superView, et vous pouvez toujours l'appeler dans viewDidLayoutSubviews.

18

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 en plein écran.

3
Nerdhappy

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

3
Emiel

En fait, viewDidLayoutSubviews n’est pas non plus le meilleur endroit pour définir le cadre de votre vue. D'après ce que j'ai compris, le seul endroit où cela devrait être fait est désormais la méthode layoutSubviews dans le code de la vue réelle. J'aurais aimé ne pas avoir raison, s'il vous plaît, corrigez-moi si ce n'est pas vrai!

2
alex_roudique

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 fait à volonté

Super bizarre

0
Jan

J'ai déjà signalé ce problème à Apple. Ce problème existe depuis très 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'était pas définie au moment initial, et qu'il fallait aussi la balayer.

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

Remplacez layoutSublayers (de layer: CALayer) au lieu de layoutSubviews dans la sous-vue de cellule pour avoir les images correctes

0
Entro

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())
0
bizhara