¿Puedo cambiar la propiedad del multiplicador para NSLayoutConstraint?

143

Creé dos vistas en una supervista, y luego agregué restricciones entre las vistas:

_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];

Ahora quiero cambiar la propiedad del multiplicador con animación, pero no puedo entender cómo cambiar la propiedad del multiplicador. (Encontré _coefficient en propiedad privada en el archivo de cabecera NSLayoutConstraint.h, pero es privado).

¿Cómo cambio la propiedad del multiplicador?

Mi solución es eliminar la restricción anterior y agregar la nueva con un valor diferente para multipler.

Bimawa
fuente
1
Su enfoque actual de eliminar la restricción anterior y agregar una nueva es la opción correcta. Entiendo que no se siente "bien", pero es la forma en que se supone que debes hacerlo.
Rob
44
Creo que el multiplicador no debería ser constante. Es un mal diseño.
Borzh
1
@Borzh por qué a través de multiplicadores hago restricciones adaptativas. Para ios redimensionables.
Bimawa
1
Sí, también quiero cambiar el multiplicador para que las vistas puedan mantener su relación con la vista principal (no tanto ancho / alto sino solo ancho o alto), pero lo extraño es que Apple no lo permite. Quiero decir que es el mal diseño de Apple, no el tuyo.
Borzh
Esta es una gran charla y cubre esto y más youtube.com/watch?v=N5Ert6LTruY
DogCoffee

Respuestas:

117

Si solo tiene dos conjuntos de multiplicadores que deben aplicarse, desde iOS8 en adelante puede agregar ambos conjuntos de restricciones y decidir cuál debe estar activo en cualquier 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 ...]

Versión 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
Jack
fuente
2
@micnguyen Sí, puedes :)
Ja͢ck
77
Si necesita más de 2 conjuntos de restricciones, puede agregar tantos como desee y cambiar sus propiedades de prioridad. De esta manera, para 2 restricciones, no tiene que configurar una como activa y la otra como inactiva, simplemente establece la prioridad más alta o más baja. En el ejemplo anterior, establezca la prioridad StandardConstraint en, digamos 997, y cuando desee que esté activa, simplemente establezca la prioridad de zoomedConstraint en 995, de lo contrario, establezca en 999. También asegúrese de que el XIB no tenga las prioridades establecidas en 1000, de lo contrario no podrás cambiarlos y obtendrás un choque impresionante.
Perro
1
@WonderDog, ¿eso tiene alguna ventaja particular? El modismo de habilitar y deshabilitar me parece más fácil de digerir que tener que resolver prioridades relativas.
Ja͢ck
2
@WonderDog / Jack Terminé combinando tus enfoques porque mi restricción se definió en el guión gráfico. Y si creé dos restricciones, ambas con la misma prioridad pero multiplicadores diferentes, entraron en conflicto y me dieron mucho rojo. Y por lo que puedo decirte, no puedes configurar uno como inactivo a través de IB. Así que creé mi restricción principal con prioridad 1000 y mi restricción secundaria a la que quiero animar con prioridad 999 (funciona bien en IB). Luego, dentro de un bloque de animación, configuré la propiedad activa del primario en falso y animé muy bien al secundario. Gracias a ambos por la ayuda!
Clay Garrett
2
Tenga en cuenta que probablemente quiera referencias fuertes a sus restricciones si las activa y desactiva, ya que "Activar o desactivar las llamadas de restricción addConstraint(_:)y removeConstraint(_:)en la vista que es el ancestro común más cercano de los elementos administrados por esta restricción".
qix
166

Aquí hay una extensión NSLayoutConstraint en Swift que hace que configurar un nuevo multiplicador sea bastante fácil:

En Swift 3.0+

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 demostración:

@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
fuente
11
NSLayoutConstraint.deactivateConstraints([self])debe hacerse antes de crear newConstraint, de lo contrario puede dar lugar a una advertencia: no se pueden satisfacer las restricciones simultáneamente . También newConstraint.active = truees innecesario ya que NSLayoutConstraint.activateConstraints([newConstraint])hace exactamente lo mismo.
tzaloga
1
activar y desactivar no funciona antes de ios8, ¿hay alguna alternativa para eso?
narahari_arjun
2
Esto no funciona al cambiar el multiplicador nuevamente, obtiene un valor nulo
J. Doe
44
¡Gracias por ahorrarnos tiempo! Cambiaría el nombre de la función a algo como cloneWithMultiplierhacerlo más explícito que no solo aplica efectos secundarios, sino que devuelve una nueva restricción
Alexandre G el
55
Tenga en cuenta que si configura el multiplicador 0.0no podrá actualizarlo nuevamente.
Daniel Storm
58

La multiplierpropiedad es de solo lectura. Debe eliminar la antigua NSLayoutConstraint y reemplazarla por una nueva para modificarla.

Sin embargo, dado que sabe que desea cambiar el multiplicador, puede cambiar la constante multiplicándola usted mismo cuando sea necesario realizar cambios, que a menudo es menos código.

