Czy mogę zmienić właściwość mnożnika dla NSLayoutConstraint?

143

Utworzyłem dwa widoki w jednym superviewie, a następnie dodałem ograniczenia między widokami:

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

Teraz chcę zmienić właściwość mnożnika za pomocą animacji, ale nie mogę dowiedzieć się, jak zmienić właściwość mnożnika. (Znalazłem _coefficient we własności prywatnej w pliku nagłówkowym NSLayoutConstraint.h, ale jest to prywatne.)

Jak zmienić właściwość mnożnika?

Moje obejście polega na usunięciu starego ograniczenia i dodaniu nowego z inną wartością dla multipler.

Bimawa
źródło
1
Twoje obecne podejście polegające na usunięciu starego ograniczenia i dodaniu nowego jest właściwym wyborem. Rozumiem, że nie wydaje się to „właściwe”, ale tak właśnie powinno się to robić.
Rob
4
Myślę, że mnożnik nie powinien być stały. To zły projekt.
Borzh
1
@Borzh, dlaczego poprzez mnożniki tworzę ograniczenia adaptacyjne. W przypadku iOS o zmiennym rozmiarze.
Bimawa
1
Tak, chcę również zmienić mnożnik, aby widoki mogły zachować stosunek do widoku nadrzędnego (nie zarówno szerokość / wysokość, ale tylko szerokość lub wysokość), ale dziwna rzecz, której jabłko na to nie pozwala. Chodzi mi o to, że to zły projekt Apple, a nie twój.
Borzh
To świetna rozmowa, która obejmuje to i więcej youtube.com/watch?v=N5Ert6LTruY
DogCoffee

Odpowiedzi:

117

Jeśli masz tylko dwa zestawy mnożników, które należy zastosować, od iOS8 możesz dodać oba zestawy ograniczeń i zdecydować, które powinny być aktywne w dowolnym momencie:

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

Wersja 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
Jacek
źródło
2
@micnguyen Tak, możesz :)
Ja͢ck
7
Jeśli potrzebujesz więcej niż 2 zestawów ograniczeń, możesz dodać dowolną liczbę i zmienić ich właściwości priorytetu. W ten sposób dla 2 ograniczeń nie musisz ustawiać jednego jako aktywnego, a drugiego jako nieaktywnego, po prostu ustawiasz priorytet wyższy lub niższy. W powyższym przykładzie ustaw standardConstraint priorytet na, powiedzmy 997, a jeśli chcesz, aby był aktywny, po prostu ustaw priorytet zoomedConstraint na 995, w przeciwnym razie ustaw go na 999. Upewnij się również, że XIB nie ma priorytetów ustawionych na 1000, w przeciwnym razie nie będziesz w stanie ich zmienić, a będziesz miał niesamowitą awarię.
Dog
1
@WonderDog czy ma to jakąś szczególną zaletę? idiom włączania i wyłączania wydaje mi się łatwiejszy do strawienia niż konieczność wypracowania względnych priorytetów.
Ja͢ck,
2
@WonderDog / Jack Skończyło się na połączeniu twoich podejść, ponieważ moje ograniczenie zostało zdefiniowane w scenorysie. A gdybym stworzył dwa ograniczenia, oba z tym samym priorytetem, ale różnymi mnożnikami, były ze sobą sprzeczne i dały mi dużo czerwieni. Z tego, co mogę powiedzieć, nie można ustawić jednego jako nieaktywnego przez IB. Więc stworzyłem moje główne ograniczenie z priorytetem 1000 i moim drugorzędnym ograniczeniem, do którego chcę animować z priorytetem 999 (gra dobrze w IB). Następnie w bloku animacji ustawiłem właściwość active na wartość false i ładnie animowała się ona na drugą. Dzięki za pomoc!
Clay Garrett
2
Pamiętaj, że prawdopodobnie chcesz mieć silne odniesienia do swoich ograniczeń, jeśli je aktywujesz i dezaktywujesz, ponieważ „Aktywacja lub dezaktywacja wywołań ograniczeń addConstraint(_:)i removeConstraint(_:)poglądu, który jest najbliższym wspólnym przodkiem elementów zarządzanych przez to ograniczenie”.
qix
166

Oto rozszerzenie NSLayoutConstraint w Swift, które sprawia, że ​​ustawienie nowego mnożnika jest całkiem łatwe:

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

