Desde Xcode 8 y iOS10, las vistas no tienen el tamaño adecuado en viewDidLayoutSubviews

94

Parece que con Xcode 8, viewDidLoadtodas las subvistas del controlador de vista tienen el mismo tamaño de 1000x1000. Lo extraño, pero bueno, viewDidLoadnunca ha sido el mejor lugar para dimensionar correctamente las vistas.

¡Pero viewDidLayoutSubviewses!

Y en mi proyecto actual, trato de imprimir el tamaño de un botón:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

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

¡El registro muestra un tamaño de (1000x1000) para myButton! Luego, si inicio sesión en un botón, haga clic, por ejemplo, el registro muestra un tamaño normal.

Estoy usando diseño automático.

¿Es un error?

Martín
fuente
1
Tener el mismo problema con UIImageView: cuando imprimo obtengo un marco extraño = (0 0; 1000 1000) ;. Estoy dentro de una UITableViewCell, y una vez que actualizo la vista de la tabla, el marco es lo que espero que sea (también cuando la celda sale de la ventana gráfica y regresa). ¿Alguien tiene alguna idea de por qué está sucediendo esto (marco extraño por defecto)?
Eugen Dimboiu
4
Creo que la (0, 0, 1000, 1000)inicialización enlazada es la nueva forma en que Xcode instancia las vistas de IB. Antes de Xcode8, las vistas se creaban con su tamaño configurado en el xib y luego se cambiaban de tamaño según la pantalla justo después. Pero ahora, no hay un tamaño configurado en el documento IB ya que el tamaño depende de la selección de su dispositivo (en la parte inferior de la pantalla). Entonces, la pregunta real es: ¿existe un lugar confiable donde se pueda verificar el tamaño final de las vistas?
Martin
4
¿Estás usando esquinas redondeadas para tu botón? Intente llamar a layoutIfNeeded () antes.
Eugen Dimboiu
Interesante. De hecho, estaba usando el marco de vista para calcular un borde redondo. Incluso si no responde a la pregunta, funciona. Es un buen consejo a tener en cuenta. ¡Gracias!
Martin
Creo que tengo problemas similares al configurar un botón de imagen dentro de la vista derecha de un uitextfield. Quería establecer la altura y el ancho del botón de imagen a la altura del campo de texto para que mantuviera su relación de aspecto y el contenedor.
atlantach_james

Respuestas:

98

Ahora, Interface Builder permite al usuario cambiar dinámicamente el tamaño de cada controlador de vista en el guión gráfico, para simular el tamaño de un determinado dispositivo.

Antes de esta funcionalidad, el usuario debe configurar manualmente el tamaño de cada controlador de vista. Entonces, el controlador de vista se guardó con un cierto tamaño, que se usó initWithCoderpara establecer el marco inicial.

Ahora, parece que initWithCoderno use el tamaño definido en el guión gráfico y defina un tamaño de 1000x1000 px para la vista del controlador de vista y todas sus subvistas.

Esto no es un problema, porque las vistas siempre deben usar cualquiera de estas soluciones de diseño:

  • autolayout, y todas las restricciones diseñarán correctamente sus vistas

  • autoresizingMask, que diseñará cada vista que no tenga ninguna restricción adjunta ( tenga en cuenta que el diseño automático y las restricciones de margen ahora son compatibles en la misma vista \ o /! )

Pero esto es un problema para todas las cosas de diseño relacionadas con la capa de vista cornerRadius, ya que ni el diseño automático ni la máscara de tamaño automático se aplican a las propiedades de la capa.

Para responder a este problema, la forma más común es utilizarlo viewDidLayoutSubviewssi está en el controlador o layoutSubviewsi está en una vista. En este punto (no olvide llamar a sus supermétodos relativos), ¡está bastante seguro de que se ha hecho todo el diseño!

¿Bastante seguro? Hum ... no del todo, he comentado, y es por eso que hice esta pregunta, en algunos casos la vista todavía tiene su tamaño de 1000x1000 en este método. Creo que no hay respuesta a mi propia pregunta. Para dar la máxima información al respecto:

1- ¡ocurre solo al colocar celdas! En las subclases UITableViewCell& UICollectionViewCell, layoutSubviewno se llamará después de que las subvistas estén correctamente distribuidas.

2- Como comentó @EugenDimboiu (por favor, vote a favor de su respuesta si es útil para usted), llamar [myView layoutIfNeeded]a la subvista no diseñada la distribuirá correctamente justo a tiempo.

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

