Створення багаторазового UIView за допомогою xib (і завантаження з розкадрування)


81

Добре, на StackOverflow є десятки постів про це, але жоден з них не є особливо зрозумілим щодо рішення. Я хотів би створити власний UIViewфайл із супровідним файлом xib. Вимоги:

  • Ніякого окремого UIViewController- цілком самостійний клас
  • Розетки в класі, щоб дозволити мені встановлювати / отримувати властивості подання

Мій поточний підхід до цього:

  1. Замінити -(id)initWithFrame:

    -(id)initWithFrame:(CGRect)frame {
        self = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:self
                                            options:nil] objectAtIndex:0];
        self.frame = frame;
        return self;
    }
    
  2. Застосовуйте програмне забезпечення за допомогою -(id)initWithFrame:контролера мого подання

    MyCustomView *myCustomView = [[MyCustomView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height)];
    [self.view insertSubview:myCustomView atIndex:0];
    

Це працює нормально (хоча ніколи не викликати [super init]і просто налаштовувати об’єкт за допомогою вмісту завантаженого наконечника здається трохи підозрілим - тут є порада додати підпрогляд у цьому випадку, який також чудово працює). Однак я хотів би мати можливість також створити примірник подання з розкадрування. Що ж я можу:

  1. Розмістіть a UIViewна батьківському поданні в розкадруванні
  2. Встановіть для власного класу значення MyCustomView
  3. Замінити -(id)initWithCoder:- код, який я найчастіше бачив, відповідає такому шаблону:

    -(id)initWithCoder:(NSCoder *)aDecoder {
        self = [super initWithCoder:aDecoder];
        if (self) {
            [self initializeSubviews];
        }
        return self;
    }
    
    -(id)initWithFrame:(CGRect)frame {
        self = [super initWithFrame:frame];
        if (self) {
            [self initializeSubviews];
        }
        return self;
    }
    
    -(void)initializeSubviews {
        typeof(view) view = [[[NSBundle mainBundle]
                             loadNibNamed:NSStringFromClass([self class])
                                    owner:self
                                  options:nil] objectAtIndex:0];
        [self addSubview:view];
    }
    

Звичайно, це не працює, незалежно від того, чи використовую я підхід, наведений вище, чи програмую екземпляри програм, обидва закінчуються рекурсивними викликами -(id)initWithCoder:при введенні -(void)initializeSubviewsта завантаженні перо з файлу.

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

  • Здається, загальною пропозицією є вбудовувати весь клас у UIViewController і робити там завантаження перо, але це мені здається неоптимальним, оскільки вимагає додавання іншого файлу як обгортки

Хтось може дати пораду, як вирішити цю проблему, і отримати робочі точки на замовлення UIViewз мінімальною суєтою / відсутністю тонкої обгортки контролера? Або існує альтернативний, більш чистий спосіб робити щось із мінімальним типовим кодом?


1
Чи отримували ви коли-небудь задовільну відповідь на це? Я зараз борюся за це. Усі інші відповіді здаються недостатньо хорошими, як ви вже згадали. Ви завжди можете відповісти на запитання самі, якщо щось дізналися за останні кілька місяців.
Mike Meyers

13
Чому так складно створювати перегляди багаторазового використання в iOS?
годинник


1
Насправді, відповідь, на яку ви посилаєтесь, використовує точно такий самий підхід (хоча ваша відповідь не включає функцію init from rect, що означає, що її можна ініціалізувати лише за допомогою розкадрування, а не програмно)
Кен Чатфілд,

1
щодо цього дуже старого контролю якості Apple нарешті представила посилання на STORYBOARD ... developer.apple.com/library/ios/recipes/… ... так що це, тьху!
Fattie,

Відповіді:


13

Ваша проблема - дзвінок loadNibNamed:від (нащадка) initWithCoder:. loadNibNamed:внутрішні дзвінки initWithCoder:. Якщо ви хочете замінити кодер розкадровки та завжди завантажувати свою реалізацію xib, я пропоную наступний прийом. Додайте властивість до свого класу подання та у файлі xib встановіть для нього заздалегідь визначене значення (у визначених користувачем атрибутах виконання). Тепер, після дзвінка [super initWithCoder:aDecoder];перевірте вартість власності. Якщо це задане значення, не дзвоніть [self initializeSubviews];.

Отже, приблизно так:

-(instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];

    if (self && self._xibProperty != 666)
    {
        //We are in the storyboard code path. Initialize from the xib.
        self = [self initializeSubviews];

        //Here, you can load properties that you wish to expose to the user to set in a storyboard; e.g.:
        //self.backgroundColor = [aDecoder decodeObjectOfClass:[UIColor class] forKey:@"backgroundColor"];
    }

    return self;
}

