it-swarm.com.de

Emulieren des Aspektanpassungsverhaltens mit AutoLayout-Einschränkungen in Xcode 6

Ich möchte AutoLayout verwenden, um die Größe und das Layout einer Ansicht so zu ändern, dass sie an den an das Seitenverhältnis angepassten 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 ist als die Unteransicht, 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 einer früheren Version zu erreichen? Idealerweise mit Interface Builder, aber wenn nicht, ist es möglich, solche Einschränkungen programmgesteuert zu definieren.

327
chris838

Sie beschreiben keine maßstabsgetreue Darstellung. Sie beschreiben Aspekt-Fit. (Ich habe Ihre Frage in diesem Zusammenhang bearbeitet.) Die Unteransicht wird so groß wie möglich, behält jedoch ihr Seitenverhältnis bei und passt vollständig in die übergeordnete Ansicht.

Auf jeden Fall können Sie dies mit dem automatischen Layout tun. Ab Xcode 5.1 ist dies vollständig in IB möglich. Beginnen wir mit einigen Ansichten:

some views

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 festlegen, damit 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 vergrößert wird, während das Seitenverhältnis beibehalten wird und in das Bild passt Container.

Zunächst werden Einschränkungen für alle vier Seiten der blauen Ansicht erstellt. Ich werde es mit einem Abstand von 0 an den nächsten Nachbarn an jeder Kante anheften. Ich stelle sicher, dass die Ränder deaktiviert sind:

blue constraints

Beachten Sie, dass ich nicht den Frame noch aktualisiere. Ich finde es einfacher, beim Einrichten von Einschränkungen zwischen den Ansichten Platz zu lassen und die Konstanten 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.

pink constraints

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

enter image description here

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

containers laid out

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.

Das Anpassen der hellgrünen Ansicht an das Seitenverhältnis erfordert fünf Einschränkungen:

  • Eine Einschränkung für das Seitenverhältnis mit erforderlicher Priorität in der hellgrünen Ansicht. Sie können diese Einschränkung in einem XIB oder Storyboard mit Xcode 5.1 oder höher erstellen.
  • Eine Bedingung mit erforderlicher Priorität, die die Breite der hellgrünen Ansicht auf höchstens die Breite des Containers begrenzt.
  • Eine Einschränkung mit hoher Priorität, bei der die Breite der hellgrünen Ansicht der Breite ihres Containers entspricht.
  • Eine Bedingung mit erforderlicher Priorität, die die Höhe der hellgrünen Ansicht auf höchstens die Höhe des Containers begrenzt.
  • Eine Einschränkung mit hoher Priorität, bei der die Höhe der hellgrünen Ansicht der Höhe ihres Containers entspricht.

Betrachten wir die beiden Breitenbeschränkungen. Die Einschränkung „kleiner als oder gleich“ allein reicht nicht aus, um die Breite der hellgrünen Ansicht zu bestimmen. Viele Breiten passen zur Bedingung. 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. Das Minimieren des Fehlers bedeutet, die Breite so nah wie möglich an der Breite des Containers zu halten, ohne die geforderte Beschränkung von weniger als oder gleich zu verletzen.

Dasselbe passiert mit der Höhenbeschränkung. Da die Einschränkung des Seitenverhältnisses ebenfalls erforderlich ist, kann nur die Größe der Unteransicht entlang einer Achse maximiert werden (es sei denn, der Container hat zufällig das gleiche Seitenverhältnis wie die Unteransicht).

Also erstelle ich die Einschränkung des Seitenverhältnisses:

top aspect

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

top equal size

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

top less than or equal size

Als Nächstes muss ich mit dem Container einen weiteren Satz von Beschränkungen gleicher Breite und Höhe erstellen:

top equal size again

Und ich muss diese neuen Einschränkungen weniger als die erforderliche Priorität festlegen:

top equal not required

Schließlich haben Sie darum gebeten, dass die Unteransicht in ihrem Container zentriert wird, und ich werde diese Einschränkungen einrichten:

top centered

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

incorrect top layout

Hoppla! Die Unteransicht wurde erweitert, um den Container vollständig auszufüllen. Wenn ich es auswähle, kann ich sehen, dass es tatsächlich sein Seitenverhältnis beibehält, aber es macht einen Aspekt - füllen anstelle eines Aspekts - passen.

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

fix top constraints

Xcode aktualisiert das Layout:

correct top layout

Jetzt mache ich alle die gleichen Sachen mit der dunkelgrünen Ansicht auf der Unterseite. 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:

correct top and bottom 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 letztes Storyboard zu this Gist hochgeladen.

1037
rob mayoff

Rob, deine Antwort ist großartig! Ich weiß auch, dass es bei dieser Frage speziell darum geht, dies mithilfe von Auto-Layout zu erreichen. Ich möchte jedoch nur als Referenz zeigen, wie dies im Code erfolgen kann. Sie richten die obere und untere Ansicht (blau und pink) so ein, wie es Rob gezeigt hat. Dann erstellen Sie ein benutzerdefiniertes 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 legen Sie zwei Ausgänge für Ihren Viewcontroller topAspectFitView und bottomAspectFitView fest und legen deren childViews in viewDidLoad fest:

- (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 : Hier finden Sie eine Demo-App: https://github.com/jfahrenkrug/SPWKAspectFitView

24

Ich brauchte eine Lösung aus der akzeptierten Antwort, aber aus dem Code ausgeführt. Der eleganteste Weg, den ich gefunden habe, ist Mauerwerk-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);
}];
7
Tomasz Bąk

Dies ist für MacOS.

Ich habe ein Problem damit, Robs Weg zu nutzen, um eine Aspektanpassung in der OS X-Anwendung zu erreichen. Aber ich habe es auf andere Weise geschafft - anstatt Breite und Höhe zu verwenden, habe ich führenden, hinteren, oberen und unteren Raum verwendet.

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

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

Und dann ist die Arbeit erledigt!

enter image description hereenter image description here

4
brianLikeApple

Ich wollte das Aspekt -füllen -Verhalten, damit eine UIImageView ihr eigenes Seitenverhältnis beibehält und die Containeransicht vollständig ausfüllt. Verwirrenderweise hat mein UIImageView BEIDE Einschränkungen gleicher Breite und Höhe (beschrieben in Robs Antwort) mit hoher Priorität 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 gleicher Breite und Höhe festzulegen:

Content Compression Resistance

3
Gregor

Dies ist eine Portierung von @ rob_mayoffs hervorragender Antwort auf einen Code-zentrierten Ansatz, der NSLayoutAnchor Objekte verwendet und nach Xamarin portiert. Für mich haben NSLayoutAnchor und verwandte Klassen die Programmierung von AutoLayout wesentlich vereinfacht:

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
Larry OBrien

Vielleicht ist dies die kürzeste Antwort mit Mauerwerk , die auch das Ausfü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);
        }
    }
}];
1
iwill