Emulieren des Aspektanpassungsverhaltens mithilfe von AutoLayout-Einschränkungen in Xcode 6

336

Ich möchte AutoLayout verwenden, um eine Ansicht so zu dimensionieren und zu gestalten, dass sie an den aspektangepassten Inhaltsmodus von UIImageView erinnert.

Ich habe eine Unteransicht in einer Containeransicht in Interface Builder. Die Unteransicht hat ein inhärentes Seitenverhältnis, das ich respektieren möchte. Die Größe der Containeransicht ist bis zur Laufzeit unbekannt.

Wenn das Seitenverhältnis der Containeransicht breiter als die Unteransicht ist, soll die Höhe der Unteransicht der Höhe der übergeordneten Ansicht entsprechen.

Wenn das Seitenverhältnis der Containeransicht größer als die Unteransicht ist, soll die Breite der Unteransicht der Breite der übergeordneten Ansicht entsprechen.

In beiden Fällen möchte ich, dass die Unteransicht horizontal und vertikal in der Containeransicht zentriert wird.

Gibt es eine Möglichkeit, dies mithilfe von AutoLayout-Einschränkungen in Xcode 6 oder in früheren Versionen zu erreichen? Idealerweise mit Interface Builder, aber wenn nicht, ist es möglich, solche Einschränkungen programmgesteuert zu definieren.

chris838
quelle

Antworten:

1046

Sie beschreiben nicht Scale-to-Fit. Sie beschreiben Aspekt-Fit. (Ich habe Ihre Frage in dieser Hinsicht bearbeitet.) Die Unteransicht wird so groß wie möglich, während das Seitenverhältnis beibehalten wird und vollständig in das übergeordnete Element passt.

Auf jeden Fall können Sie dies mit automatischem Layout tun. Sie können dies vollständig in IB ab Xcode 5.1 tun. Beginnen wir mit einigen Ansichten:

einige Ansichten

Die hellgrüne Ansicht hat ein Seitenverhältnis von 4: 1. Die dunkelgrüne Ansicht hat ein Seitenverhältnis von 1: 4. Ich werde Einschränkungen so einrichten, dass die blaue Ansicht die obere Hälfte des Bildschirms ausfüllt, die rosa Ansicht die untere Hälfte des Bildschirms ausfüllt und jede grüne Ansicht so weit wie möglich erweitert wird, während das Seitenverhältnis beibehalten und in das Bild passt Container.

Zuerst werde ich Einschränkungen auf allen vier Seiten der blauen Ansicht erstellen. Ich werde es an jedem Rand mit einem Abstand von 0 an den nächsten Nachbarn heften. Ich stelle sicher, dass die Ränder ausgeschaltet sind:

blaue Einschränkungen

Beachten Sie, dass ich den Frame noch nicht aktualisiere. Ich finde es einfacher, beim Einrichten von Einschränkungen Platz zwischen den Ansichten zu lassen und die Konstanten einfach von Hand auf 0 (oder was auch immer) zu setzen.

Als nächstes stecke ich den linken, unteren und rechten Rand der rosa Ansicht an den nächsten Nachbarn. Ich muss keine Oberkantenbeschränkung einrichten, da die Oberkante bereits auf die Unterkante der blauen Ansicht beschränkt ist.

rosa Einschränkungen

Ich brauche auch eine Einschränkung gleicher Höhe zwischen der rosa und der blauen Ansicht. Dadurch füllen sie jeweils die Hälfte des Bildschirms aus:

Geben Sie hier die Bildbeschreibung ein

Wenn ich Xcode anweise, alle Frames jetzt zu aktualisieren, erhalte ich Folgendes:

Behälter ausgelegt

Die Einschränkungen, die ich bisher festgelegt habe, sind also korrekt. Ich mache das rückgängig und beginne mit der Arbeit an der hellgrünen Ansicht.

Die Aspektanpassung der hellgrünen Ansicht erfordert fünf Einschränkungen:

  • Eine Einschränkung des Seitenverhältnisses mit erforderlicher Priorität für die hellgrüne Ansicht. Sie können diese Einschränkung in einer XIB oder einem Storyboard mit Xcode 5.1 oder höher erstellen.
  • Eine Einschränkung mit erforderlicher Priorität, die die Breite der hellgrünen Ansicht auf weniger als oder gleich der Breite ihres Containers begrenzt.
  • Eine Einschränkung mit hoher Priorität, mit der die Breite der hellgrünen Ansicht gleich der Breite des Containers festgelegt wird.
  • Eine Einschränkung mit erforderlicher Priorität, die die Höhe der hellgrünen Ansicht auf weniger als oder gleich der Höhe ihres Containers begrenzt.
  • Eine Einschränkung mit hoher Priorität, mit der die Höhe der hellgrünen Ansicht gleich der Höhe des Containers festgelegt wird.