3- En mi opinión, esto es definitivamente un error. Lo envié al radar (id 28562874).

PD: no soy nativo de inglés, así que siéntete libre de editar mi publicación si se debe corregir mi gramática;)

PS2: Si tiene una solución mejor, no dude en escribir otra respuesta. Moveré la respuesta aceptada.

Martín
fuente
3
gracias, pero no pude encontrar el radar con la identificación 28562874, ¿puedo tener la URL del radar?
Joey
Funcionó como un encanto para mí, ¡también para las capas de la vista!
hlynbech
@ Joey, no sé si puedo obtener un enlace de mi error. No encontré ninguna URL directa y parece que otros usuarios no pueden ver mis informes. De acuerdo con esta respuesta SO stackoverflow.com/a/145223/127493 , parece que la mejor manera de aumentar la prioridad de un error es hacer un duplicado.
Martin
Esto es más que estúpido por parte de Apple. Tengo un controlador de vista de diseño automático completamente especificado, y varios de los campos de texto y un UIView tienen este estúpido marco 0,0,1000,1000. Pero no todos. ¿Cómo puede esto escapar del control de calidad? Supongo que puedo presentar otro radar que no leerán.
ahwulf
3
Me gustaría agradecerle a usted y a todos los demás sus conocimientos y sugerencias. Pasé todo el día tirándome el pelo porque el UIStackViewinterior de un UICollectionViewCellno devolvía la altura correcta durante viewDidLayoutSubviews. Llamar layoutIfNeededinmediatamente rectificó el problema.
Ruiz
40

¿Estás usando esquinas redondeadas para tu botón? Intente llamar layoutIfNeeded()antes.

Eugen Dimboiu
fuente
1
ahah, ¿tratando de obtener más reputación? Como dije en mi comentario, esto no responde a la pregunta. Sin embargo, me ayudó, así que obtuviste un +1 :)
Martin
4
Puede ayudar a alguien en el futuro, y es más fácil de detectar en comparación con los comentarios
Eugen Dimboiu
1
¡Me ayudó hace un momento!
daidai
¡@daidai me alegra escuchar eso!
Eugen Dimboiu
¡esto funciona! ¿pero por qué? también, pero ¿cuál es la solución adecuada para obtener el tamaño de marco adecuado?
Crashalot
24

Solución: envolver todo el interior viewDidLayoutSubviewsen DispatchQueue.main.async.

// swift 3

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    DispatchQueue.main.async {
        // do stuff here
    }
}
Derek Soike
fuente
1
esto funciona incluso si se pone en -viewDidLoad... una solución tan extraña
medvedNick
1
Probablemente porque cuando viewDidLayoutSubviews el sistema no había tenido tiempo de hacer lo que había que hacer. Llamar al despacho te hace saltar un bucle de ejecución, lo que permite que el sistema termine su palabra. Si estoy en lo cierto cuando podrías tener el mismo comportamiento con un sueño de unos pocos milisegundos
thibaut noah
porque todas las tareas de la interfaz de usuario deben realizarse dentro del hilo principal, ¡gracias por su solución!
danywarner
18

Sé que esta no era su pregunta exacta, pero me encontré con un problema similar en el que, en la actualización, algunas de mis vistas se estropearon a pesar de tener el tamaño de marco correcto en viewDidLayoutSubviews. Según las notas de la versión de iOS 10:

"No se espera que el envío de layoutIfNeeded a una vista mueva la vista, pero en versiones anteriores, si la vista tenía translatesAutoresizingMaskIntoConstraints establecido en NO, y si estaba siendo posicionado por restricciones, layoutIfNeeded movería la vista para que coincida con el motor de diseño antes de enviar el diseño Estos cambios corrigen este comportamiento, y la posición del receptor y normalmente su tamaño no se verán afectados por layoutIfNeeded.

Es posible que parte del código existente se base en este comportamiento incorrecto que ahora está corregido. No hay ningún cambio de comportamiento para los binarios vinculados antes de iOS 10, pero al construir en iOS 10, es posible que deba corregir algunas situaciones enviando -layoutIfNeeded a una supervista de la vista translatesAutoresizingMaskIntoConstraints que era el receptor anterior, o bien posicionarlo y dimensionarlo antes ( o después, dependiendo de su comportamiento deseado) layoutIfNeeded.

