Как использовать автоматический макет для перемещения других видов, когда вид скрыт?

334

Я разработал свою собственную ячейку в IB, разделил ее на подклассы и соединил свои розетки с моим специальным классом. У меня есть три подпредставления в содержимом ячейки: UIView (cdView) и две метки (titleLabel и emailLabel). В зависимости от данных, доступных для каждой строки, иногда я хочу, чтобы UIView и две метки отображались в моей ячейке, а иногда только две метки. То, что я пытаюсь сделать, это установить ограничения таким образом, если я установлю свойство UIView скрытым или я удалю его из суперпредставления, две метки переместятся влево. Я попытался установить ведущее ограничение UIView на Superview (содержимое ячейки) для 10px и UILabels, ведущее ограничение на 10 px для следующего представления (UIView). Позже в моем коде

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(IndexPath *)indexPath {
...
Record *record = [self.records objectAtIndex:indexPath.row];

if ([record.imageURL is equalToString:@""]) {
     cell.cdView.hidden = YES;
}

Я скрываю свой cell.cdView и хотел бы, чтобы метки двигались влево, но они остаются в той же позиции в Cell. Я пытался удалить cell.cdView из superview, но он тоже не работал. Я приложил изображение, чтобы уточнить, о чем я.

клетка

Я знаю, как сделать это программно, и я не ищу это решение. Я хочу установить ограничения в IB, и я ожидаю, что мои подпредставления будут перемещаться динамически, если другие виды будут удалены или скрыты. Можно ли это сделать в IB с автопрокатом?

.....
Guferos
источник
Измените значение ограничения времени выполнения - проверьте этот ответ
Кампай
Для этого конкретного случая вы также можете использовать UIStackView. когда вы прячете диск, этикетки будут занимать свое место
Марко Паппалардо,

Ответы:

373

Это возможно, но вам придется проделать небольшую дополнительную работу. Есть несколько концептуальных вещей, которые нужно сначала убрать с пути:

  • Скрытые виды, даже если они не рисуют, по- прежнему участвуют в автоматической компоновке и обычно сохраняют свои фреймы , оставляя другие связанные виды на своих местах.
  • При удалении представления из его суперпредставления все связанные ограничения также удаляются из этой иерархии представлений.

В вашем случае это скорее всего означает:

  • Если вы устанавливаете левый вид скрытым, надписи остаются на месте, поскольку этот левый вид все еще занимает место (даже если он не виден).
  • Если вы удалите свое левое представление, ваши метки, вероятно, останутся неоднозначно ограниченными, поскольку у вас больше не будет ограничений для левых краев ваших меток.

Что вам нужно сделать, так это разумно ограничить ваши ярлыки. Оставьте ваши существующие ограничения (10-тпс пространства для другого вида) в покое, но добавьте еще одно ограничение: отодвиньте левые края ваших меток на 10 пунктов от левого края их суперпредставления с необязательным приоритетом (высокий приоритет по умолчанию, вероятно, будет работать хорошо).

Затем, когда вы хотите, чтобы они переместились влево, полностью удалите левое представление. Обязательное ограничение 10pt для левого представления исчезнет вместе с представлением, к которому оно относится, и у вас останется только высокоприоритетное ограничение, чтобы метки были на расстоянии 10 пунктов от их суперпредставления. На следующем проходе макета это должно заставить их расширяться влево, пока они не заполнят ширину суперпредставления, но для вашего промежутка вокруг краев.

Одно важное предостережение: если вы хотите, чтобы ваш левый вид снова отображался на картинке, вам нужно не только добавить его обратно в иерархию представлений, но и одновременно восстановить все его ограничения . Это означает, что вам нужен способ поместить ограничение 10pt между представлением и его метками обратно всякий раз, когда это представление отображается снова.

Тим
источник
8
Хотя этот ответ, безусловно, сработает, IMO, чрезмерное ограничение для обработки различных вариантов использования, похоже, пахнет кодом - тем более что вам придется заново установить все ограничения для любых удаленных представлений, которые вы хотите снова показать.
меммон
На мой взгляд, это не тот путь. Вместо этого вы должны использовать ограничение ширины / высоты для вида, который вы хотите скрыть.
ullstrm
26
Я с уважением не согласен. Если вы (например) установите ширину вида на 0, вы столкнетесь с двумя проблемами. Во-первых, теперь у вас есть двойной интервал между суперпредставлением и видимым видом: |-(space)-[hidden(0)]-(space)-[visible]эффективно |-(2*space)-[visible]. Во-вторых, это представление может начать генерировать нарушения ограничений в зависимости от его собственного поддерева представления и ограничений - вы не можете гарантировать, что вы можете произвольно ограничить представление на ширине 0 и заставить его продолжать работать.
Тим
1
Если вы используете представление с внутренним размером контента, ответ Тима кажется уникальным.
Balkoth
Спасибо, Тим. Я установил ограничение 0 с более высоким приоритетом, чтобы избежать несовместимости ограничений, но теперь я понимаю, что есть проблема с двойным интервалом. У меня не было этой проблемы, потому что я никогда не показываю два представления вместе (мой случай :) |-[otherViews]-[eitherThis][orThis]-|, но я столкнулся с этой проблемой в конце концов.
Ферран Мэйлинч
207

Добавление или удаление ограничений во время выполнения - тяжелая операция, которая может повлиять на производительность. Однако есть более простая альтернатива.

Для вида, который вы хотите скрыть, установите ограничение ширины. Ограничьте другие взгляды с ведущим горизонтальным разрывом к этому представлению.

Чтобы скрыть, обновите .constantограничение ширины до 0.f. Другие виды автоматически переместятся влево, чтобы занять позицию.

Смотрите мой другой ответ здесь для более подробной информации:

Как изменить ограничения меток во время выполнения?

Макс Маклеод
источник
29
Единственная проблема с этим решением состоит в том, что поле слева будет вдвое больше того, что вы, вероятно, хотите, поэтому я бы тоже обновил одно из этих ограничений, но даже тогда я думаю, что это меньше, чем удаление подпредставления.
Хосе Мануэль Санчес
2
@ skinsfan00atg Если вы используете представление с внутренним размером контента, вы не можете использовать это решение.
Balkoth
@ balkoth, почему бы и нет? Вы могли бы просто уменьшить приоритет объятия контента
Макс МакЛауд
2
@MaxMacLeod Если вы уменьшаете приоритет объятия контента, то вы не используете собственный размер контента, вы используете размер, указанный ограничением.
Балхот
2
@MaxMacLeod Хорошо, я понял, что вы имеете в виду. Вам нужно установить приоритет сопротивления сжатию на 0 (не на обнимание содержимого) в коде, когда вы хотите скрыть представление, и когда вы хотите показать снова, восстановите это значение. Помимо этого вам нужно добавить ограничение в конструкторе интерфейса, чтобы установить размер представления равным 0. Нет необходимости касаться этого ограничения в коде.
Балхот
87

Для тех, кто поддерживает только iOS 8+ , есть новое активное логическое свойство . Это поможет включить только необходимые ограничения динамически

PS Ограничение выхода должно быть сильным , а не слабым

Пример:

@IBOutlet weak var optionalView: UIView!
@IBOutlet var viewIsVisibleConstraint: NSLayoutConstraint!
@IBOutlet var viewIsHiddenConstraint: NSLayoutConstraint!

func showView() {
    optionalView.isHidden = false
    viewIsVisibleConstraint.isActive = true
    viewIsHiddenConstraint.isActive = false
}

func hideView() {
    optionalView.isHidden = true
    viewIsVisibleConstraint.isActive = false
    viewIsHiddenConstraint.isActive = true
}

Также, чтобы исправить ошибку в раскадровке, вам нужно снять Installedфлажок для одного из этих ограничений.

UIStackView (iOS 9+)
Еще один вариант - обернуть ваши взгляды в UIStackView. Как только вид будет скрыт, UIStackViewбудет автоматически обновляться макет

Silmaril
источник
частично работает активно. Но если я использую его внутри многократно используемой ячейки, от активации до деактивации, работает, но не для деактивации для активации. Любые идеи? Или не могли бы вы привести пример?
Aoke Li
10
Это происходит потому, что деактивированное ограничение не загружается и не освобождается. Ваше ограничение должно быть сильным, а не слабым
Сильмарил
Где мы можем найти это «активное» свойство?
Лакшми Редди
Кажется, наиболее разумным ответом может быть чрезмерно ограниченная + установленная активность ограничений.
Тинг Йи Ши
68

UIStackViewавтоматически hiddenизменяет свои представления, когда свойство изменяется в любом из его подпредставлений (iOS 9+).

UIView.animateWithDuration(1.0) { () -> Void in
   self.mySubview.hidden = !self.mySubview.hidden
}

Перейти к 11:48 в этом видео WWDC для демонстрации:

Тайны Авто Макета, Часть 1

5bars
источник
Я скрыл вложенный вид стека, и весь содержащий вид стека исчез.
Ян Уорбертон
5
Это должен быть принятый ответ. Следующие яблочные рекомендации wwdc 2015 по разработке интерфейса.
Тибо Ноа
@thibautnoah А как насчет поддержки iOS 8?
багаж
5
@bagage Если вы не Facebook или Google, вы можете оставить его. С охватом iOS 9 вы будете поддерживать более 90% устройств, более чем достаточно. Поддержка ниже нанесет ущерб вашему процессу разработки и не позволит вам использовать новейшие функции imo
thibaut noah
16

Мой проект использует пользовательский @IBDesignableподкласс UILabel(для обеспечения согласованности цвета, шрифта, вставок и т. Д.), И я реализовал что-то вроде следующего:

override func intrinsicContentSize() -> CGSize {
    if hidden {
        return CGSizeZero
    } else {
        return super.intrinsicContentSize()
    }
}

Это позволяет подклассу меток принимать участие в автоматической компоновке, но при скрытии не занимать места.

Роберт Аткинс
источник
13

Для Google: основываясь на ответе Макса, чтобы решить проблему с заполнением, которую многие заметили, я просто увеличил высоту метки и использовал эту высоту в качестве разделителя вместо фактического заполнения. Эта идея может быть расширена для любого сценария, содержащего представления.

Вот простой пример:

Скриншот IB

В этом случае я сопоставляю высоту метки Author с соответствующим IBOutlet:

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

и когда я устанавливаю высоту ограничения равным 0.0f, мы сохраняем «отступ», потому что высота кнопки воспроизведения позволяет это сделать.

jterry
источник
Для тех, кто новичок в, NSLayoutConstraintя считаю, что вы хотите обновить constantсвойство вашего authorLabelHeight.
Кайл
8

связать ограничение между пользовательским интерфейсом и метками как IBOutlet и установить для элемента приоритета меньшее значение, если установлено скрытое = ДА

N0C
источник
3
Вы не можете настроить приоритет NSLayoutConstraint после того, как он установлен; Вы должны удалить и прочитать новое ограничение с другим приоритетом.
Тим
7
Я получил этот метод на работу. У меня есть чехол, в котором используется метка и кнопка, где мне нужно скрыть кнопку и развернуть метку. У меня есть два ограничения: одно изначально с приоритетом 751, а другое с 750. Затем, когда я прячу кнопку, я переворачиваю приоритеты, и длина метки увеличивается. Следует отметить, что если вы попытаетесь установить более высокий приоритет 1000, вы получите сообщение об ошибке «Изменение приоритета от обязательного к отсутствию установленного ограничения (или наоборот) не поддерживается». Так что не надо, и вы, кажется, в порядке. Xcode 5.1 / viewDidLoad.
Джон Бушнелл
8

Я закончил тем, что создал 2 xibs. Один с левым обзором и один без него. Я зарегистрировал оба в контроллере и затем решил, что использовать во время cellForRowAtIndexPath.

Они используют один и тот же класс UITableViewCell. Недостатком является то, что между XIB есть некоторое дублирование контента, но эти ячейки довольно простые. Плюс в том, что у меня нет кучки кода для ручного управления удалением вида, обновлением ограничений и т. Д.

В общем, это, вероятно, лучшее решение, поскольку они технически отличаются друг от друга и поэтому должны иметь разные XIB.

[self.table registerNib:[UINib nibWithNibName:@"TrackCell" bundle:nil] forCellReuseIdentifier:@"TrackCell"];
[self.table registerNib:[UINib nibWithNibName:@"TrackCellNoImage" bundle:nil] forCellReuseIdentifier:@"TrackCellNoImage"];

TrackCell *cell = [tableView dequeueReusableCellWithIdentifier:(appDelegate.showImages ? @"TrackCell" : @"TrackCellNoImage") forIndexPath:indexPath];
Майкл Петерсон
источник
7

В этом случае я сопоставляю высоту метки Author с соответствующим IBOutlet:

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

и когда я устанавливаю высоту ограничения равной 0.0f, мы сохраняем «отступ», потому что высота кнопки воспроизведения позволяет это сделать.

cell.authorLabelHeight.constant = 0;

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

Митул Марсония
источник
6

Используйте два UIStackView по горизонтали и вертикали, когда какое-то подпредставление в стеке скрыто, другие подпредставления стека будут перемещены, используйте Распределение -> Заполнить пропорционально для вертикального стека с двумя UILabel, и для первого UIView необходимо задать параметры ширины и высотывведите описание изображения здесь

Роман Солодяшкин
источник
2

Попробуйте это, я реализовал ниже код,

У меня есть один вид на ViewController, в котором добавлены другие три вида. Когда любой вид скрыт, два других вида переместятся, выполните следующие шаги. ,

1. ViewController.h Файл

#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property (strong, nonatomic) IBOutlet UIView *viewOne;
@property (strong, nonatomic) IBOutlet UIView *viewTwo;
@property (strong, nonatomic) IBOutlet UIView *viewThree;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *viewOneWidth;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *viewTwoWidth;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *viewThreeWidth;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *viewBottomWidth;
@end

2.ViewController.m

 #import "ViewController.h"
 @interface ViewController ()
{
  CGFloat viewOneWidthConstant;
  CGFloat viewTwoWidthConstant;
  CGFloat viewThreeWidthConstant;
  CGFloat viewBottomWidthConstant;
}
@end

@implementation ViewController
@synthesize viewOne, viewTwo, viewThree;

- (void)viewDidLoad {
  [super viewDidLoad];
 // Do any additional setup after loading the view, typically from a 
  nib.

  /*
   0  0   0
   0  0   1
   0  1   0
   0  1   1
   1  0   0
   1  0   1
   1  1   0
   1  1   1
   */

  //    [viewOne setHidden:NO];
  //    [viewTwo setHidden:NO];
  //    [viewThree setHidden:NO];

  //    [viewOne setHidden:NO];
  //    [viewTwo setHidden:NO];
  //    [viewThree setHidden:YES];

  //    [viewOne setHidden:NO];
  //    [viewTwo setHidden:YES];
  //    [viewThree setHidden:NO];

  //    [viewOne setHidden:NO];
  //    [viewTwo setHidden:YES];
  //    [viewThree setHidden:YES];


  //    [viewOne setHidden:YES];
  //    [viewTwo setHidden:NO];
  //    [viewThree setHidden:NO];

  //    [viewOne setHidden:YES];
  //    [viewTwo setHidden:NO];
  //    [viewThree setHidden:YES];

 //    [viewOne setHidden:YES];
 //    [viewTwo setHidden:YES];
 //    [viewThree setHidden:NO];

//    [viewOne setHidden:YES];
//    [viewTwo setHidden:YES];
//    [viewThree setHidden:YES];

 [self hideShowBottomBar];
  }

- (void)hideShowBottomBar
{
  BOOL isOne = !viewOne.isHidden;
  BOOL isTwo = !viewTwo.isHidden;
  BOOL isThree = !viewThree.isHidden;

  viewOneWidthConstant = _viewOneWidth.constant;
  viewTwoWidthConstant = _viewTwoWidth.constant;
  viewThreeWidthConstant = _viewThreeWidth.constant;
  viewBottomWidthConstant = _viewBottomWidth.constant;

   if (isOne && isTwo && isThree) {
    // 0    0   0
    _viewOneWidth.constant = viewBottomWidthConstant / 3;
    _viewTwoWidth.constant = viewBottomWidthConstant / 3;
    _viewThreeWidth.constant = viewBottomWidthConstant / 3;
    }
    else if (isOne && isTwo && !isThree) {
     // 0    0   1
    _viewOneWidth.constant = viewBottomWidthConstant / 2;
    _viewTwoWidth.constant = viewBottomWidthConstant / 2;
    _viewThreeWidth.constant = 0;
    }
   else if (isOne && !isTwo && isThree) {
    // 0    1   0
    _viewOneWidth.constant = viewBottomWidthConstant / 2;
    _viewTwoWidth.constant = 0;
    _viewThreeWidth.constant = viewBottomWidthConstant / 2;
    }
    else if (isOne && !isTwo && !isThree) {
    // 0    1   1
    _viewOneWidth.constant = viewBottomWidthConstant;
    _viewTwoWidth.constant = 0;
    _viewThreeWidth.constant = 0;
   }
   else if (!isOne && isTwo && isThree) {
    // 1    0   0
    _viewOneWidth.constant = 0;
    _viewTwoWidth.constant = viewBottomWidthConstant / 2;
    _viewThreeWidth.constant = viewBottomWidthConstant / 2;
   }
   else if (!isOne && isTwo && !isThree) {
    // 1    0   1
    _viewOneWidth.constant = 0;
    _viewTwoWidth.constant = viewBottomWidthConstant;
    _viewThreeWidth.constant = 0;
   }
   else if (!isOne && !isTwo && isThree) {
    // 1    1   0
    _viewOneWidth.constant = 0;
    _viewTwoWidth.constant = 0;
    _viewThreeWidth.constant = viewBottomWidthConstant;
   }
   else if (isOne && isTwo && isThree) {
    // 1    1   1
    _viewOneWidth.constant = 0;
    _viewTwoWidth.constant = 0;
    _viewThreeWidth.constant = 0;
   }
  }

 - (void)didReceiveMemoryWarning {
  [super didReceiveMemoryWarning];
 // Dispose of any resources that can be recreated.
 }
 @end

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

Надеюсь, что эта логика поможет кому-то.

Джейвант Хедкар
источник
1

В моем случае я установил постоянную высоту ограничения на 0.0fа также установить hiddenсвойствоYES .

Чтобы снова показать представление (с подпредставлениями), я сделал наоборот: я установил постоянную высоты в ненулевое значение и установил hiddenсвойство в NO.

тестирование
источник
1

Я буду использовать горизонтальное представление стека. Он может удалить кадр, когда подпредставление скрыто.

На изображении ниже красный вид является фактическим контейнером для вашего контента и имеет конечный интервал 10pt для оранжевого суперпредставления (ShowHideView), затем просто подключите ShowHideView к IBOutlet и покажите / спрячьте / удалите его программно.

  1. Это когда вид виден / установлен.

виден

  1. Это когда вид скрыт / не установлен.

вид скрыт / удален

mnemonic23
источник
1

Это мое другое решение, использующее ограничение приоритета. Идея установить ширину 0.

  1. создать вид контейнера (оранжевый) и установить ширину. введите описание изображения здесь

  2. создайте представление контента (красный) и установите конечное пространство 10pt в superview (оранжевый). Обратите внимание на концевые пробелы, есть 2 концевых ограничения с разным приоритетом. Низкий (= 10) и Высокий (<= 10). Это важно, чтобы избежать двусмысленности. введите описание изображения здесь

  3. Установите оранжевую ширину вида на 0, чтобы скрыть вид. введите описание изображения здесь

mnemonic23
источник
0

Как и предположил no_scene, вы можете сделать это, изменив приоритет ограничения во время выполнения. Это было намного проще для меня, потому что у меня было более одного вида блокировки, который нужно было удалить.

Вот фрагмент кода с использованием ReactiveCocoa:

RACSignal* isViewOneHiddenSignal = RACObserve(self.viewModel, isViewOneHidden);
RACSignal* isViewTwoHiddenSignal = RACObserve(self.viewModel, isViewTwoHidden);
RACSignal* isViewThreeHiddenSignal = RACObserve(self.viewModel, isViewThreeHidden);
RAC(self.viewOne, hidden) = isViewOneHiddenSignal;
RAC(self.viewTwo, hidden) = isViewTwoHiddenSignal;
RAC(self.viewThree, hidden) = isViewThreeHiddenSignal;

RAC(self.viewFourBottomConstraint, priority) = [[[[RACSignal
    combineLatest:@[isViewOneHiddenSignal,
                    isViewTwoHiddenSignal,
                    isViewThreeHiddenSignal]]
    and]
    distinctUntilChanged]
    map:^id(NSNumber* allAreHidden) {
        return [allAreHidden boolValue] ? @(780) : @(UILayoutPriorityDefaultHigh);
    }];

RACSignal* updateFramesSignal = [RACObserve(self.viewFourBottomConstraint, priority) distinctUntilChanged];
[updateFramesSignal
    subscribeNext:^(id x) {
        @strongify(self);
        [self.view setNeedsUpdateConstraints];
        [UIView animateWithDuration:0.3 animations:^{
            [self.view layoutIfNeeded];
        }];
    }];
skensell
источник
0

Если это кому-то поможет, я создал вспомогательный класс для использования визуального формата ограничений . Я использую это в моем текущем приложении.

AutolayoutHelper

Это может быть немного приспособлено к моим потребностям, но вы можете найти это полезным или захотите изменить его и создать своего собственного помощника.

Я должен поблагодарить Тима за его ответ выше , этот ответ о UIScrollView, а также этот учебник .

Ферран Майлинч
источник
0

Вот как я бы выровнял свои uiviews, чтобы получить ваше решение:

  1. Перетащите один UIImageView и поместите его влево.
  2. Перетащите один UIView и поместите его справа от UIImageView.
  3. Перетащите две метки UILabels внутри того UIView, у которого начальные и конечные ограничения равны нулю.
  4. Установите ведущее ограничение UIView, содержащее 2 метки, для superview вместо UIImagView.
  5. Если UIImageView скрыт, установите ведущую константу ограничения в 10 пикселей для суперпредставления. В противном случае установите ведущую постоянную ограничения равной 10 пикселей + UIImageView.width + 10 пикселей.

Я создал собственное правило большого пальца. Всякий раз, когда вам нужно скрыть / показать какой-либо пользовательский вид, на чьи ограничения могут повлиять, добавьте все затронутые / зависимые подпредставления внутри пользовательского интерфейса и программно обновите его константу начальных / конечных / верхних / нижних ограничений.

Дипак Тхакур
источник
0

Это старый вопрос, но все же я надеюсь, что это поможет. Исходя из Android, на этой платформе у вас есть удобный метод, isVisibleчтобы скрыть его от вида, но при этом не учитывать кадр, когда автопутешествие рисует вид.

используя extension и "extension" uiview, вы можете сделать аналогичную функцию в ios (не уверен, почему его нет в UIKit), здесь реализация в swift 3:

    func isVisible(_ isVisible: Bool) {
        self.isHidden = !isVisible
        self.translatesAutoresizingMaskIntoConstraints = isVisible
        if isVisible { //if visible we remove the hight constraint 
            if let constraint = (self.constraints.filter{$0.firstAttribute == .height}.first){
                self.removeConstraint(constraint)
            }
        } else { //if not visible we add a constraint to force the view to have a hight set to 0
            let height = NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal , toItem: nil, attribute: .notAnAttribute, multiplier: 0, constant: 0)
            self.addConstraint(height)
        }
        self.layoutIfNeeded()
    }
Винсент Чубард
источник
0

правильный способ сделать это - отключить ограничения с помощью isActive = false. заметьте, однако, что деактивация ограничения удаляет и освобождает его, поэтому вы должны иметь сильные выходы для них.

Hogdotmac
источник
0

Самое простое решение - использовать UIStackView (горизонтальный). Добавить в стек: первый вид и второй вид с метками. Затем установите для свойства isHidden первого вида значение false. Все ограничения будут рассчитаны и обновлены автоматически.

Олег Кудинов
источник
0

Я думаю, что это самый простой ответ. Пожалуйста, убедитесь, что это работает:

        StackFullView.layer.isHidden = true
        Task_TopSpaceSections.constant = 0.   //your constraint of top view

проверьте здесь https://www.youtube.com/watch?v=EBulMWMoFuw

Gowthaman M
источник