Układ automatyczny z ukrytymi widokami UIV?

164

Wydaje mi się, że dość powszechnym paradygmatem jest pokazywanie / ukrywanie UIViews, najczęściej w UILabelszależności od logiki biznesowej. Moje pytanie brzmi: jaki jest najlepszy sposób używania AutoLayout do reagowania na ukryte widoki, tak jakby ich ramka była 0x0. Oto przykład dynamicznej listy 1-3 funkcji.

Lista funkcji dynamicznych

W tej chwili mam górną przestrzeń od przycisku do ostatniej etykiety o wielkości 10 pikseli, która oczywiście nie przesunie się, gdy etykieta jest ukryta. W tej chwili stworzyłem ujście dla tego ograniczenia i zmodyfikowałem stałą w zależności od tego, ile etykiet wyświetlam. Jest to oczywiście trochę hakerskie, ponieważ używam ujemnych wartości stałych, aby naciskać przycisk nad ukrytymi ramkami. Jest również zły, ponieważ nie jest ograniczony do rzeczywistych elementów układu, tylko podstępne obliczenia statyczne oparte na znanych wysokościach / wypełnieniach innych elementów i oczywiście walka z tym, do czego został zbudowany AutoLayout.

Mógłbym oczywiście po prostu utworzyć nowe ograniczenia w zależności od moich dynamicznych etykiet, ale to dużo mikrozarządzania i dużo gadatliwości przy próbie zwinięcia niektórych białych znaków. Czy są lepsze podejścia? Zmieniasz rozmiar ramki 0,0 i pozwalasz, aby AutoLayout działało bez manipulacji ograniczeniami? Całkowicie usunąć widoki?

Szczerze mówiąc, sama modyfikacja stałej z kontekstu widoku ukrytego wymaga jednej linii kodu z prostymi obliczeniami. Odtwarzanie nowych ograniczeń w programie constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:wydaje się takie ciężkie.

Edycja lutego 2018 : Zobacz odpowiedź Bena z UIStackViews

Ryan Romanchuk
źródło
Dzięki Ryan za to pytanie. Szalałem, co zrobić w tych przypadkach, tak jak prosiłeś. Za każdym razem, gdy sprawdzam samouczek dotyczący autoukładu, większość z nich mówi, że odnosi się do witryny z samouczkami raywenderlich, która jest dla mnie trochę trudna do zrozumienia.
Nassif,

Odpowiedzi:

82

UIStackViewjest prawdopodobnie drogą do iOS 9+. Nie tylko obsługuje ukryty widok, ale także usuwa dodatkowe odstępy i marginesy, jeśli jest skonfigurowane poprawnie.

Ben Packard
źródło
8
Uważaj, używając UIStackView w UITableViewCell. Dla mnie, gdy widok stał się bardzo skomplikowany, przewijanie zaczęło się zacinać za każdym razem, gdy komórka była przewijana do wewnątrz / na zewnątrz. Pod okładkami StackView dodaje / usuwa ograniczenia i niekoniecznie będzie to wystarczająco wydajne do płynnego przewijania.
Kento
7
Byłoby miło podać mały przykład, jak radzi sobie z tym widok stosu.
Lostintranslation
@Kento czy to nadal problem w iOS 10?
Crashalot
3
Pięć lat później ... czy powinienem zaktualizować i ustawić to jako akceptowaną odpowiedź? Jest już dostępny przez trzy cykle wydawnicze.
Ryan Romanchuk
1
@RyanRomanchuk Powinieneś, to jest teraz zdecydowanie najlepsza odpowiedź. Dzięki Ben!
Duncan Luk
234

Moje osobiste preferencje dotyczące pokazywania / ukrywania widoków to tworzenie IBOutlet z odpowiednim ograniczeniem szerokości lub wysokości.

Następnie aktualizuję constantwartość, aby 0ukryć lub jakąkolwiek wartość powinna być pokazana.

Dużą zaletą tej techniki jest to, że zostaną zachowane względne ograniczenia. Na przykład, powiedzmy, że masz widok A i widok B z poziomą przerwą x . Gdy szerokość widoku A constantjest ustawiona na, 0.fwidok B przesunie się w lewo, aby wypełnić tę przestrzeń.