-(instancetype)initializeSubviews {
    id view =   [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];

    return view;
}

Дякую @LeoNatan! Я приймаю цю відповідь, оскільки це найкраще рішення проблеми, як було спочатку зазначено. Однак зауважте, що в Swift це вже неможливо - я додав окремі примітки щодо можливої ​​роботи в такому випадку.
Ken Chatfield

@KenChatfield Я помітив це у своєму підпроекті Swift, і мене це дратувало. Я не впевнений, про що вони думають, тому що без цього багато можливостей внутрішньої реалізації Cocoa / Cocoa Touch в Swift неможливо. Мою ставку на те, що будуть деякі динамічні функції, коли вони насправді встигають зосередитись на функціях, а не на помилках. Свіфт зовсім не готовий, і найгірші порушники - це інструменти розвитку.
Лео Натан

Так, ти маєш рацію, у Свіфта зараз багато грубих країв. Сказавши це, здається, що безпосереднє присвоєння самому собі було трохи зламаним, тому я щасливий у тому, що компілятор тепер застерігає від таких речей (ця жорсткіша перевірка типу здається однією з приємних речей про мова). Однак ви маєте рацію, що це робить зв'язок користувацьких переглядів з xibs дуже брудним і трохи незадовільним. Будемо сподіватися, що коли ви скажете, що, коли вони закінчать розбирати помилки, ми побачимо кілька більш динамічних функцій, які допоможуть нам трохи більше допомогти з подібними речами!
Ken Chatfield

Це не хакерство, це те, як працюють кластери класів. І насправді, немає технічної проблеми, яка дозволяла б повернути об’єкт, який є підкласом класу return, замість цього. В даний час один із наріжних каменів кластерів класу какао та какао Touch неможливо реалізувати. Жахливий. Деякі фреймворки, такі як Core Data, не можуть бути реалізовані в Swift, що робить його марною мовою для більшості моїх потреб.
Лео Натан

1
Якось це не спрацювало для мене (iOS8.1 SDK). Я встановив RestoIdentifier в XIB замість атрибута виконання, ніж він працював. Наприклад, я встановив "MyViewRestorationID" у xib, ніж у initWithCoder: я перевірив, що! [[СамовідновленняIdentifier] isEqualToString: @ "MyViewRestorationID"]
ingaham

26

Зауважте, що цей контроль якості (як і багато інших) насправді представляє лише історичний інтерес.

Сьогодні Роками та роками в iOS все є лише видом контейнера. Повний посібник тут

(Дійсно, Apple нарешті додала посилання на Storyboard References , деякий час тому, що набагато полегшило це.)

Ось типова розкадровка з переглядом контейнерів скрізь. Все є видом контейнера. Це просто те, як ви створюєте програми.

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

(Як цікавість, відповідь KenC показує, як саме це раніше робилося для завантаження xib у своєрідний вид обгортки, оскільки насправді не можна "призначити собі".)


Проблема цього полягає в тому, що у вас вийде багато ViewControllers для всіх вкладених переглядів вмісту.
Богдан Ону,

Привіт @BogdanOnu! У вас повинно бути багато-багато-багато контролерів перегляду. Для "найменшої" речі - у вас повинен бути контролер перегляду.
Fattie,

2
Це дуже корисно - дякую @JoeBlow. Використання переглядів контейнерів, безсумнівно, є альтернативним підходом і простим способом уникнути всіх ускладнень безпосередньої роботи з xibs. Однак це не здається на 100% задовільною альтернативою для створення компонентів, які можна багаторазово використовувати для розповсюдження / використання в проектах, оскільки вимагає, щоб весь дизайн інтерфейсу користувача був безпосередньо вбудований в розкадрування.
Ken Chatfield

3
У мене менше проблем із використанням додаткового ViewController у цьому випадку, оскільки він містив би просто логіку програми, яка в іншому випадку належала б до власного класу View у випадку xib, але тісне зчеплення з розкадровкою означає, що я не впевнений, що подання контейнерів можуть повністю вирішити цю проблему. Можливо, у більшості практичних ситуацій подання є специфічними для проекту, і тому це найкраще і найбільш "стандартне" рішення, але я здивований, що досі не існує легкого способу упаковки власних подань для розкадрування. Імпульс мого програміста розділити та перемогти на окремі компоненти свербить;)
Кен Чатфілд,

