Emulazione del comportamento in base all'aspetto utilizzando i vincoli AutoLayout in Xcode 6


336

Voglio utilizzare AutoLayout per ridimensionare e impaginare una vista in un modo che ricorda la modalità di contenuto adattabile di UIImageView.

Ho una sottoview all'interno di una vista container in Interface Builder. La sottoview ha alcune proporzioni intrinseche che desidero rispettare. Le dimensioni della vista contenitore sono sconosciute fino al runtime.

Se le proporzioni della vista contenitore sono più ampie della vista secondaria, allora voglio che l'altezza della vista secondaria sia uguale all'altezza della vista padre.

Se le proporzioni della vista contenitore sono più alte della vista secondaria, allora voglio che la larghezza della vista secondaria sia uguale alla larghezza della vista padre.

In entrambi i casi, desidero che la vista secondaria sia centrata orizzontalmente e verticalmente all'interno della vista contenitore.

C'è un modo per raggiungere questo obiettivo usando i vincoli AutoLayout in Xcode 6 o nella versione precedente? Idealmente usando Interface Builder, ma se non forse è possibile definire tali vincoli a livello di codice.

Risposte:


1046

Non stai descrivendo la scala per adattarsi; stai descrivendo l'aspetto adatto. (Ho modificato la tua domanda al riguardo.) La sottoview diventa il più ampia possibile mantenendo le proporzioni e adattandosi completamente al suo genitore.

Ad ogni modo, puoi farlo con il layout automatico. Puoi farlo interamente in IB a partire da Xcode 5.1. Cominciamo con alcune visualizzazioni:

alcune viste

La vista verde chiaro ha un rapporto di formato 4: 1. La vista verde scuro ha un rapporto di 1: 4. Ho intenzione di impostare i vincoli in modo che la vista blu riempia la metà superiore dello schermo, la vista rosa riempia la metà inferiore dello schermo e ogni vista verde si espande il più possibile mantenendo le proporzioni e adattandosi al suo contenitore.

Innanzitutto, creerò vincoli su tutti e quattro i lati della vista blu. Lo appunterò al vicino più vicino su ciascun bordo, con una distanza di 0. Mi assicuro di disattivare i margini:

vincoli blu

Nota che non aggiorno ancora il frame. Trovo più facile lasciare spazio tra le viste quando si impostano i vincoli e basta impostare manualmente le costanti su 0 (o qualsiasi altra cosa).

Successivamente, fisso i bordi sinistro, inferiore e destro della vista rosa al vicino più vicino. Non ho bisogno di impostare un vincolo del bordo superiore perché il suo bordo superiore è già vincolato al bordo inferiore della vista blu.

vincoli rosa

Ho anche bisogno di un vincolo di uguale altezza tra le viste rosa e blu. Questo li farà riempire ciascuno metà dello schermo:

inserisci qui la descrizione dell'immagine

Se dico a Xcode di aggiornare tutti i frame ora, ottengo questo:

contenitori disposti

Quindi i vincoli che ho impostato finora sono corretti. Lo annullo e inizio a lavorare sulla vista verde chiaro.

L'adattamento alla vista verde chiaro richiede cinque vincoli:

  • Un vincolo delle proporzioni con priorità richiesta sulla vista verde chiaro. È possibile creare questo vincolo in un xib o storyboard con Xcode 5.1 o successivo.
  • Un vincolo di priorità richiesta che limita la larghezza della vista verde chiaro ad essere minore o uguale alla larghezza del suo contenitore.
  • Un vincolo ad alta priorità che imposta la larghezza della vista verde chiaro in modo che sia uguale alla larghezza del suo contenitore.
  • Un vincolo di priorità richiesta che limita l'altezza della vista verde chiaro in modo che sia inferiore o uguale all'altezza del suo contenitore.
  • Un vincolo ad alta priorità che imposta l'altezza della vista verde chiaro in modo che sia uguale all'altezza del suo contenitore.

