Статьи

Создание бэкенда для вашего iOS-приложения с использованием Firebase

Firebase – это платформа Backend as Service, которая может помочь вам быстро разработать и развернуть приложение. Он предоставляет множество функций, включая базу данных в реальном времени, аутентификацию (с электронной почтой и паролем, Facebook, Twitter, GitHub и Google), облачные сообщения, хранилище, хостинг, удаленную настройку, тестовую лабораторию, отчеты о сбоях, уведомления, индексацию приложений, динамические ссылки, Приглашения, AdWords и AdMob.

В этой статье мы создадим простое приложение To Do, которое покажет, как сохранять и извлекать данные из Firebase, как проверять подлинность пользователей, устанавливать разрешения на чтение / запись для данных и проверять данные на сервере.

Начиная

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

Когда вы запустите загруженный проект, вы увидите окно входа в систему.

Login View

В представлении «Вход в систему» ​​есть кнопка «Регистрация», которая при нажатии на нее позволяет перейти в представление «Регистрация»; и в этом представлении есть форма регистрации, а также кнопка входа в систему, которая возвращает вас к представлению входа в систему.

Sign Up View

Чтобы добавить библиотеки Firebase в проект, мы будем использовать CocoaPods. Сначала убедитесь, что у вас установлены CocoaPods на вашем компьютере.

Откройте Терминал и перейдите к корню загруженного проекта с помощью CD Path / To / ToDo \ App ( \ экранирует пробел в имени каталога).

Создайте подфайл с помощью следующей команды.

 pod init 

Затем откройте Podfile с помощью:

 open - a Xcode Podfile 

Измените содержимое файла, как показано ниже.

 platform :ios, '10.0' target 'ToDo App' do use_frameworks! pod 'Firebase/Auth' pod 'Firebase/Database' end 

Библиотека Firebase состоит из разных подвидов . Выше мы включили подспец /Auth который используется для аутентификации, и подспец /Database , необходимый для работы с базой данных Firebase в реальном времени. Подспец /Core необходим для того, чтобы ваше приложение работало с Firebase, но нам не нужно включать его в Podfile, потому что от него зависят подспецификации. Поэтому, когда мы запускаем pod install для получения зависимостей проекта, будет также извлечена библиотека /Core .

Запустите pod install чтобы получить зависимости проекта. После завершения установки закройте проект Xcode и откройте ToDo App.xcworkspace .

Затем перейдите к консоли Firebase и нажмите кнопку « Создать новый проект» . Появится диалоговое окно, в котором вы сможете ввести детали проекта. Введите название и страну / регион для проекта.

Create Firebase Project

Страна / регион представляет страну / регион вашей организации / компании. Ваш выбор также устанавливает соответствующую валюту для отчетов о доходах. После задания имени (я использовал ToDoApp) и региона для проекта, нажмите кнопку « Создать проект» . Проект будет создан и его консоль открыта. В консоли проекта нажмите « Добавить Firebase в ваше приложение iOS» . Затем введите данные проекта iOS в появившемся окне.

Add Firebase to iOS App

Мы оставим поле идентификатора App Store пустым, но если вы интегрируете Firebase в приложение, которое есть в App Store, вы можете найти идентификатор в URL своего приложения. В приведенном ниже примере 123456789 – это идентификатор App Store. https://itunes.apple.com/gb/app/yourapp/id123456789

Нажмите на кнопку « Добавить приложение» , и файл GoogleService-Info.plist будет загружен на ваш компьютер. Переместите файл в корень вашего проекта XCode и добавьте его ко всем целям.

Файл plist содержит параметры конфигурации, необходимые приложению iOS для взаимодействия с серверами Firebase. Он содержит такие вещи, как URL-адрес проекта Firebase, ключ API и т. Д. В предыдущей версии Firebase вам приходилось хранить их вручную в коде приложения, но теперь процесс был упрощен благодаря использованию одного файла, который содержит необходимые данные.

Если вы используете управление версиями и хранение своего кода в общедоступном репозитории, вам следует не делать общедоступным доступ к GoogleService-Info.plist . Если он используется за его пределами, он перестанет работать, и если вы пользуетесь платным планом, вы не захотите, чтобы кто-то злоупотреблял этим и увеличивал ваши расходы.