Betrachten wir die beiden Breitenbeschränkungen. Die weniger als oder gleiche Einschränkung allein reicht nicht aus, um die Breite der hellgrünen Ansicht zu bestimmen. Viele Breiten passen zur Einschränkung. Da es Unklarheiten gibt, versucht Autolayout, eine Lösung zu wählen, die den Fehler in der anderen Einschränkung (mit hoher Priorität, aber nicht erforderlich) minimiert. Um den Fehler zu minimieren, muss die Breite so nahe wie möglich an der Breite des Containers liegen, ohne die erforderliche Einschränkung zu verletzen, die kleiner oder gleich ist.

Das gleiche passiert mit der Höhenbeschränkung. Und da die Seitenverhältnisbeschränkung ebenfalls erforderlich ist, kann sie nur die Größe der Unteransicht entlang einer Achse maximieren (es sei denn, der Container hat zufällig das gleiche Seitenverhältnis wie die Unteransicht).

Also erstelle ich zuerst die Seitenverhältnisbeschränkung:

oberster Aspekt

Dann erstelle ich mit dem Container gleiche Breiten- und Höhenbeschränkungen:

oben gleich groß

Ich muss diese Einschränkungen so bearbeiten, dass sie weniger als oder gleich sind:

Oberseite kleiner oder gleich groß

Als nächstes muss ich einen weiteren Satz von Einschränkungen für Breite und Höhe mit dem Container erstellen:

oben wieder gleich groß

Und ich muss diese neuen Einschränkungen weniger als erforderlich machen:

oben gleich nicht erforderlich

Schließlich haben Sie darum gebeten, dass die Unteransicht in ihrem Container zentriert wird, damit ich die folgenden Einschränkungen einrichte:

oben zentriert

Zum Testen wähle ich jetzt den View Controller aus und fordere Xcode auf, alle Frames zu aktualisieren. Das bekomme ich:

falsches Top-Layout

Hoppla! Die Unteransicht wurde erweitert, um den Container vollständig zu füllen. Wenn ich es wählen, kann ich sehen , dass in der Tat ist es das Seitenverhältnis beibehalten wird , aber es ist ein aspekt tun Füllung anstelle eines aspekt fit .

Das Problem ist, dass es bei einer Einschränkung, die kleiner oder gleich ist, darauf ankommt, welche Ansicht sich an jedem Ende der Einschränkung befindet, und Xcode die Einschränkung entgegen meiner Erwartung eingerichtet hat. Ich könnte jede der beiden Einschränkungen auswählen und die ersten und zweiten Elemente umkehren. Stattdessen wähle ich einfach die Unteransicht aus und ändere die Einschränkungen so, dass sie größer oder gleich sind:

Beheben Sie die wichtigsten Einschränkungen

Xcode aktualisiert das Layout:

korrektes Top-Layout

Jetzt mache ich die gleichen Dinge mit der dunkelgrünen Ansicht unten. Ich muss sicherstellen, dass das Seitenverhältnis 1: 4 ist (Xcode hat die Größe auf seltsame Weise geändert, da es keine Einschränkungen gab). Ich werde die Schritte nicht mehr zeigen, da sie gleich sind. Hier ist das Ergebnis:

korrektes oberes und unteres Layout

Jetzt kann ich es im iPhone 4S-Simulator ausführen, der eine andere Bildschirmgröße als der verwendete IB hat, und die Rotation testen:

iPhone 4s Test

Und ich kann im iPhone 6 Simulator testen:

iPhone 6 Test

Ich habe mein endgültiges Storyboard zu Ihrer Übersicht hochgeladen .

Rob Mayoff
quelle
27
Ich habe LICEcap verwendet . Es ist kostenlos (GPL) und zeichnet direkt in GIF auf. Solange das animierte GIF nicht mehr als 2 MB groß ist, können Sie es wie jedes andere Bild auf dieser Site veröffentlichen.
Rob Mayoff
1
@LucaTorella Mit nur den Einschränkungen mit hoher Priorität ist das Layout nicht eindeutig. Nur weil es heute das gewünschte Ergebnis erzielt, heißt das nicht, dass es in der nächsten Version von iOS verfügbar sein wird. Angenommen, die Unteransicht möchte ein Seitenverhältnis von 1: 1, die Übersicht hat die Größe 99 × 100 und Sie haben nur die erforderlichen Einschränkungen für das Seitenverhältnis und die Gleichheit mit hoher Priorität. Dann kann das automatische Layout die Unteransicht auf Größe 99 × 99 (mit Gesamtfehler 1) oder Größe 100 × 100 (mit Gesamtfehler 1) einstellen. Eine Größe ist Aspekt-Fit; Das andere ist Aspektfüllung. Durch Hinzufügen der erforderlichen Einschränkungen wird eine eindeutige Lösung mit den geringsten Fehlern erstellt.
Rob Mayoff
1
@LucaTorella Vielen Dank für die Korrektur bezüglich der Verfügbarkeit von Seitenverhältnisbeschränkungen.
Rob Mayoff
11
10.000 bessere Apps mit nur einem Beitrag ... unglaublich.
DonVaughn
2
Wow .. Beste Antwort hier im Stapelüberlauf, den ich je gesehen habe .. Danke für diesen Sir ..
0yeoj
24