Consideriamo i due vincoli di larghezza. Il vincolo minore o uguale, da solo, non è sufficiente per determinare la larghezza della vista verde chiaro; molte larghezze si adatteranno al vincolo. Dato che c'è ambiguità, il layout automatico proverà a scegliere una soluzione che minimizzi l'errore nell'altro vincolo (priorità alta ma non obbligatorio). Ridurre al minimo l'errore significa rendere la larghezza il più vicino possibile alla larghezza del contenitore, senza violare il vincolo minore o uguale richiesto.

La stessa cosa accade con il vincolo di altezza. E poiché è richiesto anche il vincolo delle proporzioni, può solo massimizzare le dimensioni della sottoview lungo un asse (a meno che il contenitore non abbia le stesse proporzioni della sottoview).

Quindi prima creo il vincolo delle proporzioni:

aspetto superiore

Quindi creo uguali vincoli di larghezza e altezza con il contenitore:

massima dimensione uguale

Devo modificare questi vincoli in modo che siano vincoli inferiori o uguali:

top di dimensioni inferiori o uguali

Quindi devo creare un altro set di vincoli di larghezza e altezza uguali con il contenitore:

di nuovo uguali

E ho bisogno di rendere questi nuovi vincoli meno della priorità richiesta:

massimo uguale non richiesto

Infine, hai chiesto che la sottoview sia centrata nel suo contenitore, quindi imposterò questi vincoli:

centrato in alto

Ora, per testare, selezionerò il controller di visualizzazione e chiederò a Xcode di aggiornare tutti i frame. Questo è quello che ottengo:

layout superiore errato

Oops! La sottoview si è espansa per riempire completamente il suo contenitore. Se lo seleziono, vedo che in effetti ha mantenuto le proporzioni, ma sta eseguendo un riempimento aspetto anziché un adattamento aspetto .

Il problema è che su un vincolo inferiore o uguale a, importa quale sia la vista a ciascuna estremità del vincolo e Xcode ha impostato il vincolo opposto alle mie aspettative. Potrei selezionare ciascuno dei due vincoli e invertire il suo primo e il secondo elemento. Invece, selezionerò semplicemente la vista secondaria e cambierò i vincoli in modo che siano maggiori o uguali:

correggere i vincoli principali

Xcode aggiorna il layout:

layout superiore corretto

Ora faccio tutte le stesse cose con la vista verde scuro in basso. Devo assicurarmi che le proporzioni siano 1: 4 (Xcode lo ha ridimensionato in modo strano poiché non aveva vincoli). Non mostrerò più i passaggi poiché sono gli stessi. Ecco il risultato:

layout superiore e inferiore corretti

Ora posso eseguirlo nel simulatore di iPhone 4S, che ha una dimensione dello schermo diversa da quella utilizzata da IB, e testare la rotazione:

test di iphone 4s

E posso provare nel simulatore di iPhone 6:

test di iphone 6

Ho caricato il mio storyboard finale su questa sintesi per tua comodità.


27
Ho usato LICEcap . È gratuito (GPL) e registra direttamente in GIF. Finché la GIF animata non supera i 2 MB, puoi pubblicarla su questo sito come qualsiasi altra immagine.
Rob Mayoff,

1
@LucaTorella Con solo i vincoli ad alta priorità, il layout è ambiguo. Solo perché ottiene il risultato desiderato oggi non significa che lo farà nella prossima versione di iOS. Supponiamo che la sottoview desideri proporzioni 1: 1, la superview abbia dimensioni 99 × 100 e che tu abbia solo le proporzioni richieste e i vincoli di uguaglianza ad alta priorità. Quindi il layout automatico può impostare la visualizzazione secondaria su dimensioni 99 × 99 (con errore totale 1) o dimensioni 100 × 100 (con errore totale 1). Una taglia è adatta all'aspetto; l'altro è aspetto pieno. L'aggiunta dei vincoli richiesti produce una soluzione unica con il minimo errore.
Rob Mayoff,

1
@LucaTorella Grazie per la correzione relativa alla disponibilità dei vincoli delle proporzioni.
Rob Mayoff,