Użycie wersji demonstracyjnej:

@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
źródło
11
NSLayoutConstraint.deactivateConstraints([self])należy to zrobić przed utworzeniem newConstraint, w przeciwnym razie może to spowodować ostrzeżenie: Nie można jednocześnie spełnić ograniczeń . newConstraint.active = trueJest również niepotrzebne, ponieważ NSLayoutConstraint.activateConstraints([newConstraint])robi dokładnie to samo.
tzaloga,
1
aktywacja i dezaktywacja nie działa przed ios8, czy jest na to jakaś alternatywa ??
narahari_arjun
2
To nie działa po ponownej zmianie mnożnika, dostaje wartość zerową
J. Doe
4
Dzięki za zaoszczędzenie czasu! Zmieniłbym nazwę funkcji na taką, cloneWithMultiplieraby była bardziej wyraźna, że ​​nie tylko stosuje efekty uboczne, ale raczej zwraca nowe ograniczenie
Alexandre G
5
Pamiętaj, że jeśli ustawisz mnożnik na 0.0, nie będziesz mógł go ponownie zaktualizować.
Daniel Storm
58

Plik multiplierWłaściwość jest tylko do odczytu. Musisz usunąć stary NSLayoutConstraint i zastąpić go nowym, aby go zmodyfikować.

Ponieważ jednak wiesz, że chcesz zmienić mnożnik, możesz po prostu zmienić stałą, mnożąc ją samodzielnie, gdy potrzebne są zmiany, co często jest mniejszą ilością kodu.

Fruity Geek
źródło
64
Wydaje się dziwne, że mnożnik jest tylko do odczytu. Nie spodziewałem się tego!
jowie
135
@jowie to ironia losu, że „stałą” można zmienić, a mnożnika nie.
Tomas Andrle
7
To jest źle. NSLayoutConstraint określa afiniczne (nie) ograniczenie równości. Na przykład: „Widok1.bottomY = A * Widok2.bottomY + B” „A” to mnożnik, „B” to stała. Bez względu na to, jak dostroisz B, nie da to tego samego równania, co zmiana wartości A.
Tamás Zahola
8
@FruityGeek porządku, więc używasz View2.bottomY„s aktualną wartość do obliczenia ograniczenie w nowej stałej. Jest to wadliwe, ponieważ View2.bottomYstara wartość zostanie upieczona w stałej, więc musisz zrezygnować z deklaratywnego stylu i ręcznie aktualizować ograniczenie w dowolnym momencie View2.bottomY. W takim przypadku możesz również wykonać układ ręczny.
Tamás Zahola
2
To nie zadziała w przypadku, gdy chcę, aby NSLayoutConstraint odpowiadało na zmianę granic lub obrót urządzenia bez dodatkowego kodu.
tzaloga,
49

Funkcja pomocnicza, której używam do zmiany mnożnika istniejącego ograniczenia układu. Tworzy i aktywuje nowe ograniczenie i dezaktywuje stare.

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

Użycie, zmiana mnożnika na 1,2:

constraint = MyConstraint.changeMultiplier(constraint, multiplier: 1.2)
Evgenii
źródło
1
Czy to działa od iOS 8? lub może być używany we wcześniejszych wersjach
Durai Amuthan.H
1
NSLayoutConstraint.deactivatedziała tylko w systemie iOS 8.0+.
Evgenii
Dlaczego to zwracasz?
mskw
@mskw, funkcja zwraca nowy obiekt ograniczenia, który tworzy. Poprzedni można odrzucić.
Evgenii,
42

Wersja Objective-C dla odpowiedzi Andrew Schreibera

Utwórz kategorię dla klasy NSLayoutConstraint i dodaj metodę w pliku .h w ten sposób

    #import <UIKit/UIKit.h>

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

W pliku .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

Później w ViewController utwórz wylot dla ograniczenia, które chcesz zaktualizować.

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

i zaktualizuj mnożnik, gdzie chcesz, jak poniżej.

self.topConstraint = [self.topConstraint updateMultiplier:0.9099];
Koti Tummala
źródło
W tym przykładowym kodzie przeniosłem opcję dezaktywacji, która active = truejest taka sama jak aktywacja i dlatego powoduje dodanie zduplikowanego ograniczenia przed wywołaniem dezaktywacji, co powoduje błąd ograniczeń w niektórych scenariuszach.
Jules
13