1
Крім того, із введенням реального рендерінгу користувацьких підкласів UIView в Xcode 6, я не впевнений, чи купую я передумову про те, що створення переглядів за допомогою xibs таким чином вже застаріло
Кен Чатфілд,

24

Я додаю це як окремий допис, щоб оновити ситуацію з виходом Swift. Підхід, описаний LeoNatan, чудово працює в Objective-C. Однак, суворіші перевірки часу компіляції запобігають selfпризначенню при завантаженні з файлу xib у Swift.

Як наслідок, не залишається жодної можливості, крім як додати подання, завантажене з файлу xib, як підпрогляд користувацького підкласу UIView, замість того, щоб повністю замінити self. Це аналогічно другому підходу, викладеному у вихідному питанні. Орієнтовний контур класу в Swift, що використовує цей підхід, такий:

@IBDesignable // <- to optionally enable live rendering in IB
class ExampleView: UIView {

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initializeSubviews()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        initializeSubviews()
    }

    func initializeSubviews() {
        // below doesn't work as returned class name is normally in project module scope
        /*let viewName = NSStringFromClass(self.classForCoder)*/
        let viewName = "ExampleView"
        let view: UIView = NSBundle.mainBundle().loadNibNamed(viewName,
                               owner: self, options: nil)[0] as! UIView
        self.addSubview(view)
        view.frame = self.bounds
    }

}

Недоліком цього підходу є введення в ієрархію подання додаткового надлишкового шару, який не існує при використанні підходу, описаного LeoNatan в Objective-C. Однак це можна сприймати як необхідне зло і результат основного способу створення речей у Xcode (мені все ще здається божевільним, що настільки складно пов'язати власний клас UIView із макетом інтерфейсу таким чином, щоб він працював послідовно як для розкадрувань, так і з коду) - заміна selfоптових продажів в ініціалізаторі раніше ніколи не здавалася особливо зрозумілим способом здійснення дій, хоча наявність по суті двох класів перегляду на одне представлення теж не здається таким чудовим.

Тим не менш, одним щасливим результатом цього підходу є те, що нам більше не потрібно встановлювати власний клас представлення для нашого файлу класу в конструкторі інтерфейсу, щоб забезпечити правильну поведінку при призначенні self, і тому рекурсивний виклик init(coder aDecoder: NSCoder)при видачі loadNibNamed()порушується (не встановлюючи користувацький клас у файлі xib, init(coder aDecoder: NSCoder)замість нашої користувацької версії буде замінено звичайний ванільний UIView).

Незважаючи на те, що ми не можемо зробити налаштування класу для подання, що зберігається безпосередньо в xib, ми все одно можемо зв'язати представлення з нашим "батьківським" підкласом UIView за допомогою розеток / дій тощо після встановлення власника файлу представлення до нашого користувацького класу:

Встановлення властивості власника файлу спеціального подання

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


Привіт Джо - дякую за ваші коментарі, це дуже сміливо (як і в багатьох інших ваших коментарях!) Як я вже зазначив у відповіді на вашу відповідь, я згоден, що для більшості ситуацій перегляди контейнерів є, мабуть, найкращим підходом, але в цих випадках де погляди повинні використовуватися в проектах (або розподілятися), має сенс, принаймні для мене, і, здається, для інших, також мати альтернативу. Ви можете особисто вважати, що це поганий стиль, але, можливо, ви можете просто дозволити багатьом іншим публікаціям тут запропонувати, як це можна зробити, для довідки, і нехай люди судять самі. Обидва підходи здаються корисними.
Кен Чатфілд,

Дякую за це. Я спробував безліч успіхів у Swift без успіху, поки не прислухався до вашої поради щодо виходу з класу перо як UIView. Я згоден, що божевільно, що Apple ніколи не робила цього простим, а тепер це практично неможливо. Контейнер - це не завжди відповідь.
Ешелон

16

КРОК 1. Заміна selfз Storyboard

Заміна selfв initWithCoder:методі не вдасться з наступною помилкою.

'NSGenericException', reason: 'This coder requires that replaced objects be returned from initWithCoder:'

Натомість декодований об’єкт можна замінити на awakeAfterUsingCoder:(not awakeFromNib). подібно до:

@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end

КРОК2. Запобігання рекурсивному дзвінку

Звичайно, це також спричиняє рекурсивну проблему дзвінка. (декодування розкадровки -> awakeAfterUsingCoder:-> loadNibNamed:-> awakeAfterUsingCoder:-> loadNibNamed:-> ...)
Отже, вам потрібно перевірити струмawakeAfterUsingCoder: чи викликається у процесі розшифровки Storyboard або в процесі декодування XIB. У вас є кілька способів зробити це:

а) Використовуйте приватне, @propertyяке встановлено лише у NIB.