Nie ma potrzeby dodawania ani usuwania ograniczeń, co jest operacją ciężką. Wystarczy zaktualizować ograniczenie constant.

Max MacLeod
źródło
1
dokładnie. O ile - w tym przykładzie - umieszczasz widok B na prawo od widoku A za pomocą poziomej przerwy. Używam tej techniki cały czas i działa bardzo dobrze
Max MacLeod,
9
@MaxMacLeod Aby się upewnić: jeśli przerwa między dwoma widokami wynosi x, aby widok B zaczynał się od pozycji widoku A, musimy również zmienić stałą ograniczenia luki na 0, prawda?
kernix
1
@kernix tak, jeśli musiałoby istnieć poziome ograniczenie luki między dwoma widokami. stała 0 oczywiście oznacza brak przerwy. Więc ukrycie widoku A przez ustawienie jego szerokości na 0 przesunąłoby widok B w lewo, aby był równo z kontenerem. Przy okazji dzięki za głosy w górę!
Max MacLeod,
2
@MaxMacLeod Dzięki za odpowiedź. Próbowałem to zrobić z UIView, który zawiera inne widoki, ale podwidoki tego widoku nie są ukryte, gdy ustawię jego ograniczenie wysokości na 0. Czy to rozwiązanie powinno działać z widokami zawierającymi podwidoki? Dzięki!
shaunlim
6
Doceniłby również rozwiązanie, które działa dla UIView zawierającego inne widoki ...
Mark Gibaud,
96

Rozwiązanie polegające na użyciu stałej 0po ukryciu i innej stałej po jej ponownym wyświetleniu jest funkcjonalne, ale nie daje satysfakcji, jeśli treść ma elastyczny rozmiar. Musiałbyś zmierzyć swoją elastyczną zawartość i ustawić stałą wartość wsteczną. Wydaje się to niewłaściwe i powoduje problemy, jeśli zawartość zmienia rozmiar z powodu zdarzeń serwera lub interfejsu użytkownika.

Mam lepsze rozwiązanie.

Chodzi o to, aby reguła wysokości 0 miała wysoki priorytet, gdy ukryjemy element, aby nie zajmował miejsca na autoukładanie.

Oto jak to robisz:

1. ustaw szerokość (lub wysokość) na 0 w konstruktorze interfejsów z niskim priorytetem.

ustawienie reguły szerokości 0

Interface Builder nie będzie krzyczeć o konfliktach, ponieważ priorytet jest niski. Przetestuj zachowanie wysokości, ustawiając tymczasowo priorytet na 999 (1000 nie może programowo mutować, więc nie będziemy go używać). Konstruktor interfejsów prawdopodobnie będzie teraz krzyczeć o sprzecznych ograniczeniach. Możesz to naprawić, ustawiając priorytety na powiązanych obiektach na około 900.

2. Dodaj gniazdko, aby móc zmienić priorytet ograniczenia szerokości w kodzie:

przyłącze wylotowe

3. Dostosuj priorytet, gdy ukrywasz swój element:

cell.alertTimingView.hidden    = place.closingSoon != true
cell.alertTimingWidth.priority = place.closingSoon == true ? 250 : 999
SimplGy
źródło
@SimplGy, czy możesz mi powiedzieć, co to za place.closingSoon?
Niharika
To nie jest część ogólnego rozwiązania, @Niharika, tak właśnie była w moim przypadku zasada biznesowa (pokaż widok, jeśli restauracja wkrótce się zamknie).
SimplGy
11

W tym przypadku przyporządkowuję wysokość etykiety Autor do odpowiedniego IBOutlet:

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

a kiedy ustawiam wysokość ograniczenia na 0,0f, zachowujemy „wypełnienie”, ponieważ pozwala na to wysokość przycisku Odtwórz.

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

wprowadź opis obrazu tutaj

Mitul Marsoniya
źródło
9

Jest tu wiele rozwiązań, ale moje podejście jest znowu inne :)

Ustaw dwa zestawy ograniczeń podobne do odpowiedzi Jorge Arimany'ego i TMina:

wprowadź opis obrazu tutaj

Wszystkie trzy zaznaczone ograniczenia mają tę samą wartość dla stałej. Ograniczenia oznaczone A1 i A2 mają priorytet ustawiony na 500, podczas gdy ograniczenie oznaczone B ma priorytet ustawiony na 250 (lubUILayoutProperty.defaultLow w kodzie).