Zamiast tego można zmienić właściwość „stała”, aby osiągnąć ten sam cel za pomocą odrobiny matematyki. Załóżmy, że domyślny mnożnik ograniczenia wynosi 1,0f. To jest kod Xamarin C #, który można łatwo przetłumaczyć na Objective-c

private void SetMultiplier(nfloat multiplier)
{
    FirstItemWidthConstraint.Constant = -secondItem.Frame.Width * (1.0f - multiplier);
}
Tianfu Dai
źródło
to jest fajne rozwiązanie
J. Doe
3
Spowoduje to zerwanie, jeśli szerokość ramki secondItem zmieni się po wykonaniu powyższego kodu. W porządku, jeśli secondItem nigdy nie zmieni rozmiaru, ale jeśli secondItem zmieni rozmiar, wówczas ograniczenie nie będzie już obliczać prawidłowej wartości dla układu.
John Stephen
Jak zauważył John Stephen, nie będzie to działać w przypadku dynamicznych widoków, urządzeń: nie aktualizuje się automatycznie wraz z układem.
Womble,
1
Świetne rozwiązanie dla mojego przypadku, w którym szerokość drugiego elementu to w rzeczywistości [UIScreen mainScreen] .bounds.size.width (to nigdy się nie zmienia)
Arik Segal
7

Jak w innych wyjaśnieniach: Musisz usunąć ograniczenie i utworzyć nowe.


Możesz uniknąć zwracania nowego ograniczenia, tworząc metodę statyczną dla NSLayoutConstraintz inoutparametrem, która umożliwia ponowne przypisanie przekazanego ograniczenia

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
    }

}

Przykładowe użycie:

@IBOutlet weak var constraint: NSLayoutConstraint!

override func viewDidLoad() {
    NSLayoutConstraint.setMultiplier(0.8, of: &constraint)
    // view.layoutIfNeeded() 
}
Robert Dresler
źródło
2

ŻADEN Z POWYŻSZEGO KODU NIE DZIAŁAŁ DLA MNIE TAK PO PRÓBIE ZMIANY MÓJ WŁASNEGO KODU TEN KOD Ten kod działa w Xcode 10 i szybkim 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
źródło
dzięki, wygląda na to, że działa dobrze
Radu Ursache
witamy radu-ursache
Ullas Pujary
2

Tak, możemy zmienić wartości mnożnika, po prostu utwórz rozszerzenie NSLayoutConstraint i użyj go jak ->

   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
źródło
1

Przełącz, zmieniając aktywne ograniczenie w kodzie, jak sugeruje wiele innych odpowiedzi, nie działa dla mnie. Utworzyłem więc 2 ograniczenia, jedno zainstalowane, a drugie nie, powiąż oba z kodem, a następnie przełącz się, usuwając jedno i dodając drugie.

Dla kompletności, aby związać ograniczenie, przeciągnij ograniczenie do kodu za pomocą prawego przycisku myszy, tak jak każdy inny element graficzny:

wprowadź opis obrazu tutaj

Nazwałam jedną proporcjęIPad, a drugą proporcjęIPhone.

Następnie dodaj następujący kod w viewDidLoad

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

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

Używam xCode 10 i Swift 5.0

MiguelSlv
źródło
0

Oto odpowiedź oparta na odpowiedzi @ Tianfu w C #. Inne odpowiedzi, które wymagają aktywacji i dezaktywacji ograniczeń, nie działały w moim przypadku.

    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
źródło
0

Aby zmienić wartość mnożnika, wykonaj następujące kroki: 1. utwórz wyloty dla wymaganego ograniczenia, np. SomeConstraint. 2. Zmień wartość mnożnika, np. SomeConstraint.constant * = 0.8 lub dowolną wartość mnożnika.

to wszystko, miłego kodowania.

Gulshan Kumar
źródło
-1

Można przeczytać:

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

na tej stronie dokumentacji . Czy to nie oznacza, że ​​należy umieć modyfikować mnożnik (ponieważ jest to zmienna)?

Michel
źródło
var multiplier: CGFloat { get }to definicja, która oznacza, że ​​ma tylko element pobierający.
Jonny