AutoLayout mit versteckten UIViews?

164

Ich denke, es ist ein ziemlich verbreitetes Paradigma , das je nach Geschäftslogik UIViewsmeistens ein- UILabelsund ausgeblendet wird. Meine Frage ist, wie AutoLayout am besten verwendet wird, um auf ausgeblendete Ansichten zu reagieren, als ob ihr Frame 0x0 wäre. Hier ist ein Beispiel für eine dynamische Liste von 1-3 Funktionen.

Liste der dynamischen Funktionen

Im Moment habe ich einen oberen Bereich von 10 Pixel von der Schaltfläche bis zum letzten Etikett, der offensichtlich nicht nach oben rutscht, wenn das Etikett ausgeblendet ist. Ab sofort habe ich einen Ausgang für diese Einschränkung erstellt und die Konstante abhängig von der Anzahl der angezeigten Beschriftungen geändert. Dies ist offensichtlich etwas hackig, da ich negative konstante Werte verwende, um den Knopf über den versteckten Frames nach oben zu drücken. Es ist auch schlecht, weil es nicht auf tatsächliche Layoutelemente beschränkt ist, sondern nur hinterhältige statische Berechnungen, die auf bekannten Höhen / Auffüllungen anderer Elemente basieren und offensichtlich gegen das kämpfen, wofür AutoLayout entwickelt wurde.

Ich könnte natürlich nur neue Einschränkungen in Abhängigkeit von meinen dynamischen Beschriftungen erstellen, aber das ist viel Mikromanagement und viel Ausführlichkeit für den Versuch, nur ein Leerzeichen zu reduzieren. Gibt es bessere Ansätze? Ändern Sie die Frame-Größe 0,0 und lassen Sie AutoLayout ohne Manipulation von Einschränkungen arbeiten? Ansichten komplett entfernen?

Um ehrlich zu sein, erfordert das Ändern der Konstante aus dem Kontext der verborgenen Ansicht eine einzige Codezeile mit einfacher Berechnung. Das Wiederherstellen neuer Einschränkungen mit constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:scheint so schwer zu sein.

Edit Feb 2018 : Siehe Bens Antwort mit UIStackViews

Ryan Romanchuk
quelle
Danke Ryan für diese Frage. Ich war verrückt, was ich für die Fälle tun sollte, wie Sie gefragt haben. Jedes Mal, wenn ich nach dem Tutorial für Autolayout Ausschau halte, beziehen sich die meisten von ihnen auf die Raywenderlich-Tutorial-Site, die ich nur schwer herausfinden kann.
Nassif

Antworten:

82

UIStackViewist wahrscheinlich der richtige Weg für iOS 9+. Es verarbeitet nicht nur die ausgeblendete Ansicht, sondern entfernt bei korrekter Einrichtung auch zusätzliche Abstände und Ränder.

Ben Packard
quelle
8
Achten Sie darauf, wenn Sie UIStackView in einer UITableViewCell verwenden. Sobald die Ansicht sehr kompliziert wurde, wurde das Scrollen für mich abgehackt, wo es jedes Mal stotterte, wenn eine Zelle ein- oder ausgeblendet wurde. Unter der Decke fügt StackView Einschränkungen hinzu / entfernt diese, und dies ist nicht unbedingt effizient genug für ein reibungsloses Scrollen.
Kento
7
Es wäre schön, ein kleines Beispiel dafür zu geben, wie die Stapelansicht damit umgeht.
Verlorene Übersetzung
@Kento ist dies immer noch ein Problem unter iOS 10?
Crashalot
3
Fünf Jahre später ... sollte ich dies aktualisieren und als akzeptierte Antwort festlegen? Es ist jetzt seit drei Release-Zyklen verfügbar.
Ryan Romanchuk
1
@ RyanRomanchuk Du solltest, das ist jetzt definitiv die beste Antwort. Danke Ben!
Duncan Luk
234

Meine persönliche Präferenz für das Ein- und Ausblenden von Ansichten besteht darin, ein IBOutlet mit der entsprechenden Einschränkung für Breite oder Höhe zu erstellen.

Ich aktualisiere dann den constantWert, 0um ihn auszublenden, oder was auch immer der Wert sein soll, um ihn anzuzeigen.