11
10.000 app migliori con un solo post ... incredibile.
DonVaughn,

2
Wow .. La migliore risposta qui in overflow dello stack che abbia mai visto .. Grazie per questo signore ..
0yeoj

24

Rob, la tua risposta è fantastica! So anche che questa domanda riguarda specificamente il raggiungimento di questo obiettivo utilizzando il layout automatico. Tuttavia, proprio come riferimento, vorrei mostrare come ciò può essere fatto nel codice. Hai impostato le viste superiore e inferiore (blu e rosa) proprio come ha mostrato Rob. Quindi si crea un'abitudine AspectFitView:

AspectFitView.h :

#import <UIKit/UIKit.h>

@interface AspectFitView : UIView

@property (nonatomic, strong) UIView *childView;

@end

AspectFitView.m :

#import "AspectFitView.h"

@implementation AspectFitView

- (void)setChildView:(UIView *)childView
{
    if (_childView) {
        [_childView removeFromSuperview];
    }

    _childView = childView;

    [self addSubview:childView];
    [self setNeedsLayout];
}

- (void)layoutSubviews
{
    [super layoutSubviews];

    if (_childView) {
        CGSize childSize = _childView.frame.size;
        CGSize parentSize = self.frame.size;
        CGFloat aspectRatioForHeight = childSize.width / childSize.height;
        CGFloat aspectRatioForWidth = childSize.height / childSize.width;

        if ((parentSize.height * aspectRatioForHeight) > parentSize.height) {
            // whole height, adjust width
            CGFloat width = parentSize.width * aspectRatioForWidth;
            _childView.frame = CGRectMake((parentSize.width - width) / 2.0, 0, width, parentSize.height);
        } else {
            // whole width, adjust height
            CGFloat height = parentSize.height * aspectRatioForHeight;
            _childView.frame = CGRectMake(0, (parentSize.height - height) / 2.0, parentSize.width, height);
        }
    }
}

@end

Successivamente, cambi la classe delle viste blu e rosa nello storyboard in modo che sia AspectFitViews. Alla fine hai impostato due punti vendita sul tuo viewcontroller topAspectFitViewe hai bottomAspectFitViewimpostato i loro childViewin viewDidLoad:

- (void)viewDidLoad {
    [super viewDidLoad];

    UIView *top = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 100)];
    top.backgroundColor = [UIColor lightGrayColor];

    UIView *bottom = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 500)];
    bottom.backgroundColor = [UIColor greenColor];

    _topAspectFitView.childView = top;
    _bottomAspectFitView.childView = bottom;
}

Quindi non è difficile farlo nel codice ed è ancora molto adattabile e funziona con viste di dimensioni variabili e diverse proporzioni.

Aggiornamento luglio 2015 : trova un'app demo qui: https://github.com/jfahrenkrug/SPWKAspectFitView


1
@DanielGalasko Grazie, hai ragione. Lo menziono nella mia risposta. Ho solo pensato che sarebbe stato un utile riferimento per confrontare quali passaggi sono coinvolti in entrambe le soluzioni.
Johannes Fahrenkrug,

1
Molto utile avere anche il riferimento programmatico.
ctpenrose,

@JohannesFahrenkrug se hai un progetto tutorial, puoi condividerlo? grazie :)
Erhan Demirci,


8

Avevo bisogno di una soluzione dalla risposta accettata, ma eseguita dal codice. Il modo più elegante che ho trovato è usare il framework Masonry .

#import "Masonry.h"

...

[view mas_makeConstraints:^(MASConstraintMaker *make) {
    make.width.equalTo(view.mas_height).multipliedBy(aspectRatio);
    make.size.lessThanOrEqualTo(superview);
    make.size.equalTo(superview).with.priorityHigh();
    make.center.equalTo(superview);
}];

5

Questo è per macOS.

Ho un problema ad usare il modo di Rob per ottenere un aspetto adatto all'applicazione OS X. Ma l'ho fatto in un altro modo - Invece di usare larghezza e altezza, ho usato lo spazio iniziale, finale, superiore e inferiore.

