Disposizione automatica con UIVview nascoste?


164

Mi sembra che sia un paradigma abbastanza comune mostrare / nascondere UIViews, il più delle volte UILabels, a seconda della logica aziendale. La mia domanda è: qual è il modo migliore usando AutoLayout per rispondere alle viste nascoste come se il loro frame fosse 0x0. Ecco un esempio di un elenco dinamico di 1-3 funzioni.

Elenco delle funzionalità dinamiche

In questo momento ho uno spazio superiore di 10px dal pulsante all'ultima etichetta, che ovviamente non scivolerà verso l'alto quando l'etichetta è nascosta. A partire da ora ho creato uno sbocco a questo vincolo e modificando la costante a seconda di quante etichette sto visualizzando. Questo è ovviamente un po 'confuso dal momento che sto usando valori di costante negativi per spingere il pulsante verso l'alto sui frame nascosti. È anche negativo perché non è vincolato a elementi di layout reali, solo calcoli statici subdoli basati su altezze / imbottiture note di altri elementi e ovviamente combattendo contro ciò per cui AutoLayout è stato costruito.

Potrei ovviamente creare nuovi vincoli a seconda delle mie etichette dinamiche, ma questo è un sacco di microgestione e molta verbosità per aver provato a far crollare un po 'di spazio. Ci sono approcci migliori? Cambiare la dimensione del frame 0,0 e lasciare che AutoLayout faccia la sua cosa senza manipolazione dei vincoli? Rimozione completa delle viste?

Onestamente, tuttavia, la modifica della costante dal contesto della vista nascosta richiede una singola riga di codice con un semplice calcolo. Ricreare nuovi vincoli con constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:sembra così pesante.

Modifica febbraio 2018 : vedi la risposta di Ben con UIStackViews


Grazie Ryan per questa domanda. Stavo impazzendo cosa fare per i casi come mi hai chiesto. Ogni volta che cerco il tutorial per il autolayout, la maggior parte di loro dice di fare riferimento al sito del tutorial di raywenderlich che trovo un po 'difficile da capire.
Nassif,

Risposte:


82

UIStackViewè probabilmente la strada da percorrere per iOS 9+. Non solo gestisce la vista nascosta, ma rimuoverà anche la spaziatura e i margini aggiuntivi se impostato correttamente.


8
Fai attenzione quando usi UIStackView in un UITableViewCell. Per me, una volta che la vista è diventata molto complicata, lo scorrimento ha iniziato a diventare instabile dove balbettava ogni volta che una cella veniva inserita / estratta. Sotto le coperte StackView sta aggiungendo / rimuovendo i vincoli e questo non sarà necessariamente abbastanza efficiente per uno scorrimento regolare.
Kento,

7
Sarebbe bello dare un piccolo esempio di come lo stack view gestisce questo.
lostintranslation

@Kento è ancora un problema su iOS 10?
Crashalot,

3
Cinque anni dopo ... dovrei aggiornarlo e impostarlo come risposta accettata? Ora è disponibile per tre cicli di rilascio.
Ryan Romanchuk,

1
@RyanRomanchuk Dovresti, questa è sicuramente la migliore risposta ora. Grazie Ben!
Duncan Luk,

234

La mia preferenza personale per mostrare / nascondere le viste è quella di creare un IBOutlet con il vincolo di larghezza o altezza appropriato.

Quindi aggiorno il constantvalore da 0nascondere o qualunque sia il valore da mostrare.

Il grande vantaggio di questa tecnica è che verranno mantenuti i relativi vincoli. Ad esempio, supponiamo che tu abbia la vista A e la vista B con uno spazio orizzontale di x . Quando la larghezza della vista A constantè impostata su, la 0.fvista B si sposterà a sinistra per riempire quello spazio.

Non è necessario aggiungere o rimuovere vincoli, che è un'operazione pesante. Semplicemente l'aggiornamento dei vincoli constantfarà il trucco.


1
Esattamente. Fintanto che, in questo esempio, posizionate la vista B a destra della vista A usando uno spazio orizzontale. Uso sempre questa tecnica e funziona molto bene
Max MacLeod,

9
@MaxMacLeod Solo per essere sicuri: se lo spazio tra le due viste è x, affinché la vista B inizi dalla vista A, dobbiamo anche modificare la costante del vincolo di gap su 0, giusto?
Kernix,

1
@kernix Sì, se ci fosse bisogno di un vincolo di spazio orizzontale tra le due viste. costante 0 ovviamente significa nessuna lacuna. Quindi nascondere la vista A impostando la sua larghezza su 0 sposterebbe la vista B a sinistra per essere a filo con il contenitore. A proposito grazie per i voti positivi!
Max MacLeod

2
@MaxMacLeod Grazie per la risposta. Ho provato a farlo con un UIView che contiene altre viste, ma le viste secondarie di quella vista non sono nascoste quando ho impostato il suo limite di altezza su 0. Si suppone che questa soluzione funzioni con viste contenenti viste secondarie? Grazie!
shaunlim,