Der große Vorteil dieser Technik besteht darin, dass relative Einschränkungen beibehalten werden. Angenommen, Sie haben Ansicht A und Ansicht B mit einer horizontalen Lücke von x . Wenn die Breite von Ansicht A festgelegt constantist, 0.fwird Ansicht B nach links verschoben, um diesen Bereich zu füllen.

Es ist nicht erforderlich, Einschränkungen hinzuzufügen oder zu entfernen, was eine schwere Operation ist. Das einfache Aktualisieren der Einschränkungen reicht aus constant.

Max MacLeod
quelle
1
genau. Solange Sie in diesem Beispiel Ansicht B mit einer horizontalen Lücke rechts von Ansicht A positionieren. Ich benutze diese Technik die ganze Zeit und es funktioniert sehr gut
Max MacLeod
9
@MaxMacLeod Nur um sicherzugehen: Wenn die Lücke zwischen den beiden Ansichten x ist, müssen wir auch die Lückenbeschränkungskonstante auf 0 ändern, damit die Ansicht B von der Position der Ansicht A ausgeht, oder?
Kernix
1
@kernix ja, wenn es eine horizontale Lückenbeschränkung zwischen den beiden Ansichten geben müsste. Konstante 0 bedeutet natürlich keine Lücke. Wenn Sie also Ansicht A durch Setzen der Breite auf 0 ausblenden, wird Ansicht B nach links verschoben, um bündig mit dem Container zu sein. Übrigens danke für die Up Votes!
Max MacLeod
2
@ MaxMacLeod Danke für deine Antwort. Ich habe versucht, dies mit einer UIView zu tun, die andere Ansichten enthält, aber die Unteransichten dieser Ansicht werden nicht ausgeblendet, wenn ich die Höhenbeschränkung auf 0 setze. Soll diese Lösung mit Ansichten funktionieren, die Unteransichten enthalten? Vielen Dank!
Shaunlim
6
Würde mich auch über eine Lösung freuen, die für eine UIView mit anderen Ansichten funktioniert ...
Mark Gibaud
96

Die Lösung, eine Konstante zu verwenden, 0wenn sie ausgeblendet ist, und eine andere Konstante, wenn Sie sie erneut anzeigen, ist funktional, aber unbefriedigend, wenn Ihr Inhalt eine flexible Größe hat. Sie müssten Ihren flexiblen Inhalt messen und eine Konstante zurücksetzen. Dies fühlt sich falsch an und hat Probleme, wenn sich die Größe des Inhalts aufgrund von Server- oder UI-Ereignissen ändert.

Ich habe eine bessere Lösung.

Die Idee ist, die Höhenregel 0 so festzulegen, dass sie eine hohe Priorität hat, wenn wir das Element ausblenden, damit es keinen Platz für die automatische Auslagerung beansprucht.

So machen Sie das:

1. Richten Sie im Interface Builder eine Breite (oder Höhe) von 0 mit niedriger Priorität ein.

Regel Breite 0 einstellen

Interface Builder schreit nicht über Konflikte, weil die Priorität niedrig ist. Testen Sie das Höhenverhalten, indem Sie die Priorität vorübergehend auf 999 setzen (1000 darf nicht programmgesteuert mutieren, daher wird es nicht verwendet). Der Interface Builder wird jetzt wahrscheinlich über widersprüchliche Einschränkungen schreien. Sie können diese Probleme beheben, indem Sie Prioritäten für verwandte Objekte auf etwa 900 festlegen.

2. Fügen Sie eine Steckdose hinzu, damit Sie die Priorität der Breitenbeschränkung im Code ändern können:

Steckdosenanschluss

3. Passen Sie die Priorität an, wenn Sie Ihr Element ausblenden:

cell.alertTimingView.hidden    = place.closingSoon != true
cell.alertTimingWidth.priority = place.closingSoon == true ? 250 : 999
Einfach
quelle
@ SimplGy Kannst du mir bitte sagen, was ist das für ein Ort?
Niharika
Es ist nicht Teil der allgemeinen Lösung, @Niharika. Es ist genau das, was die Geschäftsregel in meinem Fall war (zeigen Sie die Ansicht, ob das Restaurant bald schließt / nicht schließt).
SimplGy
11

In diesem Fall ordne ich die Höhe des Autorenetiketts einem geeigneten IBOutlet zu:

@property (retain, nonatomic) IBOutlet NSLayoutConstraint* authorLabelHeight;