Fondamentalmente, aggiungi due spazi iniziali dove uno è> = 0 @ 1000 priorità richiesta e un altro è = 0 @ 250 bassa priorità. Effettua le stesse impostazioni per lo spazio finale, superiore e inferiore.

Ovviamente, devi impostare le proporzioni e il centro X e il centro Y.

E poi il lavoro è fatto!

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine


Come potresti fare questo a livello di programmazione?
FateNuller,

@FateNuller Stai solo seguendo i passaggi?
brianLikeApple il

4

Mi sono trovato a desiderare un comportamento di riempimento aspetto in modo che UIImageView potesse mantenere le proprie proporzioni e riempire completamente la vista del contenitore. Confusamente, il mio UIImageView stava rompendo ENTRAMBI i vincoli di larghezza uguale e altezza uguale di priorità alta (descritti nella risposta di Rob) e il rendering a piena risoluzione.

La soluzione consisteva semplicemente nell'impostare la Priorità di resistenza alla compressione del contenuto di UIImageView su un valore inferiore alla priorità dei vincoli di uguale larghezza e uguale altezza:

Resistenza alla compressione del contenuto


2

Questa è una porta dell'eccellente risposta di @ rob_mayoff a un approccio incentrato sul codice, che utilizza NSLayoutAnchoroggetti e trasferito su Xamarin. Per me, NSLayoutAnchore le classi correlate hanno reso AutoLayout molto più facile da programmare:

public class ContentView : UIView
{
        public ContentView (UIColor fillColor)
        {
            BackgroundColor = fillColor;
        }
}