6
Gradirei anche una soluzione che funzioni per un UIView contenente altre viste ...
Mark Gibaud,

96

La soluzione di utilizzare una costante 0nascosta e un'altra costante se la si mostra di nuovo è funzionale, ma non è soddisfacente se il contenuto ha una dimensione flessibile. Dovresti misurare i tuoi contenuti flessibili e tornare indietro. Questo sembra sbagliato e ha problemi se il contenuto cambia dimensioni a causa di eventi del server o dell'interfaccia utente.

Ho una soluzione migliore

L'idea è di impostare la regola altezza a 0 per avere la massima priorità quando nascondiamo l'elemento in modo che non occupi spazio di autolayout.

Ecco come lo fai:

1. imposta una larghezza (o altezza) di 0 nel generatore di interfacce con una priorità bassa.

impostazione larghezza 0 regola

Interface Builder non urlerà per i conflitti perché la priorità è bassa. Prova il comportamento in altezza impostando temporaneamente la priorità su 999 (a 1000 è vietato mutare a livello di codice, quindi non lo useremo). Probabilmente ora il builder di interfacce urlerà vincoli contrastanti. Puoi risolverli impostando le priorità sugli oggetti correlati a circa 900.

2. Aggiungi uno sbocco in modo da poter modificare la priorità del vincolo di larghezza nel codice:

connessione di uscita

3. Regola la priorità quando nascondi l'elemento:

cell.alertTimingView.hidden    = place.closingSoon != true
cell.alertTimingWidth.priority = place.closingSoon == true ? 250 : 999

@SimplGy puoi per favore dirmi che cos'è questo place.closingSoon?
Niharika,

Non fa parte della soluzione generale, @Niharika, è proprio quello che era la regola aziendale nel mio caso (mostra la vista se il ristorante è / non chiuderà presto).
Semplicemente il

11

In questo caso, associo l'altezza dell'etichetta dell'autore a un IBOutlet appropriato:

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

e quando imposto l'altezza del vincolo su 0.0f, conserviamo il "padding", perché l'altezza del pulsante Play lo consente.

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

inserisci qui la descrizione dell'immagine


9

Ci sono molte soluzioni qui, ma il mio solito approccio è di nuovo diverso :)

Imposta due serie di vincoli simili alla risposta di Jorge Arimany e TMin:

inserisci qui la descrizione dell'immagine

Tutti e tre i vincoli contrassegnati hanno lo stesso valore per la costante. I vincoli contrassegnati A1 e A2 hanno la priorità impostata su 500, mentre il vincolo contrassegnato B ha la priorità impostata su 250 (o UILayoutProperty.defaultLownel codice).

Collegare il vincolo B a un IBOutlet. Quindi, quando nascondi l'elemento, devi solo impostare la priorità del vincolo su alta (750):

constraintB.priority = .defaultHigh

Ma quando l'elemento è visibile, riporta la priorità su bassa (250):

constraintB.priority = .defaultLow

Il vantaggio (certamente minore) di questo approccio rispetto alla semplice modifica isActivedel vincolo B è che si ha ancora un vincolo di funzionamento se l'elemento transitorio viene rimosso dalla vista con altri mezzi.


Ciò ha il vantaggio di non avere valori duplicati nello Storyboard e nel codice come altezza / larghezza dell'elemento. Molto elegante.
Robin Daugherty,

Grazie!! Mi ha aiutato
Parth Barot,

Grande approccio, grazie
Basilio

5

Sottoclasse la vista e sostituzione func intrinsicContentSize() -> CGSize. Ritorna CGSizeZerose la vista è nascosta.


2
Questo è fantastico invalidateIntrinsicContentSizeTuttavia, alcuni elementi dell'interfaccia utente richiedono un trigger . Puoi farlo in un override setHidden.
DrMickeyLauer

5

Ho appena scoperto che per far sì che un UILabel non occupi spazio, devi nasconderlo E impostarne il testo su una stringa vuota. (iOS 9)

Conoscere questo fatto / bug potrebbe aiutare alcune persone a semplificare i loro layout, forse anche quello della domanda originale, quindi ho pensato di pubblicarlo.


1
Non penso che tu debba nasconderlo. L'impostazione del testo su una stringa vuota dovrebbe essere sufficiente.
Rob VS

4

Sono sorpreso che non ci sia un approccio più elegante fornito da UIKitquesto comportamento desiderato. Sembra una cosa molto comune voler essere in grado di fare.

Dal momento che collegare i vincoli IBOutletse impostare le loro costanti in modo da sembrare sfortunato 0(e ha causato NSLayoutConstraintavvisi quando la vista aveva delle sottoview), ho deciso di creare un'estensione che offra un approccio semplice e con stile per nascondere / mostrare un UIViewche ha vincoli di layout automatico

Nasconde semplicemente la vista e rimuove i vincoli esterni. Quando si visualizza nuovamente la vista, vengono aggiunti nuovamente i vincoli. L'unica avvertenza è che è necessario specificare vincoli di failover flessibili per le viste circostanti.

