Posso alterar a propriedade do multiplicador para NSLayoutConstraint?

142

Criei duas visualizações em uma superview e adicionei restrições entre as visualizações:

_indicatorConstrainWidth = [NSLayoutConstraint constraintWithItem:self.view1 attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.view2 attribute:NSLayoutAttributeWidth multiplier:1.0f constant:0.0f];
[_indicatorConstrainWidth setPriority:UILayoutPriorityDefaultLow];
_indicatorConstrainHeight = [NSLayoutConstraint constraintWithItem:self.view1 attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.view2 attribute:NSLayoutAttributeHeight multiplier:1.0f constant:0.0f];
[_indicatorConstrainHeight setPriority:UILayoutPriorityDefaultLow];
[self addConstraint:_indicatorConstrainWidth];
[self addConstraint:_indicatorConstrainHeight];

Agora, quero alterar a propriedade do multiplicador com animação, mas não consigo descobrir como alterar a propriedade do multiplicador. (Encontrei _coeficiente na propriedade privada no arquivo de cabeçalho NSLayoutConstraint.h, mas é privado.)

Como altero a propriedade do multiplicador?

Minha solução alternativa é remover a restrição antiga e adicionar a nova com um valor diferente para multipler.

Bimawa
fonte
1
Sua abordagem atual de remover restrições antigas e adicionar uma nova é a escolha certa. Entendo que não parece "certo", mas é assim que você deve fazê-lo.
25413 Rob
4
Eu acho que o multiplicador não deve ser constante. É um design ruim.
22415 Borzh
1
@ Borzh por que através de multiplicadores eu faço restrições adaptativas. Para ios redimensionável.
Bimawa
1
Sim, também quero alterar o multiplicador para que as visualizações mantenham sua proporção com a visualização principal (não a largura / altura, mas apenas a largura ou a altura), mas a coisa estranha que a maçã não permite. Quero dizer, é o mau design da Apple, não o seu.
Borzh
Esta é uma ótima conversa e cobre isso e muito mais youtube.com/watch?v=N5Ert6LTruY
DogCoffee

Respostas:

117

Se você tiver apenas dois conjuntos de multiplicadores que precisam ser aplicados, a partir do iOS8 , poderá adicionar os dois conjuntos de restrições e decidir qual deve estar ativo a qualquer momento:

NSLayoutConstraint *standardConstraint, *zoomedConstraint;

// ...
// switch between constraints
standardConstraint.active = NO; // this line should always be the first line. because you have to deactivate one before activating the other one. or they will conflict.
zoomedConstraint.active = YES;
[self.view layoutIfNeeded]; // or using [UIView animate ...]

Versão Swift 5.0

var standardConstraint: NSLayoutConstraint!
var zoomedConstraint: NSLayoutConstraint!

// ...

// switch between constraints
standardConstraint.isActive = false // this line should always be the first line. because you have to deactivate one before activating the other one. or they will conflict.
zoomedConstraint.isActive = true
self.view.layoutIfNeeded() // or using UIView.animate
Ja͢ck
fonte
2
@micnguyen Sim, você pode :) #
296
7
Se você precisar de mais de 2 conjuntos de restrições, poderá adicionar quantas desejar e alterar suas propriedades prioritárias. Dessa forma, para 2 restrições, você não precisa definir uma como ativa e a outra como inativa, basta definir a prioridade como mais alta ou mais baixa. No exemplo acima, defina a prioridade standardConstraint como, digamos, 997, e quando quiser que ela esteja ativa, defina a prioridade zoomedConstraint como 995, caso contrário, defina-a como 999. Além disso, verifique se o XIB não tem as prioridades definidas como 1000, caso contrário você não poderá alterá-los e obterá uma falha impressionante.
Dog
1
@ WonderDog isso tem alguma vantagem em particular? o idioma de habilitar e desabilitar me parece mais fácil de digerir do que ter que definir prioridades relativas.
Ja͢ck
2
@WonderDog / Jack Acabei combinando suas abordagens porque minha restrição foi definida no storyboard. E se eu criei duas restrições, ambas com a mesma prioridade, mas com multiplicadores diferentes, elas entraram em conflito e me deram muito vermelho. E pelo que posso dizer, você não pode definir um como inativo via IB. Então, criei minha restrição principal com prioridade 1000 e minha restrição secundária que desejo animar com prioridade 999 (funciona bem no IB). Então, dentro de um bloco de animação, defino a propriedade ativa da principal como false e ela animada agradavelmente para a secundária. Obrigado pela ajuda!
Clay Garrett
2
Lembre-se de que você provavelmente deseja referências fortes a suas restrições ao ativá-las e desativá-las, pois "Ativando ou desativando as chamadas de restrição addConstraint(_:)e removeConstraint(_:)na exibição que é o ancestral comum mais próximo dos itens gerenciados por essa restrição".
Qix #
166

