Могу ли я изменить свойство множителя для NSLayoutConstraint?

151

Я создал два представления в одном супервизоре, а затем добавил ограничения между представлениями:

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

Теперь я хочу изменить свойство мультипликатора с помощью анимации, но не могу понять, как изменить свойство мультипликатора. (Я нашел _coefficient в частной собственности в файле заголовка NSLayoutConstraint.h, но это частное.)

Как изменить свойство мультиплексора?

Мой обходной путь - удалить старое ограничение и добавить новое с другим значением multipler.

Бимава
источник
1
Ваш текущий подход к удалению старого ограничения и добавлению нового - правильный выбор. Я понимаю, что это не кажется «правильным», но это то, как вы должны это делать.
Роб
4
Думаю, множитель не должен быть постоянным. Плохой дизайн.
Борж
1
@Borzh, почему через множители я делаю адаптивные ограничения. Для ios с изменяемым размером.
Бимава
1
Да, я также хочу изменить множитель, чтобы представления могли сохранять его соотношение с родительским представлением (не одновременно шириной / высотой, а только шириной или высотой), но странная вещь, которую яблоко не позволяет. Я имею в виду, что это плохой дизайн Apple, а не вашего.
Borzh
Это отличный доклад, в котором рассказывается об этом и
многом

Ответы:

121

Если у вас есть только два набора множителей, которые нужно применить, начиная с iOS8 вы можете добавить оба набора ограничений и решить, какой из них должен быть активен в любое время:

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 ...]

Версия 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
Джек
источник
2
@micnguyen Да, вы можете :)
Ja͢ck
7
Если вам нужно более двух наборов ограничений, вы можете добавить их сколько угодно и изменить их свойства приоритета. Таким образом, для двух ограничений вам не нужно устанавливать одно как активное, а другое как неактивное, вы просто устанавливаете приоритет выше или ниже. В приведенном выше примере установите приоритет standardConstraint, скажем, 997, и если вы хотите, чтобы он был активным, просто установите приоритет zoomedConstraint на 995, в противном случае установите его на 999. Также убедитесь, что XIB не имеет приоритетов, установленных на 1000, в противном случае вы не сможете их изменить, и вы получите потрясающий сбой.
Dog
1
У @WonderDog есть какое-то особое преимущество? идиома включения и выключения кажется мне более простой для понимания, чем необходимость определения относительных приоритетов.
Ja͢ck 06
2
@WonderDog / Jack Я объединил ваши подходы, потому что мои ограничения были определены в раскадровке. И если я создал два ограничения, оба с одинаковым приоритетом, но разными множителями, они конфликтовали и давали мне много красного. И из того, что я могу сказать, вы не можете сделать его неактивным через IB. Итак, я создал основное ограничение с приоритетом 1000 и второстепенное ограничение, которое я хочу анимировать с приоритетом 999 (отлично работает в IB). Затем внутри блока анимации я устанавливаю для активного свойства первичного объекта значение false, и оно прекрасно анимируется для вторичного. Спасибо обоим за помощь!
Клэй Гарретт
2
Имейте в виду, что вам, вероятно, понадобятся сильные ссылки на ваши ограничения при их активации и деактивации, поскольку «Активация или деактивация ограничений вызывает addConstraint(_:)и removeConstraint(_:)в представлении, которое является ближайшим общим предком элементов, управляемых этим ограничением».
Qix
171

Вот расширение NSLayoutConstraint в Swift, которое упрощает установку нового множителя:

В 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
    }
}

Использование демо:

@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() 
}
Эндрю Шрайбер
источник
12
NSLayoutConstraint.deactivateConstraints([self])должно быть выполнено перед созданием newConstraint, иначе это может привести к предупреждению: Невозможно одновременно удовлетворить ограничения . Также newConstraint.active = trueне требуется, поскольку NSLayoutConstraint.activateConstraints([newConstraint])делает то же самое.
tzaloga 05
1
активировать и деактивировать не работает до ios8, есть ли альтернатива этому ??
narahari_arjun
2
Это не работает при повторном изменении множителя, он получает нулевое значение
Дж. Доу
4
Спасибо, что сэкономили нам время! Я бы переименовал функцию во что-то вроде того, cloneWithMultiplierчтобы сделать ее более явной, что она не просто применяет побочные эффекты, а возвращает новое ограничение
Alexandre G
5
Обратите внимание: если вы установите множитель, 0.0вы не сможете обновить его снова.
Daniel Storm
58

multiplierСвойство только для чтения. Вам необходимо удалить старый NSLayoutConstraint и заменить его новым, чтобы изменить его.

Однако, поскольку вы знаете, что хотите изменить множитель, вы можете просто изменить константу, умножив ее самостоятельно, когда требуются изменения, что часто требует меньше кода.

Фруктовый Компьютерщик
источник
64
Кажется странным, что множитель доступен только для чтения. Я этого не ожидал!
jowie 08
140
@jowie иронично, что "постоянное" значение можно изменить, а множитель - нельзя.
Tomas Andrle
7
Это не правильно. NSLayoutConstraint определяет ограничение аффинного (не) равенства. Например: «View1.bottomY = A * View2.bottomY + B» 'A' - множитель, 'B' - константа. Независимо от того, как вы настраиваете B, это не приведет к тому же уравнению, что и изменение значения A.
Тамаш Захола
8
@FruityGeek хорошо, так что вы используете View2.bottomY«s текущего значение для расчета ограничения в новой константе. Это некорректно, поскольку View2.bottomYстарое значение будет записано в константу, поэтому вам придется отказаться от декларативного стиля и вручную обновлять ограничение при каждом View2.bottomYизменении. В этом случае вы также можете сделать макет вручную.
Тамаш Захола
2
Это не сработает, если я хочу, чтобы NSLayoutConstraint отвечал на изменение границ или ротацию устройства без дополнительного кода.
tzaloga 05
51