На консоли Firebase нажмите Продолжить, чтобы перейти к настройке проекта.

Add Firebase to iOS App

Мы уже выполнили вышеуказанные шаги, поэтому нажмите « Продолжить» .

Следуйте инструкциям на последней странице диалогового окна и добавьте код инициализации Firebase в свой класс AppDelegate . Это подключается к Firebase при запуске приложения.

Add Firebase to iOS App

Добавьте следующий импорт в AppDelegate.swift

 import Firebase 

Затем добавьте следующее в application(_: didFinishLaunchingWithOptions:) , перед оператором return .

 FIRApp .configure () 

На консоли Firebase нажмите « Готово», чтобы завершить настройку проекта.

Безопасность и правила

Перед извлечением и сохранением данных с сервера Firebase мы настроим аутентификацию и добавим правила, которые ограничивают доступ к данным и проверяют пользовательский ввод перед его сохранением.

Аутентификация

Firebase API позволяет вам настроить электронную почту / пароль, Facebook, Twitter, GitHub, Google и анонимную аутентификацию. В нашем приложении мы будем использовать аутентификацию по электронной почте и паролю.

Чтобы включить аутентификацию по электронной почте / паролю, выберите « Аутентификация» на левой панели консоли Firebase и перейдите на вкладку « Метод входа ». Включите провайдер аутентификации по электронной почте / паролю и нажмите Сохранить .

Enable Email Password Authentication

Вернувшись в Xcode, измените LoginViewController.swift, как показано.

 import UIKit import FirebaseAuth class LoginViewController: UIViewController { @IBOutlet weak var emailField: UITextField! @IBOutlet weak var passwordField: UITextField! override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if let _ = FIRAuth.auth()?.currentUser { self.signIn() } } @IBAction func didTapSignIn(_ sender: UIButton) { let email = emailField.text let password = passwordField.text FIRAuth.auth()?.signIn(withEmail: email!, password: password!, completion: { (user, error) in guard let _ = user else { if let error = error { if let errCode = FIRAuthErrorCode(rawValue: error._code) { switch errCode { case .errorCodeUserNotFound: self.showAlert( "User account not found. Try registering" ) case .errorCodeWrongPassword: self.showAlert( "Incorrect username/password combination" ) default : self.showAlert( "Error: \(error.localizedDescription)" ) } } return } assertionFailure( "user and error are nil" ) } self.signIn() }) } @IBAction func didRequestPasswordReset(_ sender: UIButton) { let prompt = UIAlertController(title: "To Do App" , message: "Email:" , preferredStyle: .alert) let okAction = UIAlertAction(title: "OK" , style: . default ) { (action) in let userInput = prompt.textFields![ 0 ].text if (userInput!.isEmpty) { return } FIRAuth.auth()?.sendPasswordReset(withEmail: userInput!, completion: { (error) in if let error = error { if let errCode = FIRAuthErrorCode(rawValue: error._code) { switch errCode { case .errorCodeUserNotFound: DispatchQueue.main. async { self.showAlert( "User account not found. Try registering" ) } default : DispatchQueue.main. async { self.showAlert( "Error: \(error.localizedDescription)" ) } } } return } else { DispatchQueue.main. async { self.showAlert( "You'll receive an email shortly to reset your password." ) } } }) } prompt.addTextField(configurationHandler: nil) prompt.addAction(okAction) present(prompt, animated: true , completion: nil) } func showAlert(_ message: String) { let alertController = UIAlertController(title: "To Do App" , message: message, preferredStyle: UIAlertControllerStyle.alert) alertController.addAction(UIAlertAction(title: "Dismiss" , style: UIAlertActionStyle. default ,handler: nil)) self.present(alertController, animated: true , completion: nil) } func signIn() { performSegue(withIdentifier: "SignInFromLogin" , sender: nil) } } 