@interface MyCustomView : UIView
@property (assign, nonatomic) BOOL xib
@end

і встановити "Визначені користувачем атрибути виконання" лише в "MyCustomView.xib".

Плюси:

  • Жоден

Мінуси:

  • Просто не працює: setXib:буде викликано ПІСЛЯ awakeAfterUsingCoder:

б) Перевірте, чи selfнемає підпроектів

Зазвичай у вас є підпрогляди в xib, але не в розкадруванні.

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    if(self.subviews.count > 0) {
        // loading xib
        return self;
    }
    else {
        // loading storyboard
        return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:nil
                                            options:nil] objectAtIndex:0];
    }
}

Плюси:

  • Немає хитрості в Interface Builder.

Мінуси:

  • Ви не можете мати підпрограми у своїй розкадровці.

в) Встановіть статичний прапор під час loadNibNamed:дзвінка

static BOOL _loadingXib = NO;

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    if(_loadingXib) {
        // xib
        return self;
    }
    else {
        // storyboard
        _loadingXib = YES;
        typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                           owner:nil
                                                         options:nil] objectAtIndex:0];
        _loadingXib = NO;
        return view;
    }
}

Плюси:

  • Простий
  • Немає хитрості в Interface Builder.

Мінуси:

  • Небезпечно: статичний спільний прапор небезпечний

г) Використовуйте приватний підклас у XIB

Наприклад, оголосити _NIB_MyCustomViewяк підклас MyCustomView. І використовуйте _NIB_MyCustomViewзамість цього лише MyCustomViewу своєму XIB.

MyCustomView.h:

@interface MyCustomView : UIView
@end

MyCustomView.m:

#import "MyCustomView.h"

@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    // In Storyboard decoding path.
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end

@interface _NIB_MyCustomView : MyCustomView
@end

@implementation _NIB_MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    // In XIB decoding path.
    // Block recursive call.
    return self;
}
@end

Плюси:

  • Немає явного ifвMyCustomView

Мінуси:

  • Приставка _NIB_трик в XIb Interface Builder
  • відносно більше кодів

e) Використовуйте підклас як заповнювач у Storyboard

Подібно до, d)але використовуйте підклас у Storyboard, оригінальний клас у XIB.

Тут ми оголошуємо MyCustomViewProtoяк підклас MyCustomView.

@interface MyCustomViewProto : MyCustomView
@end
@implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    // In storyboard decoding
    // Returns MyCustomView loaded from NIB.
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self superclass])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end

Плюси:

  • Дуже безпечно
  • Чистий; Немає зайвого коду в MyCustomView.
  • Немає явної ifперевірки, такої ж, якd)

Мінуси:

  • Потрібно використовувати підклас у розкадруванні.

Я думаю, що e)це найбезпечніша і найчистіша стратегія. Отже, ми приймаємо це тут.

КРОК3. Властивості копіювання

Після того, як loadNibNamed:у 'awakeAfterUsingCoder:', Ви повинні скопіювати кілька властивостей, з selfяких декодується екземпляр розкадрування. frameі особливо важливі властивості автоматичного розміщення / автоматичного зміни розміру.

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                       owner:nil
                                                     options:nil] objectAtIndex:0];
    // copy layout properities.
    view.frame = self.frame;
    view.autoresizingMask = self.autoresizingMask;
    view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;

    // copy autolayout constraints
    NSMutableArray *constraints = [NSMutableArray array];
    for(NSLayoutConstraint *constraint in self.constraints) {
        id firstItem = constraint.firstItem;
        id secondItem = constraint.secondItem;
        if(firstItem == self) firstItem = view;
        if(secondItem == self) secondItem = view;
        [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                            attribute:constraint.firstAttribute
                                                            relatedBy:constraint.relation
                                                               toItem:secondItem
                                                            attribute:constraint.secondAttribute
                                                           multiplier:constraint.multiplier
                                                             constant:constraint.constant]];
    }

    // move subviews
    for(UIView *subview in self.subviews) {
        [view addSubview:subview];
    }
    [view addConstraints:constraints];

    // Copy more properties you like to expose in Storyboard.

    return view;
}

КІНЦЕВЕ РІШЕННЯ

Як бачите, це трохи зразкового коду. Ми можемо реалізувати їх як "категорію". Тут я розширюю загальновживаний UIView+loadFromNibкод.

#import <UIKit/UIKit.h>

