Емуляція поведінки відповідно до аспектів за допомогою обмежень AutoLayout у Xcode 6


336

Я хочу використовувати функцію AutoLayout для розміру та розміщення представлення таким чином, що нагадує режим вмісту, що відповідає змісту UIImageView.

У мене є перегляд всередині перегляду контейнера в програмі Interface Builder. У підрозділу є якесь властиве співвідношення сторін, яке я хочу поважати. Розмір перегляду контейнера невідомий до часу виконання.

Якщо співвідношення сторін подання контейнера ширше, ніж підпогляд, то я хочу, щоб висота підперегляду дорівнювала висоті батьківського подання.

Якщо співвідношення сторін подання контейнера вище, ніж підпогляд, то я хочу, щоб ширина підпогляду дорівнювала ширині батьківського подання.

В будь-якому випадку я хочу, щоб підперегляд був орієнтований горизонтально та вертикально в межах подання контейнера.

Чи є спосіб досягти цього за допомогою обмежень AutoLayout в Xcode 6 або в попередній версії? Ідеально використовувати Interface Builder, але якщо ні, можливо, можна визначити такі обмеження програмно.

Відповіді:


1046

Ви не описуєте масштаб; ви описуєте відповідність аспекту. (Я відредагував ваше питання з цього приводу.) Підрозділ стає максимально великим, зберігаючи його співвідношення сторін і повністю розміщуючись всередині його батьківського.

У будь-якому випадку це можна зробити за допомогою автоматичного макета. Ви можете це зробити повністю в ІБ від Xcode 5.1. Почнемо з деяких поглядів:

деякі погляди

Світло-зелений вигляд має співвідношення сторін 4: 1. Темно-зелений вигляд має співвідношення сторін 1: 4. Я збираюся встановити обмеження, щоб блакитний вигляд заповнював верхню половину екрана, рожевий вигляд заповнював нижню половину екрана, а кожен зелений вид максимально розширювався, зберігаючи його співвідношення сторін і вписуючись у його контейнер.

По-перше, я створять обмеження з усіх чотирьох сторін синього зору. Я зафіксую його до найближчого сусіда на кожному краю, з відстані 0. Я обов'язково вимикаю поля:

сині обмеження

Зауважте, що я ще не оновлював кадр. Мені легше залишити місце між видами при встановленні обмежень, а просто встановити константи на 0 (або що завгодно) вручну.

Далі я закріплюю лівий, нижній і правий краї рожевого виду до найближчого сусіда. Мені не потрібно встановлювати обмеження для верхнього краю, оскільки його верхній край уже обмежений до нижнього краю синього виду.

рожеві обмеження

Мені також потрібно обмеження рівних висот між рожевими та блакитними видами. Це змусить їх заповнити половину екрана:

введіть тут опис зображення

Якщо я скажу Xcode оновити всі кадри зараз, я отримую це:

контейнери викладені

Тож обмеження, які я встановив поки що, є правильними. Я відміняю це і починаю роботу над світло-зеленим видом.

Зовнішній вигляд світло-зеленого виду вимагає п'яти обмежень:

  • Обмеження співвідношення сторін із необхідним пріоритетом на світло-зеленому поданні. Ви можете створити це обмеження у xib або дошці розкадрівання за допомогою Xcode 5.1 або новішої версії.
  • Обмеження з необхідним пріоритетом, що обмежує ширину світло-зеленого виду, має бути меншою або рівною ширині її контейнера.
  • Обмеження з високим пріоритетом встановлює ширину світло-зеленого виду, рівну ширині його контейнера.
  • Обмеження обов'язкового пріоритету, що обмежує висоту світло-зеленого виду, має бути меншою або рівною висоті його контейнера.
  • Обмеження з високим пріоритетом встановлює висоту світло-зеленого виду рівну висоті його контейнера.

Розглянемо два обмеження на ширину. Сам по собі обмеження менш рівного обмеження недостатньо для визначення ширини світло-зеленого виду; багато ширини підійдуть до обмеження. Оскільки існує двозначність, авторозмітка спробує вибрати рішення, яке мінімізує помилку в іншому (високий пріоритет, але не потрібно) обмеження. Мінімізація помилки означає ширину максимально наблизити до ширини контейнера, не порушуючи при цьому необхідного обмеження, меншого або рівного.

Те ж саме відбувається і з обмеженням висоти. Оскільки також потрібне обмеження співвідношення сторін, воно може лише максимізувати розмір субпрогляду вздовж однієї осі (якщо випадково контейнер не має того ж співвідношення сторін, що і підпогляд).

Тому спочатку я створюю обмеження співвідношення сторін:

верхній аспект

Потім я створюю однакові обмеження по ширині та висоті з контейнером:

верх рівного розміру

Мені потрібно відредагувати ці обмеження, щоб вони були меншими або рівними обмеженнями:

зверху менше або рівний розмір

Далі мені потрібно створити ще один набір рівних обмежень по ширині та висоті з контейнером:

знову рівний розмір

І мені потрібно зробити ці нові обмеження меншими за необхідний пріоритет:

верхній рівний не потрібно

Нарешті, ви попросили, щоб підрозділ був зосереджений у його контейнері, тому я встановлю ці обмеження:

зверху по центру

Тепер для тестування я виберу контролер перегляду та попрошу Xcode оновити всі кадри. Ось що я отримую:

неправильна верстка

На жаль! Підрозділ розширився, щоб повністю заповнити його контейнер. Якщо я вибираю його, я можу побачити, що насправді він підтримує співвідношення сторін, але він виконує заповнення аспекту замість відповідності .