Las aplicaciones de terceros con subclases de UIView personalizadas que usan Auto Layout que anulan el diseño Las subvistas y el diseño sucio en sí mismos antes de llamar a Super tienen el riesgo de desencadenar un bucle de retroalimentación de diseño cuando se reconstruyen en iOS 10. Cuando se envían correctamente el diseño posterior Las llamadas de subviews deben asegurarse de deje de ensuciar el diseño en sí mismo en algún momento (tenga en cuenta que esta llamada se omitió en la versión anterior a iOS 10) ".

Esencialmente, no puede llamar a layoutIfNeeded en un objeto secundario de la Vista si está utilizando translatesAutoresizingMaskIntoConstraints; ahora, la llamada a layoutIfNeeded debe estar en la superView, y aún puede llamar a esto en viewDidLayoutSubviews.

HannahCarney
fuente
5

Si los marcos no son correctos en layoutSubViews (que no lo son), puede enviar async un poco de código en el hilo principal. Esto le da al sistema algo de tiempo para hacer el diseño. Cuando se ejecuta el bloque que envías, los marcos tienen sus tamaños adecuados.

Emiel
fuente
3

Esto solucionó el problema (ridículamente molesto) para mí:

- (void) viewDidLayoutSubviews {

    [super viewDidLayoutSubviews];

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

}

Editar / Nota: Esto es para un ViewController de pantalla completa.

Nerdhappy
fuente
Usar los límites de mainScreen no es una buena solución porque en muchos casos el viewController no ocupa todo el espacio de la pantalla. Además, debe llamar [super viewDidLayoutSubviews];a este método debido a muchas cosas de diseño automático realizadas por la vista en sí
Martin
@Martin, ¿qué me recomiendas? De acuerdo, esto no parece ideal.
Crashalot
@Crashalot como digo en mi respuesta, el uso de autolayout o autoresizingMask distribuiría correctamente sus UIViews. Pero si debe realizar un cálculo especial en un determinado marco de vista, la respuesta de Eugen funciona: invocarlo layoutIfNeeded. Tengo la sensación de que esta no es la mejor solución, pero todavía no he encontrado ninguna mejor.
Martin
2

En realidad, viewDidLayoutSubviewstampoco es el mejor lugar para establecer el marco de su vista. Por lo que tengo entendido, a partir de ahora, el único lugar en el que debería hacerse es el layoutSubviewsmétodo en el código de la vista real. Ojalá no estuviera en lo cierto, ¡que alguien me corrija si no es verdad!

alex_roudique
fuente
gracias por tu respuesta. La documentación de Apple viewDidLayoutSubviewses bastante ambigua. La segunda oración de "discusiones" contradice de alguna manera la última. developer.apple.com/reference/uikit/uiviewcontroller/…
Martin
0

Ya informé este problema a Apple, este problema existe desde hace mucho tiempo, cuando está inicializando UIViewController desde Xib, pero encontré una solución bastante buena. Además de eso, encontré ese problema en algunos casos cuando layoutIfNeeded en UICollectionView y UITableView cuando la fuente de datos no está configurada en el momento inicial, y también necesitaba swizzle.

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

Envío una vez extensión:

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

Extensión 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
fuente
0

Mi problema se resolvió cambiando el uso de

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

a

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

Entonces, de Did a Will

Super raro

ene
fuente
0

Mejor solución para mí.

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

Utilizando

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

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

Anular layoutSublayers (de la capa: CALayer) en lugar de layoutSubviews en la subvista de celda para tener los marcos correctos

Entro
fuente
0

Si necesita hacer algo basado en el marco de su vista, anule layoutSubviews y llame a layoutIfNeeded

    override func layoutSubviews() {
    super.layoutSubviews()

    yourView.layoutIfNeeded()
    setGradientForYourView()
}

Tuve el problema con viewDidLayoutSubviews que devolvía el marco incorrecto para mi vista, por lo que necesitaba agregar un degradado. Y solo layoutIfNeeded hizo lo correcto :)

Vitya Shurapov
fuente
-1

Según la nueva actualización en ios, esto es en realidad un error, pero podemos reducirlo usando -

Si está utilizando xib con diseño automático en su proyecto, solo tiene que actualizar el marco en la configuración de diseño automático, busque la imagen para esto.ingrese la descripción de la imagen aquí

neha mishra
fuente