Aqui está uma extensão NSLayoutConstraint no Swift que facilita bastante a configuração de um novo multiplicador:

No Swift 3.0 ou superior

import UIKit
extension NSLayoutConstraint {
    /**
     Change multiplier constraint

     - parameter multiplier: CGFloat
     - returns: NSLayoutConstraint
    */
    func setMultiplier(multiplier:CGFloat) -> NSLayoutConstraint {

        NSLayoutConstraint.deactivate([self])

        let newConstraint = NSLayoutConstraint(
            item: firstItem,
            attribute: firstAttribute,
            relatedBy: relation,
            toItem: secondItem,
            attribute: secondAttribute,
            multiplier: multiplier,
            constant: constant)

        newConstraint.priority = priority
        newConstraint.shouldBeArchived = self.shouldBeArchived
        newConstraint.identifier = self.identifier

        NSLayoutConstraint.activate([newConstraint])
        return newConstraint
    }
}

Uso de demonstração:

@IBOutlet weak var myDemoConstraint:NSLayoutConstraint!

override func viewDidLoad() {
    let newMultiplier:CGFloat = 0.80
    myDemoConstraint = myDemoConstraint.setMultiplier(newMultiplier)

    //If later in view lifecycle, you may need to call view.layoutIfNeeded() 
}
Andrew Schreiber
fonte
11
NSLayoutConstraint.deactivateConstraints([self])deve ser feito antes da criação newConstraint, caso contrário, poderá resultar em aviso: Não é possível satisfazer simultaneamente as restrições . Também newConstraint.active = trueé desnecessário, pois NSLayoutConstraint.activateConstraints([newConstraint])faz exatamente a mesma coisa.
tzaloga
1
ativar e desativar não funciona antes do ios8, existe alguma alternativa para isso ??
N
2
Isso não funciona quando se muda o multiplicador de novo, ele recebe um valor nulo
J. Doe
4
Obrigado por nos poupar tempo! Gostaria de mudar o nome da função para algo como cloneWithMultiplierpara torná-lo mais explícito que não se aplica apenas efeitos colaterais, mas sim retorna uma nova restrição
Alexandre G
5
Observe que, se você definir o multiplicador como 0.0você não poderá atualizá-lo novamente.
Daniel Tempestade
57

A multiplierpropriedade é somente leitura. Você precisa remover o NSLayoutConstraint antigo e substituí-lo por um novo para modificá-lo.

No entanto, como você sabe que deseja alterar o multiplicador, basta alterar a constante multiplicando-a quando for necessário, o que geralmente é menos código.

Geek frutado
fonte
64
Parece estranho que o multiplicador seja somente leitura. Eu não estava esperando isso!
21413 jowie
135
@ jowie é irônico que o valor "constante" possa ser alterado enquanto o multiplicador não.
Tomas Andrle
7
Isto está errado. Um NSLayoutConstraint especifica uma restrição de igualdade afim (in). Por exemplo: "View1.bottomY = A * View2.bottomY + B" 'A' é o multiplicador, 'B' é a constante. Não importa como você ajusta B, ela não produzirá a mesma equação que a alteração do valor de A.
Tamás Zahola
8
@FruityGeek ok, então você está usando View2.bottomYo valor atual para calcular a nova constante da restrição . Isso é defeituoso, já que View2.bottomYo valor antigo será cozido na constante, portanto, você deve abandonar o estilo declarativo e atualizar manualmente a restrição a qualquer momento View2.bottomY. Nesse caso, você também pode fazer o layout manual.
Tamás Zahola
2
Isso não funcionará no caso, quando eu quero que o NSLayoutConstraint responda por alteração de limites ou rotação do dispositivo sem código extra.
Julio
49

Uma função auxiliar que eu uso para alterar o multiplicador de uma restrição de layout existente. Ele cria e ativa uma nova restrição e desativa a antiga.

struct MyConstraint {
  static func changeMultiplier(_ constraint: NSLayoutConstraint, multiplier: CGFloat) -> NSLayoutConstraint {
    let newConstraint = NSLayoutConstraint(
      item: constraint.firstItem,
      attribute: constraint.firstAttribute,
      relatedBy: constraint.relation,
      toItem: constraint.secondItem,
      attribute: constraint.secondAttribute,
      multiplier: multiplier,
      constant: constraint.constant)

    newConstraint.priority = constraint.priority

    NSLayoutConstraint.deactivate([constraint])
    NSLayoutConstraint.activate([newConstraint])

    return newConstraint
  }
}

