我可以更改NSLayoutConstraint的乘数属性吗?

177

我在一个父视图中创建了两个子视图,并在它们之间添加了约束:

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

现在我想使用动画来更改乘数属性,但我无法弄清如何更改乘数属性。(我在头文件NSLayoutConstraint.h中的私有属性中找到了_coefficient,但它是私有的。)
我该如何更改乘数属性?
我的解决方法是删除旧约束并添加具有不同值的新约束multipler

1
你目前的方法是移除旧的限制并添加新的,这是正确的选择。我知道这感觉不太对,但这就是你应该做的方式。 - Rob
4
我认为倍增因子不应该是恒定的,这是一个糟糕的设计。 - Borzh
1
@Borzh 为了可调整大小的iOS,我通过乘数制定自适应约束。 - Bimawa
2
是的,我也想改变乘数,这样视图就可以保持与父视图的比例(不是宽度和高度都变化,而只是宽度或高度),但奇怪的是苹果不允许这样做。我的意思是这是苹果的设计问题,而不是你的问题。 - Borzh
@Bimawa:数学是站在你的对立面的。改变乘数意味着所有布局约束都必须再次完全解决。数学上来说,改变常数要简单得多。 - gnasher729
显示剩余3条评论
18个回答

205

以下是在Swift中的一个NSLayoutConstraint扩展,这使得设置新的乘数变得非常容易:

适用于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() 
}

13
在创建newConstraint之前,应该先执行NSLayoutConstraint.deactivateConstraints([self])以避免出现警告:_Unable to simultaneously satisfy constraints_(无法同时满足约束条件)。此外,newConstraint.active = true是不必要的,因为NSLayoutConstraint.activateConstraints([newConstraint])已经实现了相同的功能。 - zalogatomek
2
当再次更改乘数时,这种方法无法正常工作,它会得到一个空值。 - J. Doe
4
谢谢您节省我们的时间!我建议将该函数重命名为cloneWithMultiplier之类的名称,以使其更加清晰明了,表明它不仅应用副作用,而且返回一个新的约束。 - Alexandre G
1
这很棒,(就我所知)应该总是有效。但是有时旧的约束仍然保持活动状态,并且与新的约束冲突。即使在调用站点再次停用旧站点,只是为了分析。为什么已停用的约束会重新激活?当然还有其他方法,如更改优先级或标记未安装并在运行时安装... - Andrew Duncan
6
请注意,如果您将乘数设置为“0.0”,您将无法再次更新它。 - Daniel Storm
显示剩余14条评论

130

如果您只有两组需要应用的倍增器,从iOS8开始,您可以添加这两组约束,并决定任何时候哪一组应该处于活动状态:

如果您只有两组需要应用的倍增器,从 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
11
如果你需要超过两组限制条件,你可以添加任意多个并更改它们的优先级属性。这样,对于两个限制条件,你不必将一个设置为激活状态,另一个设置为非激活状态,而只需将优先级设置得更高或更低即可。在上面的示例中,将standardConstraint的优先级设置为997,当你想要它处于激活状态时,只需将zoomedConstraint的优先级设置为995,否则将其设置为999。还要确保XIB没有将优先级设置为1000,否则你将无法更改它们,并会导致应用崩溃。 - Dog
1
@WonderDog,但这样做有什么特别的优势吗?对我来说,启用和禁用的习惯用语似乎比计算相对优先级更容易理解。 - Ja͢ck
3
@WonderDog / Jack,我最终采用了你们两个的方法,因为我的限制是在故事板中定义的。如果我创建了两个限制,两者优先级相同但乘数不同,则它们会发生冲突并显示很多红色警告。而且据我所知,无法通过IB将一个限制设置为非活动状态。因此,我创建了一个主要的优先级为1000的约束和一个我想要动画化的辅助约束,其优先级为999(在IB中可以很好地实现)。然后,在动画块内,我将主要约束的“active”属性设置为false,并且它很好地动画到次要约束上。感谢你们两位的帮助! - Clay Garrett
3
请注意,在激活或停用约束时,您可能需要使用强引用来引用它们,因为“激活或停用约束会在管理此约束的项目的最近公共祖先视图上调用addConstraint(_:)removeConstraint(_:)”。 - qix
显示剩余5条评论

60

multiplier 属性只能被读取,如果要修改它,你需要移除旧的NSLayoutConstraint并替换为新的。

然而,由于你知道你想要改变乘数,当需要更改时,你可以通过自己计算常量的乘积来简化代码。


70
乘数器只读似乎有些奇怪,我没有预料到这点! - jowie
157
@jowie 这里有一个讽刺之处,就是“常量”值可以被更改,而乘数却不能。 - Tomas Andrle
7
这是错误的。NSLayoutConstraint指定了一个仿射(不等)约束条件,例如: “View1.bottomY = A * View2.bottomY + B” 其中'A'是乘数,'B'是常数。无论如何调整B,都不会产生与更改A的值相同的方程式。 - Tamás Zahola
9
好的,所以您正在使用View2.bottomY的当前值来计算约束的新常量。这是有缺陷的,因为 View2.bottomY 的旧值将被固定在常量中,因此您必须放弃声明式风格,并在每次View2.bottomY更改时手动更新约束。在这种情况下,您可能需要使用手动布局。 - Tamás Zahola
2
如果我想让NSLayoutConstraint在没有额外代码的情况下响应边界更改或设备旋转,那么这种方法将行不通。 - zalogatomek
显示剩余2条评论

53

我使用的一个辅助函数,用于更改现有布局约束的 乘数(multiplier)。它会创建并激活一个新的约束,并取消激活旧约束。

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 及以上版本。 - Evgenii
为什么你要返回它? - mskw
@mskw,该函数返回它创建的新约束对象。之前的对象可以被丢弃。 - Evgenii

45

Andrew Schreiber的答案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];

我已经在这个示例代码中将“deactivate”移动了,因为“active = true”与“activate”相同,因此在调用“deactivate”之前会添加重复的约束条件,这会在某些情况下导致约束错误。 - Jules

15

您可以改变“constant”属性,通过一点点的数学运算来达到相同的目的。假设您在约束上的默认乘数为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确实发生了大小变化,那么该约束将无法正确计算布局的值。 - John Stephen
正如John Stephen所指出的那样,对于动态视图和设备,它无法自动更新布局。 - Womble
2
非常棒的解决方案,适用于我的情况,其中第二个项目的宽度实际上是[UIScreen mainScreen].bounds.size.width(从不改变)。 - Arik Segal

13

正如其他答案所解释的那样:您需要删除限制并创建新的限制。


您可以通过为 NSLayoutConstraint 创建静态方法,并使用 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() 
}

10

Swift 5+

基于 Evgenii答案,这是一种通过 extension 改变 multiplier 的优雅方式。

extension NSLayoutConstraint {
    func change(multiplier: CGFloat) {
        let newConstraint = NSLayoutConstraint(item: firstItem,
                                               attribute: firstAttribute,
                                               relatedBy: relation,
                                               toItem: secondItem,
                                               attribute: secondAttribute,
                                               multiplier: multiplier,
                                               constant: constant)

        newConstraint.priority = self.priority

        NSLayoutConstraint.deactivate([self])
        NSLayoutConstraint.activate([newConstraint])
    }
}

以下是用法:

myConstraint.change(multiplier: 0.6)

该死的,这是最好的解决方案,毫无问题! - sabiland
这真是个太棒的解决方案,完全没有任何问题! - undefined

8

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

6

以上代码对我都不起作用,所以在尝试修改自己的代码后,这段代码已经可以在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() 
 }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接