Desde o Xcode 8 e iOS10, as visualizações não são dimensionadas corretamente em viewDidLayoutSubviews

94

Parece que com o Xcode 8 viewDidLoadativado, todas as subvisualizações do viewcontroller têm o mesmo tamanho de 1000x1000. Coisa estranha, mas tudo bem, viewDidLoadnunca foi o melhor lugar para dimensionar corretamente as visualizações.

Mas viewDidLayoutSubviewsé!

E no meu projeto atual, tento imprimir o tamanho de um botão:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

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

O log mostra um tamanho de (1000x1000) para myButton! Então, se eu logar em um clique de botão, por exemplo, o log mostra um tamanho normal.

Estou usando autolayout.

É um bug?

Martin
fonte
1
Tendo o mesmo problema com UIImageView - quando imprimo, obtenho um quadro estranho = (0 0; 1000 1000) ;. Estou dentro de um UITableViewCell e, assim que atualizo o tableview, o quadro é o que eu esperava que fosse (também quando a célula sai da janela de visualização e volta novamente). Alguém tem alguma ideia do por que isso está acontecendo (quadro estranho por padrão)?
Eugen Dimboiu de
4
Acho que a (0, 0, 1000, 1000)inicialização associada é a nova maneira que o Xcode instancia visualizações do IB. Antes do Xcode8, as visualizações eram criadas com seu tamanho configurado no xib, e redimensionadas de acordo com a tela logo depois. Mas agora, não há tamanho configurado no documento IB, pois o tamanho depende da seleção do seu dispositivo (na parte inferior da tela). Portanto, a verdadeira questão é: há um lugar confiável onde o tamanho final das visualizações possa ser verificado?
Martin de
4
você está usando cantos arredondados para o botão? Tente chamar layoutIfNeeded () antes.
Eugen Dimboiu de
Interessante. Na verdade, eu estava usando o quadro de visão para calcular uma borda redonda. Mesmo que não responda à pergunta, funciona. É uma boa dica para se manter em mente. Obrigado!
Martin de
Acho que estou tendo problemas semelhantes ao configurar um botão de imagem dentro da visão direita de um uitextfield. Eu queria definir a altura e a largura do botão de imagem para a altura do campo de texto para que ele mantivesse sua proporção de aspecto e queda fora do contêiner.
atlantach_james

Respostas:

98

Agora, o Interface Builder permite que o usuário altere dinamicamente o tamanho de todos os controladores de visualização no storyboard, para simular o tamanho de um determinado dispositivo.

Antes desta funcionalidade, o usuário deve definir manualmente cada tamanho do controlador de visualização. Portanto, o controlador de visualização foi salvo com um certo tamanho, que foi usado initWithCoderpara definir o quadro inicial.

Agora, parece que initWithCodernão use o tamanho definido no storyboard, e defina um tamanho de 1000x1000 px para a visualização do controlador de visualização e todas as suas subvisualizações.

Isso não é um problema, porque as visualizações devem sempre usar uma destas soluções de layout:

  • autolayout, e todas as restrições farão o layout correto de suas visualizações

  • autoresizingMask, que fará o layout de cada visualização que não tenha nenhuma restrição anexada ( observe que o autolayout e as restrições de margem agora são compatíveis na mesma visualização \ o /! )

Mas isso é um problema para todas as coisas de layout relacionadas à camada de visualização, como cornerRadius, desde que nem autolayout nem máscara de redimensionamento automático se aplicam às propriedades da camada.

Para responder a esse problema, a maneira comum é usar viewDidLayoutSubviewsse você estiver no controlador ou layoutSubviewem uma visualização. Neste ponto (não se esqueça de chamar seus supermétodos relativos), você tem certeza de que todas as coisas de layout foram feitas!

Tem certeza? Hum ... não totalmente, eu observei, e é por isso que fiz essa pergunta, em alguns casos a visualização ainda tem seu tamanho 1000x1000 neste método. Acho que não há resposta para minha própria pergunta. Para dar o máximo de informações sobre isso:

1- acontece apenas no layout das células! Em UITableViewCell& UICollectionViewCellsubclasses, layoutSubviewnão será chamado depois que as subvisualizações forem dispostas corretamente.

2- Como @EugenDimboiu comentou (por favor, vote a favor da resposta se for útil para você), chamar [myView layoutIfNeeded]a subvisão sem layout irá exibi-la corretamente na hora certa.

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