und wenn ich die Höhe der Einschränkung auf 0.0f setze, behalten wir das "Auffüllen" bei, da die Höhe der Wiedergabetaste dies zulässt.

cell.authorLabelHeight.constant = 0; //Hide 
cell.authorLabelHeight.constant = 44; //Show

Geben Sie hier die Bildbeschreibung ein

Mitul Marsoniya
quelle
9

Hier gibt es viele Lösungen, aber mein üblicher Ansatz ist wieder anders :)

Richten Sie zwei Arten von Einschränkungen ein, die der Antwort von Jorge Arimany und TMin ähneln:

Geben Sie hier die Bildbeschreibung ein

Alle drei markierten Einschränkungen haben den gleichen Wert für die Konstante. Für die mit A1 und A2 gekennzeichneten Einschränkungen ist die Priorität auf 500 festgelegt, während für die mit B gekennzeichnete Einschränkung die Priorität auf 250 (oder UILayoutProperty.defaultLowim Code) festgelegt ist.

Verbinden Sie die Einschränkung B mit einem IBOutlet. Wenn Sie das Element ausblenden, müssen Sie nur die Einschränkungspriorität auf hoch (750) setzen:

constraintB.priority = .defaultHigh

Wenn das Element jedoch sichtbar ist, setzen Sie die Priorität wieder auf niedrig (250):

constraintB.priority = .defaultLow

Der (zugegebenermaßen geringfügige) Vorteil dieses Ansatzes gegenüber dem bloßen Ändern isActivefür Einschränkung B besteht darin, dass Sie immer noch eine funktionierende Einschränkung haben, wenn das vorübergehende Element auf andere Weise aus der Ansicht entfernt wird.

SeanR
quelle
Dies hat den Vorteil, dass in Storyboard und Code keine doppelten Werte wie Elementhöhe / -breite vorhanden sind. Sehr elegant.
Robin Daugherty
Vielen Dank!! Es hat mir geholfen
Parth Barot
Toller Ansatz, danke
Basil
5

Unterklassifizieren Sie die Ansicht und überschreiben Sie sie func intrinsicContentSize() -> CGSize. Kehren Sie einfach zurück, CGSizeZerowenn die Ansicht ausgeblendet ist.

Daniel
quelle
2
Das ist toll. Einige UI-Elemente benötigen jedoch einen Trigger invalidateIntrinsicContentSize. Sie können dies in einem überschriebenen tun setHidden.
DrMickeyLauer
5

Ich habe gerade herausgefunden, dass man ein UILabel ausblenden und seinen Text auf eine leere Zeichenfolge setzen muss, damit es keinen Speicherplatz beansprucht. (iOS 9)

Das Wissen um diese Tatsache / diesen Fehler könnte einigen Leuten helfen, ihre Layouts zu vereinfachen, möglicherweise sogar das der ursprünglichen Frage, also dachte ich mir, ich würde es posten.

Matt Koala
quelle
1
Ich glaube nicht, dass du es verstecken musst. Es sollte ausreichen, den Text auf eine leere Zeichenfolge zu setzen.
Rob VS
4

Ich bin überrascht, dass es UIKitfür dieses gewünschte Verhalten keinen eleganteren Ansatz gibt . Es scheint eine sehr häufige Sache zu sein, dies tun zu wollen.

Da das Verbinden von Einschränkungen mit IBOutletsund das Setzen ihrer Konstanten auf 0Glück (und NSLayoutConstraintWarnungen verursachte, wenn Ihre Ansicht Unteransichten hatte), habe ich beschlossen, eine Erweiterung zu erstellen, die einen einfachen, zustandsbehafteten Ansatz zum Ausblenden / UIViewAnzeigen von Einschränkungen mit automatischem Layout bietet

Es verbirgt lediglich die Ansicht und beseitigt äußere Einschränkungen. Wenn Sie die Ansicht erneut anzeigen, werden die Einschränkungen wieder hinzugefügt. Die einzige Einschränkung besteht darin, dass Sie flexible Failover-Einschränkungen für die umgebenden Ansichten festlegen müssen.

Bearbeiten Diese Antwort richtet sich an iOS 8.4 und niedriger. Verwenden Sie in iOS 9 einfach den UIStackViewAnsatz.