Podłącz ograniczenie B do IBOutlet. Następnie, gdy ukryjesz element, wystarczy ustawić priorytet ograniczenia na wysoki (750):

constraintB.priority = .defaultHigh

Ale kiedy element jest widoczny, ustaw priorytet z powrotem na niski (250):

constraintB.priority = .defaultLow

(Wprawdzie niewielka) przewaga tego podejścia w porównaniu ze zmianą tylko isActivedla ograniczenia B polega na tym, że nadal masz ograniczenie robocze, jeśli element przejściowy zostanie usunięty z widoku w inny sposób.

SeanR
źródło
Ma to tę zaletę, że nie ma zduplikowanych wartości w Storyboard i kodzie, takim jak wysokość / szerokość elementu. Bardzo elegancko.
Robin Daugherty
Dzięki!! Pomogło mi
Parth Barot
Świetne podejście, dziękuję
Basil
5

Podklasuj widok i nadpisz func intrinsicContentSize() -> CGSize. Po prostu wróć, CGSizeZerojeśli widok jest ukryty.

Daniel
źródło
2
To jest świetne. Niektóre elementy interfejsu użytkownika wymagają jednak wyzwalacza invalidateIntrinsicContentSize. Możesz to zrobić w trybie overridden setHidden.
DrMickeyLauer
5

Właśnie się dowiedziałem, że aby UILabel nie zajmował miejsca, musisz go ukryć ORAZ ustawić jego tekst na pusty ciąg. (iOS 9)

Znajomość tego faktu / błędu może pomóc niektórym ludziom uprościć ich układ, być może nawet ten z oryginalnego pytania, więc pomyślałem, że opublikuję go.

Matt Koala
źródło
1
Nie sądzę, żebyś musiał to ukrywać. Ustawienie tekstu na pusty ciąg powinno wystarczyć.
Rob VS,
4

Dziwię się, że nie ma bardziej eleganckiego podejścia UIKitdo tego pożądanego zachowania. Wydaje się, że bardzo często chce się móc to zrobić.

Odkąd łączenie ograniczeń IBOutletsi ustawianie ich stałych 0wydawało się obrzydliwe (i powodowało NSLayoutConstraintostrzeżenia, gdy twój widok miał podwidoki), zdecydowałem się stworzyć rozszerzenie, które daje proste, stanowe podejście do ukrywania / pokazywania, UIViewktóre ma ograniczenia Auto Layout

Po prostu ukrywa widok i usuwa zewnętrzne ograniczenia. Kiedy ponownie pokazujesz widok, dodaje on z powrotem ograniczenia. Jedynym zastrzeżeniem jest to, że musisz określić elastyczne ograniczenia pracy awaryjnej dla otaczających widoków.

Edytuj Ta odpowiedź jest skierowana do iOS 8.4 i starszych. W iOS 9 po prostu zastosuj UIStackViewpodejście.

Albert Bori
źródło
1
To także moje ulubione podejście. Jest to znacząca poprawa w stosunku do innych odpowiedzi, ponieważ nie ogranicza to tylko widoku do zera. Podejście do squasha będzie miało problemy z wszystkim, ale dość błahymi wypłatami. Przykładem tego jest duża luka, w której ukryty element znajduje się w innej odpowiedzi . I możesz skończyć z naruszeniem ograniczeń, jak wspomniano.
Benjohn
@DanielSchlaug Dzięki za wskazanie tego. Zaktualizowałem licencję do MIT. Jednak za każdym razem, gdy ktoś wdraża to rozwiązanie, wymagam mentalnej piątki.
Albert Bori
4

Najlepszą praktyką jest to, że gdy wszystko ma poprawne ograniczenia układu, dodaj wysokość lub z ograniczeniem, w zależności od tego, jak chcesz, aby otaczające widoki przesuwały się i łączyły ograniczenie z IBOutletwłaściwością.

Upewnij się, że Twoje właściwości są strong

w kodzie wystarczy ustawić stałą na 0 i aktywować ją, aby ukryć zawartość lub dezaktywować ją, aby wyświetlić zawartość. Jest to lepsze niż zepsucie stałej wartości i przywrócenie jej oszczędności. Nie zapomnij zadzwonić layoutIfNeededpóźniej.