Uso, alterando o multiplicador para 1,2:

constraint = MyConstraint.changeMultiplier(constraint, multiplier: 1.2)
Evgenii
fonte
1
É aplicável a partir do iOS 8? ou pode ser utilizado em versões anteriores
Durai Amuthan.H
1
NSLayoutConstraint.deactivateA função é apenas para iOS 8.0 ou superior.
precisa
Por que você está devolvendo?
mskw
@mskw, a função retorna o novo objeto de restrição que ele cria. O anterior pode ser descartado.
Evgenii
42

Versão Objective-C para resposta de Andrew Schreiber

Crie a categoria para a classe NSLayoutConstraint e adicione o método no arquivo .h como este

    #import <UIKit/UIKit.h>

@interface NSLayoutConstraint (Multiplier)
-(instancetype)updateMultiplier:(CGFloat)multiplier;
@end

No arquivo .m

#import "NSLayoutConstraint+Multiplier.h"

@implementation NSLayoutConstraint (Multiplier)
-(instancetype)updateMultiplier:(CGFloat)multiplier {

    [NSLayoutConstraint deactivateConstraints:[NSArray arrayWithObjects:self, nil]];

   NSLayoutConstraint *newConstraint = [NSLayoutConstraint constraintWithItem:self.firstItem attribute:self.firstAttribute relatedBy:self.relation toItem:self.secondItem attribute:self.secondAttribute multiplier:multiplier constant:self.constant];
    [newConstraint setPriority:self.priority];
    newConstraint.shouldBeArchived = self.shouldBeArchived;
    newConstraint.identifier = self.identifier;
    newConstraint.active = true;

    [NSLayoutConstraint activateConstraints:[NSArray arrayWithObjects:newConstraint, nil]];
    //NSLayoutConstraint.activateConstraints([newConstraint])
    return newConstraint;
}
@end

Posteriormente, no ViewController, crie a saída para a restrição que você deseja atualizar.

@property (strong, nonatomic) IBOutlet NSLayoutConstraint *topConstraint;

e atualize o multiplicador onde quiser abaixo.

self.topConstraint = [self.topConstraint updateMultiplier:0.9099];
Koti Tummala
fonte
Mudei desativar neste código de exemplo como active = trueé o mesmo que ativar e, portanto, faz com que uma restrição duplicada seja adicionada antes que a desativação seja chamada, isso causa um erro de restrições em alguns cenários.
Jules
13

Você pode alterar a propriedade "constant" para obter o mesmo objetivo com um pouco de matemática. Suponha que seu multiplicador padrão na restrição seja 1.0f. Este é o código Xamarin C # que pode ser facilmente traduzido para o objetivo-c

private void SetMultiplier(nfloat multiplier)
{
    FirstItemWidthConstraint.Constant = -secondItem.Frame.Width * (1.0f - multiplier);
}
Tianfu Dai
fonte
esta é uma solução agradável
J. Doe
3
Isso será interrompido se o quadro do secondItem alterar a largura após a execução do código acima. Não há problema se o secondItem nunca mudar de tamanho, mas se o secondItem mudar de tamanho, a restrição não calculará mais o valor correto para o layout.
John Stephen
Conforme apontado por John Stephen, isso não funcionará para visualizações dinâmicas, dispositivos: não é atualizado automaticamente junto com o layout.
Womble
1
Ótima solução para o meu caso, em que a largura do segundo item é realmente [UIScreen mainScreen] .bounds.size.width (que nunca muda)
Arik Segal
7

Como está em outras respostas explicadas: Você precisa remover a restrição e criar uma nova.


Você pode evitar o retorno de nova restrição criando um método estático para NSLayoutConstraintcom o inoutparâmetro, o que permite reatribuir a restrição passada

import UIKit

extension NSLayoutConstraint {

    static func setMultiplier(_ multiplier: CGFloat, of constraint: inout NSLayoutConstraint) {
        NSLayoutConstraint.deactivate([constraint])

        let newConstraint = NSLayoutConstraint(item: constraint.firstItem, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: constraint.secondItem, attribute: constraint.secondAttribute, multiplier: multiplier, constant: constraint.constant)

        newConstraint.priority = constraint.priority
        newConstraint.shouldBeArchived = constraint.shouldBeArchived
        newConstraint.identifier = constraint.identifier

        NSLayoutConstraint.activate([newConstraint])
        constraint = newConstraint
    }

}

Exemplo de uso:

@IBOutlet weak var constraint: NSLayoutConstraint!