Вспомогательная функция, которую я использую для изменения множителя существующего ограничения макета. Он создает и активирует новое ограничение и деактивирует старое.

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

Использование, изменение множителя на 1,2:

constraint = MyConstraint.changeMultiplier(constraint, multiplier: 1.2)
Евгений
источник
1
Применимо ли это с iOS 8? или его можно использовать в более ранних версиях
Durai Amuthan.H
1
NSLayoutConstraint.deactivateфункция только iOS 8.0+.
Евгений
Почему ты его возвращаешь?
mskw
@mskw, функция возвращает новый объект ограничения, который она создает. От предыдущего можно отказаться.
Evgenii
42

Версия Objective-C для ответа Эндрю Шрайбера

Создайте категорию для класса NSLayoutConstraint и добавьте метод в файл .h следующим образом

    #import <UIKit/UIKit.h>

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

В файле .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

Позже в ViewController создайте выход для ограничения, которое вы хотите обновить.

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

и обновите множитель, где хотите, как показано ниже ..

self.topConstraint = [self.topConstraint updateMultiplier:0.9099];
Коти Туммала
источник
Я переместил деактивировать в этот пример кода, как active = trueи активировать, и поэтому вызывает добавление дублирующего ограничения перед вызовом деактивации, это вызывает ошибку ограничений в некоторых сценариях.
Жюль
13

Вместо этого вы можете изменить свойство «константа», чтобы достичь той же цели с помощью небольшой математики. Предположим, ваш множитель по умолчанию для ограничения равен 1.0f. Это код Xamarin C #, который можно легко перевести на objective-c

private void SetMultiplier(nfloat multiplier)
{
    FirstItemWidthConstraint.Constant = -secondItem.Frame.Width * (1.0f - multiplier);
}
Тяньфу Дай
источник
это хорошее решение
J. Doe
3
Это сломается, если рамка secondItem изменит ширину после выполнения приведенного выше кода. Это нормально, если secondItem никогда не изменит размер, но если secondItem действительно изменит размер, ограничение больше не будет вычислять правильное значение для макета.
Джон Стивен
Как указал Джон Стивен, это не будет работать для динамических представлений, устройств: оно не обновляется автоматически вместе с макетом.
Уомбл
1
Отличное решение для моего случая, когда ширина второго элемента на самом деле равна [UIScreen mainScreen] .bounds.size.width (которая никогда не меняется)
Арик Сегал,
8

Как объясняется в других ответах: вам нужно удалить ограничение и создать новое.


Вы можете избежать возврата нового ограничения, создав статический метод для параметра NSLayoutConstraintwith inout, который позволяет переназначить переданное ограничение

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
    }

}

Пример использования:

@IBOutlet weak var constraint: NSLayoutConstraint!

override func viewDidLoad() {
    NSLayoutConstraint.setMultiplier(0.8, of: &constraint)
    // view.layoutIfNeeded() 
}
Роберт Дреслер
источник
4

НИ ОДИН ИЗ ВЫШЕГО КОДА НЕ РАБОТАЕТ ДЛЯ МЕНЯ ТАК ПОСЛЕ ПОПЫТКИ ИЗМЕНИТЬ МОЙ СОБСТВЕННЫЙ КОД ЭТОТ КОД Этот код работает в Xcode 10 и 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() 
 }
Уллас Пуджари
источник
Потрясающие! Сэкономил мое время! 😁
Codetard
2

Да, w может изменять значения множителя, просто сделайте расширение 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 = shouldBeArchived
        newConstraint.identifier = identifier

        NSLayoutConstraint.activate([newConstraint])
        return newConstraint
    }

            self.mainImageViewHeightMultiplier = self.mainImageViewHeightMultiplier.setMultiplier(375.0/812.0)
Раджан Сингх
источник
1

Переключиться, изменив активное ограничение в коде, как было предложено многими другими ответами, для меня не сработало. Итак, я создал 2 ограничения, одно из которых установлено, а другое нет, привяжите оба к коду, а затем переключитесь, удалив одно и добавив другое.

Для полноты картины, чтобы привязать ограничение, перетащите ограничение в код с помощью правой кнопки мыши, как и любой другой графический элемент:

введите описание изображения здесь

Я назвал одну пропорцию IPad, а другую - IPhone.

Затем добавьте следующий код в viewDidLoad

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

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

Я использую xCode 10 и Swift 5.0

MiguelSlv
источник
1

в Swift 5.x вы можете использовать:

extension NSLayoutConstraint {
    func setMultiplier(multiplier: CGFloat) -> NSLayoutConstraint {
        guard let firstItem = firstItem else {
            return self
        }
        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
    }
}
Раду Урсаке
источник
0

Вот ответ, основанный на ответе @Tianfu на C #. Другие ответы, требующие активации и деактивации ограничений, у меня не работали.

    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
источник
0

Чтобы изменить значение множителя, выполните следующие действия: 1. Создайте выходы для требуемого ограничения, например someConstraint. 2. измените значение множителя, например someConstraint.constant * = 0.8 или любое значение множителя.

вот и все, счастливого кодирования.

Гульшан Кумар
источник
-2

Можно прочитать:

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

на этой странице документации . Разве это не означает, что нужно иметь возможность изменять множитель (поскольку это переменная)?

Мишель
источник
var multiplier: CGFloat { get }- это определение, которое означает, что у него есть только геттер.
Джонни