Проблема полягає в тому, що для менш обмеженого обмеження має значення, який погляд знаходиться в кожному кінці обмеження, а Xcode встановив обмеження, протилежне моєму очікуванню. Я міг вибрати кожне з двох обмежень і змінити його перший і другий пункти. Натомість я просто виберу підперегляд і зміню обмеження на більші або рівні:

виправити верхні обмеження

Xcode оновлює макет:

правильний макет верху

Зараз я все те ж саме роблю до темно-зеленого виду на дні. Мені потрібно переконатися, що його співвідношення сторін дорівнює 1: 4 (Xcode змінив його розмір дивним чином, оскільки у нього не було обмежень). Я більше не показуватиму кроки, оскільки вони однакові. Ось результат:

правильний макет верхнього і нижнього

Тепер я можу запустити його в тренажері iPhone 4S, який має інший розмір екрана, ніж використовуваний ІБ, і тестовим поворотом:

iphone 4s тест

І я можу протестувати в тренажері iPhone 6:

iphone 6 тест

Для зручності я завантажив остаточну розкадровку в цю суть .


27
Я використовував LICEcap . Це безкоштовно (GPL) і записується безпосередньо в GIF. Поки анімований GIF не перевищує 2 Мб, ви можете розмістити його на цьому веб-сайті, як і будь-яке інше зображення.
грабувати майофф

1
@LucaTorella Маючи лише пріоритетні обмеження, макет неоднозначний. Тільки тому, що сьогодні ви отримаєте бажаний результат, це не означає, що це буде в наступній версії iOS. Припустимо, підрозділ бажає співвідношення сторін 1: 1, нагляд має розмір 99 × 100, і у вас є лише необхідне співвідношення сторін та обмеження рівності з високим пріоритетом. Тоді автоматичний макет може встановити підперегляд у розмір 99 × 99 (із загальною помилкою 1) або розміром 100 × 100 (із загальною помилкою 1). Один розмір - це відповідність аспекту; інше - це заповнення аспектів. Додавання необхідних обмежень створює унікальне рішення з найменшими помилками.
пограбувати майоф

1
@LucaTorella Дякуємо за виправлення щодо наявності обмежень співвідношення сторін.
пограбувати майоф

11
10 000 кращих додатків із однією публікацією ... неймовірно.
DonVaughn

2
Вау .. Найкраща відповідь тут в Stack Overflow я коли - небудь бачив .. Спасибі вам за це , сер ..
0yeoj

24

Роб, ваша відповідь приголомшлива! Я також знаю, що це питання стосується конкретного досягнення цього за допомогою автоматичного макетування. Однак, як орієнтир, я хотів би показати, як це можна зробити в коді. Ви налаштували вид зверху і знизу (синій і рожевий) так, як показав Роб. Потім ви створюєте звичай 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

Далі ви змінюєте клас блакитного та рожевого виду на дошці розкадрів, щоб бути AspectFitViews. Нарешті ви встановили два виходи на ваш ViewController topAspectFitViewі bottomAspectFitViewі встановити їх childViewз в 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;
}

Тому це не важко зробити в коді, і він все ще дуже пристосований і працює з переглядами різного розміру та різними співвідношеннями сторін.

Оновлення липня 2015 року : Знайдіть демо-додаток тут: https://github.com/jfahrenkrug/SPWKAspectFitView


1
@DanielGalasko Дякую, ви маєте рацію. Я це згадую у своїй відповіді. Я просто думав, що буде корисним посиланням порівняти, які кроки задіяні в обох рішеннях.
Йоганнес Фаренкруг

1
Дуже корисно мати програмну довідку.
ctpenrose

@JohannesFahrenkrug Якщо у вас є проект підручника, можете поділитися? дякую :)
Ерхан Демірчі

1
@ErhanDemirci Звичайно, ось що: github.com/jfahrenkrug/SPWKAspectFitView
Йоханнес Фаренкруг

8

Мені було потрібно рішення з прийнятої відповіді, але виконаного з коду. Найелегантніший спосіб, який я знайшов, - це використання рамки масонства .

#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

Це для macOS.

У мене є проблема використовувати спосіб Роба, щоб досягти розміщення сторін на додатку OS X. Але я зробив це іншим способом - замість ширини та висоти я використав провідний, задній, верхній і нижній простір.

В основному, додайте два провідні простори, де один> = 0 @ 1000 необхідного пріоритету, а інший = 0 @ 250 з низьким пріоритетом. Виконайте ті ж налаштування, що й для останнього, верхнього та нижнього простору.

Звичайно, потрібно встановити співвідношення сторін і центр X і центр Y.

І тоді робота закінчена!

введіть тут опис зображення введіть тут опис зображення


Як ти можеш цим займатися програмно?
FateNuller

@FateNuller Просто виконуючи кроки?
brianLikeApple

4

Я виявив , що хоче aspect- заливки поведінки так , що UIImageView буде підтримувати своє власне співвідношення сторін і повністю заповнити вид контейнера. Конфузно, мій UIImageView порушував БОЛЬШЕ пріоритетні обмеження з рівними шириною та однаковою висотою (описані у відповіді Роба) та рендерінг із повною роздільною здатністю.

Рішенням було просто встановити пріоритет стійкості до стиснення вмісту UIImageView нижче, ніж пріоритет обмежень однакової ширини та однакової висоти:

Опір вмісту стисненню


2

Це порт чудової відповіді @ rob_mayoff на підхід, орієнтований на код, використовуючи NSLayoutAnchorоб’єкти та переніс Xamarin. Для мене NSLayoutAnchorта пов’язані з ними класи значно полегшили програмування AutoLayout:

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

Можливо, це найкоротша відповідь - це масонство , яке також підтримує заповнення аспектів та розтягнення.

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);
        }
    }
}];
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.