Rob, deine Antwort ist großartig! Ich weiß auch, dass es bei dieser Frage speziell darum geht, dies durch die Verwendung des automatischen Layouts zu erreichen. Als Referenz möchte ich jedoch zeigen, wie dies im Code möglich ist. Sie richten die Draufsicht und die Unteransicht (blau und pink) genau so ein, wie Rob es gezeigt hat. Dann erstellen Sie eine benutzerdefinierte 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

Als Nächstes ändern Sie die Klasse der blauen und rosa Ansichten im Storyboard in AspectFitViews. Schließlich setzen Sie zwei Auslässe auf Ihre Viewcontroller topAspectFitViewund bottomAspectFitViewund setzen ihre childViews in 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;
}

Es ist also nicht schwer, dies im Code zu tun, und es ist immer noch sehr anpassungsfähig und funktioniert mit Ansichten unterschiedlicher Größe und unterschiedlichen Seitenverhältnissen.

Update Juli 2015 : Eine Demo-App finden Sie hier: https://github.com/jfahrenkrug/SPWKAspectFitView

Johannes Fahrenkrug
quelle
1
@ DanielGalasko Danke, du bist richtig. Ich erwähne das in meiner Antwort. Ich dachte nur, es wäre eine nützliche Referenz, um zu vergleichen, welche Schritte in beiden Lösungen enthalten sind.
Johannes Fahrenkrug
1
Sehr hilfreich, um auch die programmatische Referenz zu haben.
Ctpenrose
@JohannesFahrenkrug Wenn Sie ein Tutorial-Projekt haben, können Sie teilen? Danke :)
Erhan Demirci
1
@ErhanDemirci Sicher, hier sind Sie: github.com/jfahrenkrug/SPWKAspectFitView
Johannes Fahrenkrug
8

Ich brauchte eine Lösung aus der akzeptierten Antwort, die aber aus dem Code ausgeführt wurde. Der eleganteste Weg, den ich gefunden habe, ist die Verwendung von Masonry Framework .

#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);
}];
Tomasz Bąk
quelle
5

Dies ist für macOS.

Ich habe Probleme, Robs Methode zu verwenden, um eine Aspektanpassung an die OS X-Anwendung zu erreichen. Aber ich habe es auf eine andere Weise geschafft - Anstatt Breite und Höhe zu verwenden, habe ich führenden, nachfolgenden, oberen und unteren Raum verwendet.

Fügen Sie grundsätzlich zwei führende Leerzeichen hinzu, wobei eines die erforderliche Priorität> = 0 @ 1000 und das andere die niedrige Priorität = 0 @ 250 hat. Nehmen Sie die gleichen Einstellungen für den hinteren, oberen und unteren Bereich vor.

Natürlich müssen Sie das Seitenverhältnis und das Zentrum X und das Zentrum Y einstellen.

Und dann ist die Arbeit erledigt!

Geben Sie hier die Bildbeschreibung ein Geben Sie hier die Bildbeschreibung ein

brianLikeApple
quelle
Wie könnten Sie dies programmgesteuert tun?
FateNuller
@FateNuller Befolgen Sie einfach die Schritte?
BrianLikeApple
4

Ich wollte ein Aspektfüllungsverhalten , damit eine UIImageView ihr eigenes Seitenverhältnis beibehält und die Containeransicht vollständig ausfüllt. Verwirrenderweise hat mein UIImageView BEIDE Einschränkungen mit hoher Priorität für gleiche Breite und gleiche Höhe (beschrieben in Robs Antwort) gebrochen und mit voller Auflösung gerendert.

Die Lösung bestand einfach darin, die Priorität des Inhaltskomprimierungswiderstands von UIImageView niedriger als die Priorität der Einschränkungen für gleiche Breite und gleiche Höhe festzulegen:

Komprimierungsbeständigkeit von Inhalten

Gregor
quelle
2

Dies ist eine Portierung von @ rob_mayoffs hervorragender Antwort auf einen codezentrierten Ansatz, bei dem NSLayoutAnchorObjekte verwendet und auf Xamarin portiert werden. Für mich NSLayoutAnchorund verwandte Klassen hat das Programmieren von AutoLayout viel einfacher gemacht:

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 ();
    }
}
Larry OBrien
quelle
1

Vielleicht ist dies die kürzeste Antwort mit Masonry , das auch das Füllen und Strecken von Aspekten unterstützt.

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);
        }
    }
}];
Herr Ming
quelle