public class MyController : UIViewController 
{
    public override void ViewDidLoad ()
    {
        base.ViewDidLoad ();

        //Starting point:
        var view = new ContentView (UIColor.White);

        blueView = new ContentView (UIColor.FromRGB (166, 200, 255));
        view.AddSubview (blueView);

        lightGreenView = new ContentView (UIColor.FromRGB (200, 255, 220));
        lightGreenView.Frame = new CGRect (20, 40, 200, 60);

        view.AddSubview (lightGreenView);

        pinkView = new ContentView (UIColor.FromRGB (255, 204, 240));
        view.AddSubview (pinkView);

        greenView = new ContentView (UIColor.Green);
        greenView.Frame = new CGRect (80, 20, 40, 200);
        pinkView.AddSubview (greenView);

        //Now start doing in code the things that @rob_mayoff did in IB

        //Make the blue view size up to its parent, but half the height
        blueView.TranslatesAutoresizingMaskIntoConstraints = false;
        var blueConstraints = new []
        {
            blueView.LeadingAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.LeadingAnchor),
            blueView.TrailingAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.TrailingAnchor),
            blueView.TopAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.TopAnchor),
            blueView.HeightAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.HeightAnchor, (nfloat) 0.5)
        };
        NSLayoutConstraint.ActivateConstraints (blueConstraints);

        //Make the pink view same size as blue view, and linked to bottom of blue view
        pinkView.TranslatesAutoresizingMaskIntoConstraints = false;
        var pinkConstraints = new []
        {
            pinkView.LeadingAnchor.ConstraintEqualTo(blueView.LeadingAnchor),
            pinkView.TrailingAnchor.ConstraintEqualTo(blueView.TrailingAnchor),
            pinkView.HeightAnchor.ConstraintEqualTo(blueView.HeightAnchor),
            pinkView.TopAnchor.ConstraintEqualTo(blueView.BottomAnchor)
        };
        NSLayoutConstraint.ActivateConstraints (pinkConstraints);


        //From here, address the aspect-fitting challenge:

        lightGreenView.TranslatesAutoresizingMaskIntoConstraints = false;
        //These are the must-fulfill constraints: 
        var lightGreenConstraints = new []
        {
            //Aspect ratio of 1 : 5
            NSLayoutConstraint.Create(lightGreenView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, lightGreenView, NSLayoutAttribute.Width, (nfloat) 0.20, 0),
            //Cannot be larger than parent's width or height
            lightGreenView.WidthAnchor.ConstraintLessThanOrEqualTo(blueView.WidthAnchor),
            lightGreenView.HeightAnchor.ConstraintLessThanOrEqualTo(blueView.HeightAnchor),
            //Center in parent
            lightGreenView.CenterYAnchor.ConstraintEqualTo(blueView.CenterYAnchor),
            lightGreenView.CenterXAnchor.ConstraintEqualTo(blueView.CenterXAnchor)
        };
        //Must-fulfill
        foreach (var c in lightGreenConstraints) 
        {
            c.Priority = 1000;
        }
        NSLayoutConstraint.ActivateConstraints (lightGreenConstraints);

        //Low priority constraint to attempt to fill parent as much as possible (but lower priority than previous)
        var lightGreenLowPriorityConstraints = new []
         {
            lightGreenView.WidthAnchor.ConstraintEqualTo(blueView.WidthAnchor),
            lightGreenView.HeightAnchor.ConstraintEqualTo(blueView.HeightAnchor)
        };
        //Lower priority
        foreach (var c in lightGreenLowPriorityConstraints) 
        {
            c.Priority = 750;
        }

        NSLayoutConstraint.ActivateConstraints (lightGreenLowPriorityConstraints);

        //Aspect-fit on the green view now
        greenView.TranslatesAutoresizingMaskIntoConstraints = false;
        var greenConstraints = new []
        {
            //Aspect ratio of 5:1
            NSLayoutConstraint.Create(greenView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, greenView, NSLayoutAttribute.Width, (nfloat) 5.0, 0),
            //Cannot be larger than parent's width or height
            greenView.WidthAnchor.ConstraintLessThanOrEqualTo(pinkView.WidthAnchor),
            greenView.HeightAnchor.ConstraintLessThanOrEqualTo(pinkView.HeightAnchor),
            //Center in parent
            greenView.CenterXAnchor.ConstraintEqualTo(pinkView.CenterXAnchor),
            greenView.CenterYAnchor.ConstraintEqualTo(pinkView.CenterYAnchor)
        };
        //Must fulfill
        foreach (var c in greenConstraints) 
        {
            c.Priority = 1000;
        }
        NSLayoutConstraint.ActivateConstraints (greenConstraints);

        //Low priority constraint to attempt to fill parent as much as possible (but lower priority than previous)
        var greenLowPriorityConstraints = new []
        {
            greenView.WidthAnchor.ConstraintEqualTo(pinkView.WidthAnchor),
            greenView.HeightAnchor.ConstraintEqualTo(pinkView.HeightAnchor)
        };
        //Lower-priority than above
        foreach (var c in greenLowPriorityConstraints) 
        {
            c.Priority = 750;
        }

        NSLayoutConstraint.ActivateConstraints (greenLowPriorityConstraints);

        this.View = view;

        view.LayoutIfNeeded ();
    }
}

1

Forse questa è la risposta più breve, con Masonry , che supporta anche il riempimento dell'aspetto e l'estensione.

typedef NS_ENUM(NSInteger, ContentMode) {
    ContentMode_aspectFit,
    ContentMode_aspectFill,
    ContentMode_stretch
}

// ....

[containerView addSubview:subview];

[subview mas_makeConstraints:^(MASConstraintMaker *make) {
    if (contentMode == ContentMode_stretch) {
        make.edges.equalTo(containerView);
    }
    else {
        make.center.equalTo(containerView);
        make.edges.equalTo(containerView).priorityHigh();
        make.width.equalTo(content.mas_height).multipliedBy(4.0 / 3); // the aspect ratio
        if (contentMode == ContentMode_aspectFit) {
            make.width.height.lessThanOrEqualTo(containerView);
        }
        else { // contentMode == ContentMode_aspectFill
            make.width.height.greaterThanOrEqualTo(containerView);
        }
    }
}];
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.