Jeśli zawartość do ukrycia jest zgrupowana, najlepszą praktyką jest umieszczenie wszystkiego w widoku i dodanie ograniczeń do tego widoku

@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];
}

Gdy masz już swoją konfigurację, możesz ją przetestować w IntefaceBuilder, ustawiając ograniczenie na 0, a następnie przywróć pierwotną wartość. Nie zapomnij sprawdzić innych priorytetów ograniczeń, aby po ukryciu nie było żadnego konfliktu. Innym sposobem sprawdzenia tego jest ustawienie go na 0 i ustawienie priorytetu na 0, ale nie należy zapominać o przywróceniu go do najwyższego priorytetu.

Jorge Arimany
źródło
1
Dzieje się tak, ponieważ ograniczenie i widok nie zawsze mogły zostać zachowane przez hierarchię widoków, więc jeśli dezaktywujesz ograniczenie i ukryjesz widok, nadal je zachowujesz, aby wyświetlić je ponownie, niezależnie od tego, jaką hierarchię widoku zdecyduje się zachować, czy nie.
Jorge Arimany
2

Moja ulubiona metoda jest bardzo podobna do tej, którą sugerował Jorge Arimany.

Wolę tworzyć wiele ograniczeń. Najpierw utwórz ograniczenia, kiedy druga etykieta będzie widoczna. Utwórz wylot dla ograniczenia między przyciskiem a drugą etykietą (jeśli używasz objc, upewnij się, że jest mocny). To ograniczenie określa wysokość między przyciskiem a drugą etykietą, gdy jest ona widoczna.

Następnie utwórz kolejne ograniczenie określające wysokość między przyciskiem a górną etykietą, gdy drugi przycisk jest ukryty. Utwórz wylot dla drugiego ograniczenia i upewnij się, że ten punkt sprzedaży ma mocną wskazówkę. Następnie usuń zaznaczenie installedpola wyboru w narzędziu do tworzenia interfejsów i upewnij się, że priorytet pierwszego ograniczenia jest niższy niż priorytet drugiego ograniczenia.

Wreszcie, gdy ukryjesz drugą etykietę, przełącz .isActivewłaściwość tych ograniczeń i wywołajsetNeedsDisplay()

I to wszystko, bez magicznych liczb, bez matematyki, jeśli masz wiele ograniczeń dotyczących włączania i wyłączania, możesz nawet użyć kolekcji outletowych, aby uporządkować je według stanu. (AKA zachowaj wszystkie ukryte ograniczenia w jednym OutletCollection i nieukryte w innym i po prostu iteruj po każdej kolekcji, przełączając ich status .isActive).

Wiem, że Ryan Romanchuk powiedział, że nie chce używać wielu ograniczeń, ale wydaje mi się, że to nie jest mikrozarządzanie-y i jest prostsze, że dynamiczne tworzenie widoków i ograniczeń programowo (tego, jak sądzę, chciał uniknąć, jeśli ja dobrze czytam pytanie).

Stworzyłem prosty przykład, mam nadzieję, że się przyda ...

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

wprowadź opis obrazu tutaj

TMin
źródło
0

Podam również swoje rozwiązanie, aby zaoferować różnorodność.) Myślę, że tworzenie wylotu dla szerokości / wysokości każdego elementu plus odstępy jest po prostu śmieszne i wysadza kod, możliwe błędy i liczbę komplikacji.

Moja metoda usuwa wszystkie widoki (w moim przypadku instancje UIImageView), wybiera, które z nich należy dodać, a następnie w pętli dodaje z powrotem każdy i tworzy nowe ograniczenia. To naprawdę proste, proszę postępuj zgodnie z instrukcją. Oto mój szybki i brudny kod, aby to zrobić:

// 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);;
}];

Otrzymuję czysty, spójny układ i odstępy. Mój kod używa Masonry, zdecydowanie polecam: https://github.com/SnapKit/Masonry

Zoltán
źródło
0

Wypróbuj BoxView , dzięki czemu dynamiczny układ jest zwięzły i czytelny.
W Twoim przypadku jest to:

    boxView.optItems = [
        firstLabel.boxed.useIf(isFirstLabelShown),
        secondLabel.boxed.useIf(isSecondLabelShown),
        button.boxed
    ]
Vladimir
źródło