3- Na minha opinião, esse é definitivamente um bug. Enviei-o ao radar (id 28562874).

PS: Não sou inglês nativo, então fique à vontade para editar minha postagem se minha gramática precisar ser corrigida;)

PS2: Se você tiver alguma solução melhor, fique à vontade para não escrever outra resposta. Vou mover a resposta aceita.

Martin
fonte
3
thx, mas não consegui encontrar o radar com a id 28562874, posso ter o URL do radar?
Joey,
Funcionou como um encanto para mim, também para as camadas da vista!
hlynbech
@Joey, não sei se consigo um link do meu bug. Não encontrei nenhum URL direto e parece que outros usuários não podem ver meus relatórios. De acordo com esta resposta do SO stackoverflow.com/a/145223/127493 , parece que a melhor maneira de aumentar a prioridade de um bug é duplicá-lo.
Martin,
Isso é mais do que estúpido da parte da Apple. Eu tenho um controlador de visualização de layout automático totalmente especificado e vários dos campos de texto e um UIView têm todos esse quadro estúpido de 0,0,1000,1000. Mas nem todos. Como isso pode escapar do controle de qualidade? Suponho que posso arquivar outro radar que eles não irão ler.
ahwulf
3
Eu gostaria de agradecer a você e a todos por seus insights e sugestões. Passei o dia todo puxando meu cabelo porque a parte UIStackViewinterna de uma UICollectionViewCellnão estava voltando na altura correta durante viewDidLayoutSubviews. Ligar layoutIfNeededimediatamente corrigiu o problema.
Ruiz
40

Você está usando cantos arredondados para o botão? Tente ligar layoutIfNeeded()antes.

Eugen Dimboiu
fonte
1
ahah, tentando conseguir mais representação? Como eu disse em meu comentário, isso não responde à pergunta. No entanto, isso me ajudou, então você ganhou um +1 :)
Martin
4
Pode ajudar alguém no futuro, e é mais fácil de detectar em comparação com os comentários
Eugen Dimboiu,
1
Me ajudou agora mesmo!
daidai
@daidai fico feliz em ouvir isso!
Eugen Dimboiu
isso funciona! mas por que? também, mas qual é a solução certa para obter o tamanho de quadro adequado?
Crashalot
24

Solução: Enrole tudo dentro viewDidLayoutSubviewsem DispatchQueue.main.async.

// swift 3

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    DispatchQueue.main.async {
        // do stuff here
    }
}
Derek Soike
fonte
1
isso está funcionando mesmo se colocado em -viewDidLoad... uma solução tão estranha
medvedNick
1
Provavelmente porque quando viewDidLayoutSubviews o sistema não teve tempo para fazer o que precisava ser feito. Chamar despacho faz você pular um loop de execução, permitindo que o sistema termine sua palavra. Se eu estiver certo, você poderia ter o mesmo comportamento com um sono de alguns milissegundos
thibaut noah
porque todas as tarefas da IU devem ser realizadas dentro do thread principal, obrigado pela sua solução!
danywarner de
18

Sei que essa não era sua pergunta exata, mas me deparei com um problema semelhante em que, como na atualização, algumas das minhas visualizações estavam confusas, apesar de ter o tamanho de quadro correto em viewDidLayoutSubviews. De acordo com as notas de lançamento do iOS 10:

"Não se espera que o envio de layoutIfNeeded para uma visualização mova a visualização, mas em versões anteriores, se a visualização tivesse translatesAutoresizingMaskIntoConstraints definido como NÃO, e se estivesse sendo posicionada por restrições, layoutIfNeeded moveria a visualização para corresponder ao mecanismo de layout antes de enviar o layout para a subárvore. Essas alterações corrigem esse comportamento, e a posição do receptor e geralmente seu tamanho não serão afetados por layoutIfNeeded.

Alguns códigos existentes podem estar contando com esse comportamento incorreto que agora foi corrigido. Não há mudança de comportamento para binários vinculados antes do iOS 10, mas ao construir no iOS 10 você pode precisar corrigir algumas situações enviando -layoutIfNeeded para uma supervisualização da visualização translatesAutoresizingMaskIntoConstraints que era o receptor anterior, ou então posicionando e dimensionando antes ( ou depois, dependendo do comportamento desejado) layoutIfNeeded.