Albert Bori
quelle
1
Dies ist auch mein bevorzugter Ansatz. Dies ist eine signifikante Verbesserung gegenüber anderen Antworten, da die Ansicht nicht nur auf die Größe Null reduziert wird. Der Squash-Ansatz wird Probleme für alles andere als ziemlich triviale Auszahlungen haben. Ein Beispiel hierfür ist die große Lücke, in der sich das versteckte Element in dieser anderen Antwort befindet . Und Sie können wie erwähnt zu Verstößen gegen Einschränkungen kommen.
Benjohn
@ DanielSchlaug Danke, dass du darauf hingewiesen hast. Ich habe die Lizenz auf MIT aktualisiert. Ich benötige jedoch jedes Mal ein mentales High-Five, wenn jemand diese Lösung implementiert.
Albert Bori
4

Die beste Vorgehensweise ist, wenn alles die richtigen Layouteinschränkungen aufweist, eine Höhe oder eine Einschränkung hinzuzufügen, je nachdem, wie die umgebenden Ansichten verschoben werden sollen, und die Einschränkung mit einer IBOutletEigenschaft zu verbinden.

Stellen Sie sicher, dass Ihre Eigenschaften sind strong

Im Code müssen Sie nur die Konstante auf 0 setzen und aktivieren , um den Inhalt auszublenden oder zu deaktivieren , um den Inhalt anzuzeigen. Dies ist besser, als den konstanten Wert zu verfälschen und ihn zu speichern und wiederherzustellen. Vergessen Sie nicht, danach anzurufen layoutIfNeeded.

Wenn der auszublendende Inhalt gruppiert ist, empfiehlt es sich, alle Inhalte in eine Ansicht einzufügen und die Einschränkungen zu dieser Ansicht hinzuzufügen

@property (strong, nonatomic) IBOutlet UIView *myContainer;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *myContainerHeight; //should be strong!!

-(void) showContainer
{
    self.myContainerHeight.active = NO;
    self.myContainer.hidden = NO;
    [self.view layoutIfNeeded];
}
-(void) hideContainer
{
    self.myContainerHeight.active = YES;
    self.myContainerHeight.constant = 0.0f;
    self.myContainer.hidden = YES;
    [self.view layoutIfNeeded];
}

Sobald Sie Ihr Setup haben, können Sie es in IntefaceBuilder testen, indem Sie Ihre Einschränkung auf 0 und dann auf den ursprünglichen Wert zurücksetzen. Vergessen Sie nicht, die Prioritäten anderer Einschränkungen zu überprüfen, damit im versteckten Zustand überhaupt keine Konflikte auftreten. Sie können es auch testen, indem Sie es auf 0 setzen und die Priorität auf 0 setzen. Sie sollten jedoch nicht vergessen, die höchste Priorität wieder herzustellen.

Jorge Arimany
quelle
1
Dies liegt daran, dass die Einschränkung und die Ansicht nicht immer von der Ansichtshierarchie beibehalten werden können. Wenn Sie also die Einschränkung deaktivieren und die Ansicht ausblenden, behalten Sie sie weiterhin bei, um sie erneut anzuzeigen, unabhängig davon, was Ihre Ansichtshierarchie beibehalten möchte oder nicht.
Jorge Arimany
2

Meine bevorzugte Methode ist der von Jorge Arimany vorgeschlagenen sehr ähnlich.

Ich ziehe es vor, mehrere Einschränkungen zu erstellen. Erstellen Sie zunächst Ihre Einschränkungen, wenn das zweite Etikett sichtbar ist. Erstellen Sie eine Steckdose für die Einschränkung zwischen der Schaltfläche und dem zweiten Etikett (wenn Sie objc verwenden, stellen Sie sicher, dass es stark ist). Diese Einschränkung bestimmt die Höhe zwischen der Schaltfläche und der zweiten Beschriftung, wenn sie sichtbar ist.

Erstellen Sie dann eine weitere Einschränkung, die die Höhe zwischen der Schaltfläche und der oberen Beschriftung angibt, wenn die zweite Schaltfläche ausgeblendet ist. Erstellen Sie einen Ausgang für die zweite Einschränkung und stellen Sie sicher, dass dieser Ausgang einen starken Zeiger hat. Deaktivieren Sie dann das installedKontrollkästchen im Interface Builder und stellen Sie sicher, dass die Priorität der ersten Einschränkung niedriger als diese Priorität der zweiten Einschränkung ist.