override func viewDidLoad() {
    NSLayoutConstraint.setMultiplier(0.8, of: &constraint)
    // view.layoutIfNeeded() 
}
Robert Dresler
fonte
2

NENHUM CÓDIGO ACIMA FUNCIONOU PARA MIM, APÓS TENTAR MODIFICAR MEU PRÓPRIO CÓDIGO ESTE CÓDIGO Este código está funcionando no Xcode 10 e no swift 4.2

import UIKit
extension NSLayoutConstraint {
/**
 Change multiplier constraint

 - parameter multiplier: CGFloat
 - returns: NSLayoutConstraintfor 
*/i
func setMultiplier(multiplier:CGFloat) -> NSLayoutConstraint {

    NSLayoutConstraint.deactivate([self])

    let newConstraint = NSLayoutConstraint(
        item: firstItem,
        attribute: firstAttribute,
        relatedBy: relation,
        toItem: secondItem,
        attribute: secondAttribute,
        multiplier: multiplier,
        constant: constant)

    newConstraint.priority = priority
    newConstraint.shouldBeArchived = self.shouldBeArchived
    newConstraint.identifier = self.identifier

    NSLayoutConstraint.activate([newConstraint])
    return newConstraint
}
}



 @IBOutlet weak var myDemoConstraint:NSLayoutConstraint!

override func viewDidLoad() {
let newMultiplier:CGFloat = 0.80
myDemoConstraint = myDemoConstraint.setMultiplier(newMultiplier)

//If later in view lifecycle, you may need to call view.layoutIfNeeded() 
 }
Ullas Pujary
fonte
Obrigado, parece funcionar bem
Radu Ursache 19/07
bem-vindo radu-ursache
Ullas Pujary
2

Sim, podemos alterar os valores do multiplicador, basta fazer uma extensão do NSLayoutConstraint e usá-lo como ->

   func setMultiplier(_ multiplier:CGFloat) -> NSLayoutConstraint {

        NSLayoutConstraint.deactivate([self])

        let newConstraint = NSLayoutConstraint(
            item: firstItem!,
            attribute: firstAttribute,
            relatedBy: relation,
            toItem: secondItem,
            attribute: secondAttribute,
            multiplier: multiplier,
            constant: constant)

        newConstraint.priority = priority
        newConstraint.shouldBeArchived = shouldBeArchived
        newConstraint.identifier = identifier

        NSLayoutConstraint.activate([newConstraint])
        return newConstraint
    }

            self.mainImageViewHeightMultiplier = self.mainImageViewHeightMultiplier.setMultiplier(375.0/812.0)
Rajan Singh
fonte
1

Mudar, alterando a restrição ativa no código, conforme sugerido por muitas outras respostas, não funcionou para mim. Então, eu criei 2 restrições, uma instalada e a outra não, vincule ambas ao código e mude removendo uma e adicionando a outra.

Por uma questão de completude, para vincular a restrição, arraste a restrição para o código usando o botão direito do mouse, como qualquer outro elemento gráfico:

insira a descrição da imagem aqui

Eu nomeei um proporcionadoIPad e outro proporcionadoIPhone.

Em seguida, adicione o seguinte código, em viewDidLoad

override open func viewDidLoad() {
   super.viewDidLoad()
   if ... {

       view.removeConstraint(proportionIphone)
       view.addConstraint(proportionIpad) 
   }     
}

Estou usando o xCode 10 e o swift 5.0

MiguelSlv
fonte
0

Aqui está uma resposta baseada na resposta do @ Tianfu em C #. Outras respostas que exigem ativação e desativação de restrições não funcionaram para mim.

    var isMapZoomed = false
@IBAction func didTapMapZoom(_ sender: UIButton) {
    let offset = -1.0*graphHeightConstraint.secondItem!.frame.height*(1.0 - graphHeightConstraint.multiplier)
    graphHeightConstraint.constant = (isMapZoomed) ? offset : 0.0

    isMapZoomed = !isMapZoomed
    self.view.layoutIfNeeded()
}
RawMean
fonte
0

Para alterar o valor do multiplicador, siga as etapas abaixo: 1. crie saídas para a restrição necessária, por exemplo, someConstraint. 2. altere o valor do multiplicador como someConstraint.constant * = 0,8 ou qualquer valor do multiplicador.

é isso, codificação feliz.

Gulshan Kumar
fonte
-1

Pode-se ler:

var multiplier: CGFloat
The multiplier applied to the second attribute participating in the constraint.

nesta página de documentação . Isso não significa que alguém deve ser capaz de modificar o multiplicador (já que é um var)?

Michel
fonte
var multiplier: CGFloat { get }é a definição que significa que ele tem apenas um getter.
Jonny