Aplicativos de terceiros com subclasses UIView personalizadas usando Auto Layout que substituem layoutSubviews e layout sujo em si antes de chamar super correm o risco de acionar um loop de feedback de layout ao reconstruir no iOS 10. Quando são enviadas corretamente chamadas subsequentes de layoutSubviews, eles devem se certificar de pare de sujar o layout em si mesmo em algum ponto (observe que esta chamada foi ignorada no lançamento antes do iOS 10). "

Essencialmente, você não pode chamar layoutIfNeeded em um objeto filho de View se estiver usando translatesAutoresizingMaskIntoConstraints - agora, chamar layoutIfNeeded deve estar em superView e você ainda pode chamar isso em viewDidLayoutSubviews.

HannahCarney
fonte
5

Se os quadros não estiverem corretos em layoutSubViews (o que não é verdade), você pode enviar um pouco de código assíncrono na thread principal. Isso dá ao sistema algum tempo para fazer o layout. Quando o bloco que você despacha é executado, os quadros têm seus tamanhos adequados.

Emiel
fonte
3

Isso corrigiu o problema (ridiculamente irritante) para mim:

- (void) viewDidLayoutSubviews {

    [super viewDidLayoutSubviews];

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

}

Editar / Nota: Isso é para um ViewController de tela inteira.

Nerdhappy
fonte
Usar limites de mainScreen não é uma boa solução porque, em muitos casos, o viewController não ocupa todo o espaço da tela. Além disso, você deve chamar [super viewDidLayoutSubviews];este método por causa de muitas coisas de autolayout feitas pela própria visualização
Martin
@Martin o que você recomenda? Concordo que isso não parece ideal.
Crashalot
@Crashalot como eu disse na minha resposta, usar autolayout ou autoresizingMask daria o layout correto ao seu UIViews. Mas se você deve realizar cálculos especiais em um determinado quadro de visão, a resposta de Eugen funciona: chame layoutIfNeeded-o. Tenho a sensação de que esta não é a melhor solução, mas ainda não encontrei nenhuma melhor.
Martin,
2

Na verdade, viewDidLayoutSubviewstambém não é o melhor lugar para definir o quadro de sua visão. Pelo que entendi, de agora em diante, o único lugar em que isso deve ser feito é o layoutSubviewsmétodo no código da visão real. Eu queria não estar certo, alguém me corrija por favor se não é verdade!

alex_roudique
fonte
obrigado pela sua resposta. A documentação da Apple sobre viewDidLayoutSubviewsé bastante ambígua. A segunda frase em "discussões" contradiz de alguma forma a última. developer.apple.com/reference/uikit/uiviewcontroller/…
Martin,
0

Eu já relatei esse problema para a apple, esse problema existe há muito tempo, quando você está inicializando o UIViewController do Xib, mas achei uma boa solução. Além disso, descobri esse problema em alguns casos quando layoutIfNeeded em UICollectionView e UITableView quando a fonte de dados não está definida no momento inicial e também precisei fazer um 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()
        }
    }
}

Enviar uma vez a extensão:

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

Extensão 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
fonte
0

Meu problema foi resolvido alterando o uso de

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

para

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

Então, de fez para vontade

Super estranho

Jan
fonte
0

Melhor solução para mim.

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

Usando

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

myButton.add(layoutComplement: CircleShapeComplement())
Bizhara
fonte
0

Substitua layoutSublayers (da camada: CALayer) em vez de layoutSubviews na subvisualização da célula para ter quadros corretos

Entro
fonte
0

Se você precisar fazer algo com base no quadro de sua visão - sobrescreva layoutSubviews e chame layoutIfNeeded

    override func layoutSubviews() {
    super.layoutSubviews()

    yourView.layoutIfNeeded()
    setGradientForYourView()
}

Tive o problema de viewDidLayoutSubviews retornando o quadro errado para minha visualização, para o qual precisei adicionar um gradiente. E apenas layoutIfNeeded fez a coisa certa :)

Vitya Shurapov
fonte
-1

De acordo com a nova atualização no ios, isso é na verdade um bug, mas podemos reduzir isso usando -

Se você estiver usando xib com autolayout em seu projeto, então você tem que apenas atualizar o quadro na configuração de autolayout, encontre a imagem para isso.insira a descrição da imagem aqui

neha mishra
fonte