Wenn Sie die zweite Beschriftung ausblenden, schalten Sie die .isActiveEigenschaft dieser Einschränkungen um und rufen Sie aufsetNeedsDisplay()

Und das war's, keine magischen Zahlen, keine Mathematik. Wenn Sie mehrere Einschränkungen zum Ein- und Ausschalten haben, können Sie sogar Outlet-Sammlungen verwenden, um sie nach Bundesstaaten zu organisieren. (AKA behält alle versteckten Einschränkungen in einer OutletCollection und die nicht versteckten in einer anderen bei und iteriert einfach über jede Sammlung, um ihren .isActive-Status umzuschalten.)

Ich weiß, dass Ryan Romanchuk sagte, er wolle nicht mehrere Einschränkungen verwenden, aber ich denke, dies ist kein Mikromanagement und es ist einfacher, Ansichten und Einschränkungen programmgesteuert dynamisch zu erstellen (was er meiner Meinung nach vermeiden wollte, wenn ich es tun würde lese die Frage richtig).

Ich habe ein einfaches Beispiel erstellt, ich hoffe es ist nützlich ...

import UIKit

class ViewController: UIViewController {

    @IBOutlet var ToBeHiddenLabel: UILabel!


    @IBOutlet var hiddenConstraint: NSLayoutConstraint!
    @IBOutlet var notHiddenConstraint: NSLayoutConstraint!


    @IBAction func HideMiddleButton(_ sender: Any) {

        ToBeHiddenLabel.isHidden = !ToBeHiddenLabel.isHidden
        notHiddenConstraint.isActive = !notHiddenConstraint.isActive
        hiddenConstraint.isActive = !hiddenConstraint.isActive

        self.view.setNeedsDisplay()
    }
}

Geben Sie hier die Bildbeschreibung ein

TMin
quelle
0

Ich werde auch meine Lösung anbieten, um Abwechslung zu bieten.) Ich finde es einfach lächerlich, eine Steckdose für die Breite / Höhe jedes Artikels plus für den Abstand zu erstellen, und sprengt den Code, die möglichen Fehler und die Anzahl der Komplikationen.

Meine Methode entfernt alle Ansichten (in meinem Fall UIImageView-Instanzen), wählt aus, welche wieder hinzugefügt werden müssen, und fügt in einer Schleife jede wieder hinzu und erstellt neue Einschränkungen. Es ist eigentlich ganz einfach, bitte folgen Sie durch. Hier ist mein schneller und schmutziger Code, um es zu tun:

// remove all views
[self.twitterImageView removeFromSuperview];
[self.localQuestionImageView removeFromSuperview];

// self.recipients always has to be present
NSMutableArray *items;
items = [@[self.recipients] mutableCopy];

// optionally add the twitter image
if (self.question.sharedOnTwitter.boolValue) {
    [items addObject:self.twitterImageView];
}

// optionally add the location image
if (self.question.isLocal) {
    [items addObject:self.localQuestionImageView];
}

UIView *previousItem;
UIView *currentItem;

previousItem = items[0];
[self.contentView addSubview:previousItem];

// now loop through, add the items and the constraints
for (int i = 1; i < items.count; i++) {
    previousItem = items[i - 1];
    currentItem = items[i];

    [self.contentView addSubview:currentItem];

    [currentItem mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.centerY.equalTo(previousItem.mas_centerY);
        make.right.equalTo(previousItem.mas_left).offset(-5);
    }];
}


// here I just connect the left-most UILabel to the last UIView in the list, whichever that was
previousItem = items.lastObject;

[self.userName mas_remakeConstraints:^(MASConstraintMaker *make) {
    make.right.equalTo(previousItem.mas_left);
    make.leading.equalTo(self.text.mas_leading);
    make.centerY.equalTo(self.attachmentIndicator.mas_centerY);;
}];

Ich bekomme ein sauberes, konsistentes Layout und Abstand. Mein Code verwendet Masonry. Ich empfehle es dringend: https://github.com/SnapKit/Masonry

Zoltán
quelle
0

Probieren Sie BoxView aus , es macht das dynamische Layout übersichtlich und lesbar.
In Ihrem Fall ist es:

    boxView.optItems = [
        firstLabel.boxed.useIf(isFirstLabelShown),
        secondLabel.boxed.useIf(isSecondLabelShown),
        button.boxed
    ]
Vladimir
quelle