В приведенном выше коде мы сначала проверяем, viewDidAppear() ли пользователь в viewDidAppear() . FIRAuth.auth()?.currentUser выдает аутентифицированного пользователя (если есть). Пользователь представлен объектом FIRUser . Если они signIn() в систему, мы вызываем signIn() который выполняет переход, который уже создан в файле раскадровки. Этот переход переходит к ItemsTableViewController, где будет показан наш список элементов.

didTapSignIn() отправляет входные данные пользователя в Firebase для аутентификации. FIRAuth.auth()?.signIn(withEmail: password: completion:) используется для входа пользователя с использованием комбинации FIRAuth.auth()?.signIn(withEmail: password: completion:) электронной почты и пароля. Каждый из провайдеров аутентификации (например, Google, Facebook, Twitter, Github и т. Д.) Использует разные вызовы методов.

Когда пользователь успешно аутентифицирован, signIn() , в противном случае пользователю будет показано сообщение об ошибке. Если аутентификация не удалась, объект NSError возвращается с сервера. Он содержит код ошибки , который можно использовать для определения причины ошибки и предоставления пользователю соответствующего сообщения об ошибке. В приведенном выше коде мы выдаем разные сообщения об ошибках, когда пользователь входит в несуществующую учетную запись или когда он вводит неправильный пароль. Для других ошибок, мы показываем пользователю описание ошибки с error.localizedDescription . В реальном приложении вы можете не показывать это пользователю. Сообщение, возвращаемое error.localizedDescription , лучше всего подходит для разработчика при отладке, но для ваших пользователей вы должны использовать более качественное сообщение об ошибке.

didRequestPasswordReset() вызывается при нажатии кнопки « Забыли пароль» . Здесь мы создаем окно оповещения, которое пользователь может использовать для ввода адреса электронной почты, на который будет отправлено электронное письмо с паролем сброса. Это еще одно из многих преимуществ, предлагаемых Firebase. Функция сброса пароля уже настроена; Вам не нужно кодировать это самостоятельно.

Если вы посмотрите на элемент управления Firebase в разделе Аутентификация> Шаблоны электронной почты , вы можете изменить сообщение электронной почты, отправленное пользователям, для проверки адреса электронной почты , сброса пароля и изменения адреса электронной почты .

Email Templates

Обратите внимание, что в приведенном выше коде предупреждение вызывается в основном потоке с помощью DispatchQueue.main.async а не в фоновом потоке, который обрабатывает функцию обратного вызова. Любой код, который обновляет пользовательский интерфейс приложения, должен выполняться в основной очереди.

Чтобы проверить функциональность сброса пароля, вы можете создать пользователя на консоли Firebase в разделе Аутентификация> Пользователи> Добавить пользователя . Введите адрес электронной почты и пароль для пользователя (используйте реальный адрес электронной почты, если вы хотите получить письмо для сброса пароля).

Вернувшись в Xcode, запустите проект. Вы должны иметь возможность ввести электронное письмо и отправить ему электронное письмо для сброса пароля.

Reset Password

Reset Password

Закончив вход в систему, давайте добавим функцию регистрации. Измените SignUpViewController, как показано.

 import UIKit import FirebaseAuth class SignUpViewController: UIViewController { @IBOutlet weak var emailField: UITextField ! @IBOutlet weak var passwordField: UITextField ! @IBAction func didTapSignUp(_ sender: UIButton) { let email = emailField . text let password = passwordField . text FIRAuth . auth() ? . createUser(withEmail: email ! , password: password ! , completion: { (user, error) in if let error = error { if let errCode = FIRAuthErrorCode(rawValue: error . _code) { switch errCode { case . errorCodeInvalidEmail: self . showAlert( "Enter a valid email." ) case . errorCodeEmailAlreadyInUse: self . showAlert( "Email already in use." ) default: self . showAlert( "Error: \(error.localizedDescription)" ) } } return } self . signIn() }) } @IBAction func didTapBackToLogin(_ sender: UIButton) { self . dismiss(animated: true , completion: {}) } func showAlert(_ message: String ) { let alertController = UIAlertController(title: "To Do App" , message: message, preferredStyle: UIAlertControllerStyle . alert) alertController . addAction(UIAlertAction(title: "Dismiss" , style: UIAlertActionStyle . default,handler: nil)) self . present(alertController, animated: true , completion: nil) } func signIn() { performSegue(withIdentifier: "SignInFromSignUp" , sender: nil) } } 