Geek afrutado
fuente
64
Parece extraño que el multiplicador sea de solo lectura. ¡No esperaba eso!
jowie
135
@jowie es irónico que el valor "constante" se pueda cambiar mientras que el multiplicador no.
Tomás Andrle
77
Esto está mal. Una NSLayoutConstraint especifica una restricción de igualdad afín (in). Por ejemplo: "View1.bottomY = A * View2.bottomY + B" 'A' es el multiplicador, 'B' es la constante. No importa cómo sintonice B, no producirá la misma ecuación que cambiar el valor de A.
Tamás Zahola
8
@FruityGeek está bien, entonces estás usando View2.bottomYel valor actual para calcular la nueva constante de la restricción . Eso es defectuoso, ya que View2.bottomYel valor anterior se horneará en la constante, por lo que debe renunciar al estilo declarativo y actualizar manualmente la restricción en cualquier momento que View2.bottomYcambie. En este caso, también puede hacer el diseño manual.
Tamás Zahola
2
Esto no funcionará en el caso, cuando quiero que NSLayoutConstraint responda para el cambio de límites o la rotación del dispositivo sin código adicional.
tzaloga
49

Una función auxiliar que uso para cambiar el multiplicador de una restricción de diseño existente. Crea y activa una nueva restricción y desactiva la anterior.

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, cambiando el multiplicador a 1.2:

constraint = MyConstraint.changeMultiplier(constraint, multiplier: 1.2)
Evgenii
fuente
1
¿Es aplicable desde iOS 8? o puede usarse en versiones anteriores
Durai Amuthan. H
1
NSLayoutConstraint.deactivatela función es solo iOS 8.0+.
Evgenii
¿Por qué lo devuelves?
mskw
@mskw, la función devuelve el nuevo objeto de restricción que crea. El anterior puede ser descartado.
Evgenii
42

Versión Objective-C para la respuesta de Andrew Schreiber

Cree la categoría para la clase NSLayoutConstraint y agregue el método en un archivo .h como este

    #import <UIKit/UIKit.h>

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

En el archivo .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

Más adelante en ViewController, cree la salida para la restricción que desea actualizar.

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

y actualiza el multiplicador donde quieras como a continuación.

self.topConstraint = [self.topConstraint updateMultiplier:0.9099];
Koti Tummala
fuente
He movido desactivar en este código de muestra, ya que active = truees lo mismo que activar y, por lo tanto, hace que se agregue una restricción duplicada antes de que se llame a desactivar, esto causa un error de restricciones en algunos escenarios.
Julio
13

En su lugar, puede cambiar la propiedad "constante" para lograr el mismo objetivo con un poco de matemática. Suponga que su multiplicador predeterminado en la restricción es 1.0f. Este es el código Xamarin C # que se puede traducir fácilmente a Objective-C

private void SetMultiplier(nfloat multiplier)
{
    FirstItemWidthConstraint.Constant = -secondItem.Frame.Width * (1.0f - multiplier);
}
Tianfu Dai
fuente
esta es una buena solución
J. Doe
3
Esto se romperá si el marco de secondItem cambia de ancho después de ejecutar el código anterior. Está bien si secondItem nunca cambiará de tamaño, pero si secondItem sí cambia de tamaño, la restricción ya no calculará el valor correcto para el diseño.
John Stephen el
Como señaló John Stephen, esto no funcionará para vistas dinámicas, dispositivos: no se actualiza automáticamente junto con el diseño.
Womble
1
Gran solución para mi caso, en el que el ancho del segundo elemento es en realidad [UIScreen mainScreen] .bounds.size.width (que nunca cambia)
Arik Segal
7

Como se explica en otras respuestas: debe eliminar la restricción y crear una nueva.


Puede evitar devolver una nueva restricción creando un método estático para NSLayoutConstraintcon el inoutparámetro, que le permite reasignar la restricción pasada

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
    }

}

Ejemplo de uso:

@IBOutlet weak var constraint: NSLayoutConstraint!

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

NINGUNO DEL CÓDIGO ANTERIOR TRABAJÓ PARA MÍ, DESPUÉS DE INTENTAR MODIFICAR MI PROPIO CÓDIGO ESTE CÓDIGO Este código funciona en Xcode 10 y 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
fuente
gracias, parece funcionar bien
Radu Ursache
bienvenido radu-ursache
Ullas Pujary
2

Sí, puede cambiar los valores del multiplicador, simplemente haga una extensión de NSLayoutConstraint y úsela 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
fuente
1

Cambie cambiando la restricción activa en el código como lo sugieren muchas otras respuestas no funcionó para mí. Así que creé 2 restricciones, una instalada y la otra no, unir ambas al código y luego cambiar eliminando una y agregando la otra.

En aras de la integridad, para vincular la restricción, arrastre la restricción al código con el botón derecho del mouse, como cualquier otro elemento gráfico:

ingrese la descripción de la imagen aquí

Llamé una proporción IPad y la otra proporción IPhone.

Luego, agregue el siguiente código, en viewDidLoad

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

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

Estoy usando xCode 10 y swift 5.0

MiguelSlv
fuente
0

Aquí hay una respuesta basada en la respuesta de @ Tianfu en C #. Otras respuestas que requieren activación y desactivación de restricciones no funcionaron para mí.

    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
fuente
0

Para cambiar el valor del multiplicador, siga los pasos a continuación: 1. cree salidas para la restricción requerida, por ejemplo, someConstraint. 2. cambie el valor del multiplicador como someConstraint.constant * = 0.8 o cualquier valor multiplicador.

eso es todo, feliz codificación.

Gulshan Kumar
fuente
-1

Uno puede leer:

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

en esta página de documentación . ¿No significa eso que uno debería poder modificar el multiplicador (ya que es una var)?

Michel
fuente
var multiplier: CGFloat { get }es la definición que significa que solo tiene un captador.
Jonny