Modifica Questa risposta è indirizzata a iOS 8.4 e versioni precedenti. In iOS 9, basta usare l' UIStackViewapproccio.


1
Anche questo è il mio approccio preferito. È un miglioramento significativo su altre risposte qui in quanto non si limita a ridurre la vista a dimensione zero. L'approccio di squash avrà bussare alle questioni per tutto tranne che per i pagamenti piuttosto banali. Un esempio di questo è il grande divario in cui l'elemento nascosto si trova in questa altra risposta . E puoi finire con le violazioni dei vincoli come menzionato.
Benjohn,

@DanielSchlaug Grazie per averlo segnalato. Ho aggiornato la licenza al MIT. Tuttavia, ho bisogno di un high-five mentale ogni volta che qualcuno implementa questa soluzione.
Albert Bori,

4

La migliore pratica è, una volta che tutto ha i vincoli di layout corretti, aggiungere un'altezza o con un vincolo, a seconda di come si desidera spostare le viste circostanti e connettere il vincolo a una IBOutletproprietà.

Assicurati che le tue proprietà lo siano strong

nel codice devi solo impostare la costante su 0 e attivarla , per nascondere il contenuto o disattivarlo per mostrare il contenuto. È meglio che rovinare il valore costante e salvarlo e ripristinarlo. Non dimenticare di chiamare in layoutIfNeededseguito.

Se il contenuto da nascondere è raggruppato, è consigliabile mettere tutto in una vista e aggiungere i vincoli a quella vista

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

Una volta che hai la tua configurazione, puoi testarla in IntefaceBuilder impostando il tuo vincolo su 0 e poi di nuovo sul valore originale. Non dimenticare di controllare le priorità di altri vincoli, quindi quando nascosto non c'è alcun conflitto. un altro modo per testarlo è metterlo su 0 e impostare la priorità su 0, ma non dimenticare di ripristinarlo nuovamente sulla massima priorità.


1
Questo perché il vincolo e la vista non possono sempre essere mantenuti dalla gerarchia della vista, quindi se si disattiva il vincolo e si nasconde la vista, li si conserva comunque per mostrarli di nuovo indipendentemente da ciò che la gerarchia della vista decide di conservare o meno.
Jorge Arimany,


2

Il mio metodo preferito è molto simile a quello suggerito da Jorge Arimany.

Preferisco creare più vincoli. Innanzitutto crea i tuoi vincoli per quando è visibile la seconda etichetta. Crea uno sbocco per il vincolo tra pulsante e la 2a etichetta (se stai usando objc assicurati che sia forte). Questo vincolo determina l'altezza tra il pulsante e la seconda etichetta quando è visibile.

Quindi crea un altro vincolo che specifica l'altezza tra il pulsante e l'etichetta superiore quando il secondo pulsante è nascosto. Creare uno sbocco al secondo vincolo e assicurarsi che questo sbocco abbia un puntatore forte. Quindi deseleziona ilinstalled casella di controllo nel generatore di interfacce e assicurati che la priorità del primo vincolo sia inferiore alla priorità di questo secondo vincolo.

Alla fine, quando nascondi la seconda etichetta, attiva la .isActiveproprietà di questi vincoli e chiamasetNeedsDisplay()

E il gioco è fatto, niente numeri magici, niente matematica, se hai più vincoli da attivare e disattivare, puoi persino usare le collezioni di outlet per mantenerli organizzati per stato. (AKA mantiene tutti i vincoli nascosti in un OutletCollection e quelli non nascosti in un altro e scorre semplicemente su ciascuna raccolta per attivare il proprio stato .isActive).

So che Ryan Romanchuk ha detto che non voleva usare più vincoli, ma penso che questo non sia micromanage-y, ed è più semplice che creare dinamicamente viste e vincoli a livello di codice (che è quello che penso che volesse evitare se io sto leggendo la domanda a destra).

Ho creato un semplice esempio, spero sia utile ...

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

inserisci qui la descrizione dell'immagine


0

Fornirò anche la mia soluzione, per offrire varietà.) Penso che creare uno sbocco per la larghezza / altezza di ogni articolo e per la spaziatura sia semplicemente ridicolo e fa esplodere il codice, i possibili errori e il numero di complicazioni.

Il mio metodo rimuove tutte le viste (nel mio caso istanze di UIImageView), seleziona quali devono essere aggiunte di nuovo e, in un ciclo, le aggiunge di nuovo e crea nuovi vincoli. In realtà è davvero semplice, ti preghiamo di seguire. Ecco il mio codice rapido e sporco per farlo:

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

Ottengo layout e spaziatura puliti e coerenti. Il mio codice utilizza Masonry, lo consiglio vivamente: https://github.com/SnapKit/Masonry


0

Prova BoxView , rende il layout dinamico conciso e leggibile.
Nel tuo caso è:

    boxView.optItems = [
        firstLabel.boxed.useIf(isFirstLabelShown),
        secondLabel.boxed.useIf(isSecondLabelShown),
        button.boxed
    ]
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.