В приведенном выше коде, когда пользователь нажимает на кнопку Создать учетную запись , didTapSignUp() . Здесь мы берем пользовательский ввод и вызываем FIRAuth.auth()?.createUser(withEmail: password: completion:) чтобы попытаться создать учетную запись с введенными данными. В случае сбоя регистрации отображается сообщение об ошибке, в противном случае signIn() который выполняет переход к ItemsTableViewController . Этот переход уже был создан в файле раскадровки начального проекта.

Запустите приложение. Вы должны иметь возможность создать учетную запись и подтвердить ее создание на консоли Firebase. Если вы ранее вошли в систему и, таким образом, не можете получить доступ к представлению «Вход в систему», вы можете сбросить симулятор с помощью Simulator> Reset Content and Settings .

По умолчанию Firebase устанавливает правило допустимой длины пароля – оно должно быть не менее 6 символов. Вы можете установить больше правил для этого на консоли Firebase. Вскоре мы увидим, как настроить правила. Подробнее об этом здесь .

Авторизация и проверка данных

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

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

По умолчанию Firebase имеет следующие правила, доступные для просмотра, перейдя в База данных> Правила на консоли.

 { " rules ": { " .read ": "auth != null" , " .write ": "auth != null" } } 

Вышеуказанные правила безопасности требуют, чтобы пользователи проходили аутентификацию для чтения или записи любых данных в базу данных. Измените правила, как показано, и нажмите « Опубликовать» .

 { " rules ": { " users ": { " $uid ": { " .read ": "auth != null && auth.uid == $uid" , " .write ": "auth != null && auth.uid == $uid" , " items ": { " $item_id ": { " title ": { " .validate ": "newData.isString() && newData.val().length > 0" } } } } } } } 

Выше auth != null && auth.uid == $uid был установлен в разрешениях на чтение и запись. С этим правилом не только пользователь должен будет проходить аутентификацию для чтения или записи любых данных в базу данных, но он также будет иметь доступ только к своим собственным данным.

Firebase хранит данные в формате JSON. В нашей базе данных каждый пользователь будет иметь массив дел с именами items . Каждый item будет иметь title . Выше мы добавляем некоторую проверку, которая гарантирует, что элемент с пустым заголовком не будет сохранен.

Сохранение данных

После завершения аутентификации и авторизации мы увидим, как данные можно сохранять и извлекать в Firebase и из него.

Сначала создайте новый файл с именем Item.swift и измените его, как показано на рисунке. Это будет класс модели Item Каждый Item будет иметь title и FIRDatabaseReference которая будет содержать объект FIRDatabaseReference . FIRDatabaseReference представляет определенное местоположение в вашей базе данных Firebase и может использоваться для чтения или записи данных в это местоположение базы данных Firebase.

 import Foundation import FirebaseDatabase class Item { var ref: FIRDatabaseReference ? var title: String ? init (snapshot: FIRDataSnapshot) { ref = snapshot . ref let data = snapshot . value as ! Dictionary < String , String > title = data [ "title" ] ! as String } } 