@interface UIView (loadFromNib)
@end

@implementation UIView (loadFromNib)

+ (id)loadFromNib {
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self)
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}

- (void)copyPropertiesFromPrototype:(UIView *)proto {
    self.frame = proto.frame;
    self.autoresizingMask = proto.autoresizingMask;
    self.translatesAutoresizingMaskIntoConstraints = proto.translatesAutoresizingMaskIntoConstraints;
    NSMutableArray *constraints = [NSMutableArray array];
    for(NSLayoutConstraint *constraint in proto.constraints) {
        id firstItem = constraint.firstItem;
        id secondItem = constraint.secondItem;
        if(firstItem == proto) firstItem = self;
        if(secondItem == proto) secondItem = self;
        [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                            attribute:constraint.firstAttribute
                                                            relatedBy:constraint.relation
                                                               toItem:secondItem
                                                            attribute:constraint.secondAttribute
                                                           multiplier:constraint.multiplier
                                                             constant:constraint.constant]];
    }
    for(UIView *subview in proto.subviews) {
        [self addSubview:subview];
    }
    [self addConstraints:constraints];
}

Використовуючи це, ви можете оголосити, MyCustomViewProtoяк:

@interface MyCustomViewProto : MyCustomView
@end

@implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    MyCustomView *view = [MyCustomView loadFromNib];
    [view copyPropertiesFromPrototype:self];

    // copy additional properties as you like.

    return view;
}
@end

XIB:

Знімок екрана XIB

Розкадровка:

Розкадровка

Результат:

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


3
Рішення складніше, ніж початкова проблема. Щоб зупинити рекурсивний цикл, вам просто потрібно встановити об'єкт власника файлу, а не оголошувати подання вмісту як тип класу MyCustomView.
Богдан Ону,

Це просто компроміс a) простого процесу ініціалізації, але складної ієрархії подання та b) складного процесу ініціалізації, але простої ієрархії подання. n'est-ce pas? ;)
rintaro

чи є посилання для завантаження цього проекту?
karthikeyan

13

Не забувайте

Два важливі моменти:

  1. Встановіть для власника файлу .xib ім'я класу власного подання.
  2. Не встановлюйте спеціальне ім'я класу в IB для кореневого подання .xib.

Я кілька разів відвідував цю сторінку запитань та відповідей, навчаючись робити перегляди багаторазового використання. Забуття вищезазначених пунктів змусило мене витратити багато часу на спроби з’ясувати, що спричинило нескінченну рекурсію. Ці моменти згадуються в інших відповідях тут і в інших місцях , але я просто хочу підкреслити їх тут.

Моя повна відповідь Swift із кроками тут .


2

Є рішення, яке є набагато чистішим, ніж рішення вище: https://www.youtube.com/watch?v=xP7YvdlnHfA

Відсутність властивостей виконання, відсутність рекурсивних викликів. Я спробував, і це спрацювало як шарм, використовуючи розкадрування та XIB із властивостями IBOutlet (iOS8.1, XCode6).

Удачі вам у кодуванні!


1
Дякую @ingaham! Однак підхід, викладений у відео, ідентичний другому рішенню, запропонованому в оригінальному питанні (код Swift, на який представлений у моїй відповіді вище). Як і в обох випадках, це передбачає додавання підпрогляду до підкласу обгортки UIView, і саме тому немає проблем з рекурсивними викликами, а також не потрібно покладатися на властивості середовища виконання або щось інше складне. Як уже згадувалося, недоліком є ​​те, що до власного класу UIView потрібно додати надлишковий додатковий дочірній вигляд. Однак, як ми вже обговорювали, це може бути найкращим і найпростішим рішенням на даний момент.
Ken Chatfield

Так, ви абсолютно праві, це ідентичні рішення. Однак зайвий вигляд необхідний, але це найбільш чисте та ремонтопридатне рішення. Тому я вирішив використати цей.
Інгахам

Я вважаю, що зайвий погляд є цілком і цілком природним, і іншого рішення ніколи не може бути. Зверніть увагу, що ви говорите "" щось "буде йти" сюди "" ... що "тут" є природно існуючою річчю. Там просто повинно бути "щось", "місце, куди ти збираєшся ставити речі" - саме визначення того, що таке "погляд". І почекайте! Речі Apple "view view" - це, насправді, саме це ... є "кадр", "view view" ("вигляд контейнера"), в який ви щось помічаєте. Дійсно, «надлишкові» рішення подання - це саме те, що зроблено власноруч. Просто використовуйте Apple.
Fattie,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.