Затем измените ItemsTableViewController, как показано.

 import UIKit import Firebase class ItemsTableViewController: UITableViewController { var user: FIRUser! var items = [Item]() var ref : FIRDatabaseReference! private var databaseHandle: FIRDatabaseHandle! override func viewDidLoad () { super.viewDidLoad() user = FIRAuth.auth()?.currentUser ref = FIRDatabase.database().reference() startObservingDatabase() } // MARK: - Table view data source override func numberOfSections( in tableView: UITableView) -> Int { return 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return items.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell" , for : indexPath) let item = items[indexPath.row] cell.textLabel?.text = item.title return cell } override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { let item = items[indexPath.row] item. ref ?.removeValue() } } @IBAction func didTapSignOut(_ sender: UIBarButtonItem) { do { try FIRAuth.auth()?.signOut() performSegue(withIdentifier: "SignOut" , sender: nil) } catch let error { assertionFailure( "Error signing out: \(error)" ) } } @IBAction func didTapAddItem(_ sender: UIBarButtonItem) { let prompt = UIAlertController(title: "To Do App" , message: "To Do Item" , preferredStyle: .alert) let okAction = UIAlertAction(title: "OK" , style: . default ) { (action) in let userInput = prompt.textFields![ 0 ].text if (userInput!.isEmpty) { return } self. ref .child( "users" ).child(self.user.uid).child( "items" ).childByAutoId().child( "title" ).setValue(userInput) } prompt.addTextField(configurationHandler: nil) prompt.addAction(okAction) present(prompt, animated: true , completion: nil); } func startObservingDatabase () { databaseHandle = ref .child( "users/\(self.user.uid)/items" ).observe(. value , with: { (snapshot) in var newItems = [Item]() for itemSnapShot in snapshot.children { let item = Item(snapshot: itemSnapShot as ! FIRDataSnapshot) newItems.append(item) } self.items = newItems self.tableView.reloadData() }) } deinit { ref .child( "users/\(self.user.uid)/items" ).removeObserver(withHandle: databaseHandle) } } 

Выше мы начинаем с создания некоторых переменных в viewDidLoad() . Мы устанавливаем для user значение зарегистрированного пользователя, а затем устанавливаем ref для объекта FIRDatabaseReference . FIRDatabase.database().reference() получает FIRDatabaseReference для корня вашей базы данных Firebase. Затем мы вызываем startObservingDatabase() .

В startObservingDatabase() мы устанавливаем прослушиватель для любых изменений в базе данных. Данные Firebase извлекаются путем присоединения асинхронного слушателя к ссылке FIRDatabase . Слушатель запускается один раз для начального состояния данных и снова каждый раз, когда данные изменяются. Чтобы добавить прослушиватель событий, мы используем метод observeEventType() чтобы указать тип события и блок обратного вызова. Вы можете прослушать следующие типы событий:

  • FIRDataEventTypeValue – чтение и прослушивание изменений всего содержимого пути.
  • FIRDataEventTypeChildAdded – получение списков элементов или прослушивание дополнений к списку элементов. Рекомендуемое использование с FIRDataEventTypeChildChanged и FIRDataEventTypeChildRemoved для мониторинга изменений в списках.
  • FIRDataEventTypeChildChanged – прослушивать изменения элементов в списке. Используйте с FIRDataEventTypeChildAdded и FIRDataEventTypeChildRemoved для мониторинга изменений в списках.
  • FIRDataEventTypeChildRemoved – прослушивает элементы, удаляемые из списка. Используйте с FIRDataEventTypeChildAdded и FIRDataEventTypeChildChanged для мониторинга изменений в списках.
  • FIRDataEventTypeChildMoved – прослушивает изменения порядка элементов в упорядоченном списке. События FIRDataEventTypeChildMoved всегда следуют за событием FIRDataEventTypeChildChanged которое вызвало изменение порядка элемента (в зависимости от вашего текущего метода order-by).

Мы слушаем событие FIRDataEventTypeValue . Вы используете событие FIRDataEventTypeValue для чтения данных по заданному пути, поскольку они существуют на момент события. Этот метод запускается один раз, когда слушатель подключен, и снова каждый раз, когда изменяются данные, включая любые дочерние элементы Обратному вызову события передается snapshot содержащий все данные в этом месте, включая дочерние данные. Если данных нет, возвращаемое значение снимка равно nil .

Обратите внимание, что событие FIRDataEventTypeValue вызывается каждый раз, когда данные изменяются по указанной ссылке на базу данных, включая изменения дочерних FIRDataEventTypeValue . Чтобы ограничить размер ваших снимков, вы должны прикреплять только на самом высоком уровне, необходимом для просмотра изменений. Например, присоединение слушателя к корню вашей базы данных не рекомендуется. В нашем коде мы присоединяем слушателя к /users/{user id}/items который является путем, который будет содержать элементы пользователя.

Слушатель получает FIRDataSnapshot который содержит данные в указанном месте в базе данных во время события в своем свойстве значения. Если в местоположении нет данных, значение равно nil .

Из снимка мы создаем объекты Item устанавливающие их в массив items который используется для заполнения табличного представления. Затем мы перезагружаем табличное представление, чтобы отразить изменение данных.

numberOfSectionsInTableView() , tableView(_: numberOfRowsInSection:) и tableView(_: cellForRowAtIndexPath) – это обычные функции табличного представления, используемые для установки данных табличного представления.

Приложение содержит кнопку «Добавить» на панели навигации, которая при нажатии вызывает didTapAddItem() . Здесь мы показываем пользователю окно оповещения, которое он может использовать для добавления элемента в табличное представление. Мы сохраняем добавленную пользователем ценность в /users/{user id}/items/{item id}/title/ .

Существует четыре способа записи данных в базу данных Firebase Realtime:

  • setValueзаписывает или заменяет данные по определенному пути, например users/<user-id>/<username> .
  • childByAutoId – Добавить в список данных. Каждый раз, когда вы вызываете childByAutoId , Firebase генерирует уникальный ключ, который также может использоваться в качестве уникального идентификатора, например user-posts/<user-id>/<unique-post-id> .
  • updateChildValuesобновляет некоторые ключи для определенного пути без замены всех данных.
  • runTransactionBlockобновляет сложные данные, которые могут быть повреждены при одновременном обновлении.

Функция child() получает ссылку на конкретный узел, если он существует, или создает его, если он не существует. Мы используем childByAutoId() чтобы установить уникальный идентификатор для каждого item который будет создан. Затем мы используем setValue() для добавления ввода пользователя в качестве значения для title элемента.

tableView(_: commitEditingStyle: forRowAtIndexPath:) делает редактирование табличного представления. При этом пользователь сможет сильно ударить элемент, чтобы удалить его. Мы вызываем removeValue() со ссылкой на объект, на который removeValue() . Это удаляет данные в этом месте в базе данных. После удаления элемента нам не нужно вносить никаких изменений в представление таблицы. Помните, что мы устанавливаем прослушиватель на любые изменения, сделанные в пути ../items/ в базе данных, поэтому обратный вызов, который мы установили для этого, будет отвечать за обновление таблицы.

didTapSignOut() вызывается при нажатии кнопки «Выход» на панели навигации приложения. Здесь мы выходим из системы и возвращаемся к экрану входа.

Запустите приложение, и вы сможете добавить элемент.

Add Item

Любой добавленный вами элемент будет добавлен в табличное представление.

Item List

Проверьте консоль Firebase, и вы увидите добавленные данные.

Item List on Firebase Console

Чтобы удалить элемент, проведите по нему, чтобы открыть кнопку «Удалить».

Delete Item

Это подводит нас к концу урока. Мы увидели, как сохранять и извлекать данные в и из базы данных Firebase в реальном времени и как настроить аутентификацию и авторизацию данных. Рассмотренное нами сохранение данных идеально подходит для простых типов данных, таких как NSString , NSNumber , NSArray и NSDictionary . Если вы хотите сохранить файлы, такие как изображения или документы, в Firebase, то загляните в Firebase Storage .

Учебное пособие было кратким введением в использование Firebase, оно не было исчерпывающим обзором всего, что делает Firebase. Для этого обязательно прочитайте документацию . Вы можете скачать готовый проект здесь . Не забудьте добавить в проект файл GoogleService-Info.plist, созданный из Firebase.

Заметка

Возможно, вы заметили некоторые предупреждения в вашем проекте после добавления библиотек Firebase или при запуске завершенного проекта, если вы загрузили его. В предупреждающем сообщении об ошибке указано, что Conflicting nullability specifier on return types, 'nullable' conflicts with existing specifier 'nonnull' . Эта небольшая ошибка появилась с обновлением Firebase до Swift 3, и проблема была устранена . Если вы посмотрите на цепочки сообщений в этой последней ссылке, проблема была исправлена, но изменения не будут видны в Firebase до следующего выпуска. Приложение все еще работает, несмотря на это.