Приложения для знакомств в последнее время стали одним из самых популярных жанров в App Store. Однако из-за их характера разработка полнофункционального приложения для знакомств может быть сложной задачей. Во второй части этой серии мы рассмотрим, как мы можем использовать платформу Sinch в приложении для iOS для реализации голосовых вызовов и обмена сообщениями. Это две основные функции для приложений для знакомств, и их на удивление легко реализовать с помощью Sinch .
1. Обзор
Прежде чем мы начнем кодировать, давайте рассмотрим приложение, которое мы делаем. Наше приложение для знакомств будет иметь несколько основных функций, на которых мы сосредоточимся в этом уроке. Для учетных записей пользователи смогут войти с помощью Facebook. После входа они смогут видеть других пользователей приложения, которые находятся поблизости.
Пользователи хранятся на сервере, с которым мы общаемся, с помощью API-интерфейса restful, созданного в первой части этого руководства . Как только мы получаем информацию о пользователе из Facebook, на сервере публикуется новый пользователь и создается сеанс. После этого будет выбран список зарегистрированных пользователей для выбора.
Когда пользователь находит того, с кем хочет поговорить, он может либо отправить ему сообщение, либо инициировать голосовой вызов. Это где Синч вступит в игру. Мы будем использовать их SDK для выполнения всей тяжелой работы по обработке голосовых вызовов и обмену сообщениями.
Теперь, когда мы знаем, что мы развиваем, пришло время начинать.
2. Стартовый проект
Начните с загрузки начального проекта с GitHub . Приложение предназначено для iPhone и содержит пользовательский интерфейс, проверку подлинности Facebook и связь с уже существующим API RESTful. Это позволит нам сосредоточиться на интеграции с Sinch.
Прежде чем продолжить, я хотел бы обсудить базовую архитектуру приложения и то, как оно взаимодействует с сервером. Код, который связывается с остальным API, находится в классе UsersAPIClient
. Он использует популярную библиотеку AFNetworking для упрощения сетевой логики. Если вы уже использовали AFNetworking, это будет вам знакомо. Если вы этого не сделали, наш учебник об AFNetworking — отличное место для начала.
В файле заголовка вы увидите несколько методов взаимодействия с различными конечными точками RESTful API, таких как создание пользователя, запуск сеанса, удаление пользователя и т. Д.
1
2
3
4
|
— (void)createUser:(User *)user completion:(void (^)())action;
— (void)beginUserSession:(User *)user completion:(void (^)())action;
— (void)deleteUser:(User *)user completion:(void (^)())action;
— (void)getRegisteredUsersWithMeters:(double)meters completion:(void (^)(NSArray *users))action;
|
Когда ответ возвращается с сервера, JSON анализируется и используется для инициализации модели.
Говоря о моделях, в проекте используется одна модель — User
. Он содержит основную информацию о пользователе, такую как имя, местоположение, идентификатор и т. Д. Идентификатор пользователя особенно важен, так как он нам нужен, чтобы уникально идентифицировать, с кем мы общаемся, когда мы интегрируемся с Sinch.
Потратьте несколько минут, чтобы просмотреть проект, чтобы лучше понять его структуру. Как и в случае сторонних библиотек, чрезвычайно полезно просматривать документацию и исходный код, прежде чем вы его фактически используете.
3. Построить и запустить
Если вы создаете и запускаете демонстрационный проект, вам будет предложено войти через Facebook.
Идите и проверьте, можете ли вы войти через Facebook. После входа демо-приложение выполняет три действия:
- Он создает пользовательскую запись на сервере, если он отсутствует.
- Он запускает сеанс пользователя и создает токен доступа для сеанса.
- Получает список пользователей в системе.
Позже, когда вы сделали несколько тестовых аккаунтов, вы можете найти пользователей в определенном диапазоне. По умолчанию каждый пользователь в системе возвращается, так как маловероятно, что другие пользователи будут рядом во время тестирования.
4. Создайте учетную запись Sinch
Чтобы использовать Sinch SDK, сначала необходимо создать учетную запись разработчика. Перейдите на сайт Sinch и нажмите « Начать бесплатно» в правом верхнем углу, чтобы зарегистрироваться.
Введите свой адрес электронной почты и пароль, и примите условия использования. Sinch автоматически запустит процесс установки после регистрации. На данный момент все, что вам нужно сделать, это назвать свое приложение и добавить краткое описание. Вы можете пропустить остальную часть руководства по началу работы.
Затем перейдите на панель инструментов, чтобы получить ключ и секрет API вашего приложения. Вы должны увидеть Dashboard в верхней части сайта Sinch после того, как войдете в свою новую учетную запись. Выберите « Приложения» слева, выберите приложение, которое вы создали, и щелкните значок ключей справа, чтобы отобразить ключ и секрет. Вы хотите отслеживать эти значения, чтобы позже инициализировать клиент Sinch.
5. Добавить Sinch SDK
Есть два способа добавить Sinch SDK в ваш проект. На сегодняшний день самый простой подход — через CocoaPods. Добавьте следующую строку в ваш Podfile:
1
|
pod ‘SinchService’, ‘~> 1.0’
|
Затем выполните следующую команду, чтобы интегрировать Sinch SDK в ваше рабочее пространство:
1
|
pod update
|
Однако в этом руководстве я покажу вам, как добавить SDK, если вы не используете CocoaPods. Начните с загрузки Sinch SDK для iOS. Посетите раздел загрузок на веб-сайте Sinch и загрузите SDK для iOS. Sinch SDK зависит от трех платформ, поэтому нам нужно сначала связать наш проект с этими. В Навигаторе проектов выберите SinchTutorial и выберите цель SinceTutorial .
Выберите « Фазы сборки»> «Связать двоичные файлы с библиотеками» и нажмите кнопку «плюс» внизу.
Добавьте следующие рамки в список платформ для связи с:
- Безопасность
- AudioToolbox
- AVFoundation
Нам также нужно добавить три флага компоновщика. В настройках сборки цели найдите другие флаги компоновщика .
Добавьте следующие флаги компоновщика под Debug :
1
|
-ObjC -Xlinker -lc++
|
Наконец, перетащите ранее загруженный каркас Sinch в папку Frameworks в Навигаторе проектов . Создайте свой проект, чтобы убедиться в отсутствии ошибок. Теперь вы готовы начать использовать Sinch SDK.
6. Настройка Sinch Client
Клиент Sinch является движущей силой, отвечающей за связь с платформой Sinch. Прежде чем мы сможем использовать функции обмена сообщениями или вызова, нам нужно инициализировать экземпляр клиента Sinch. Для этого нам нужен ключ и секрет, которые мы получили ранее после создания нашей учетной записи Sinch.
Откройте AppDelegate.h и импортируйте инфраструктуру Sinch, как показано ниже.
1
|
#import <Sinch/Sinch.h>
|
Далее нам нужно создать свойство для клиента Sinch и прототип метода для его инициализации. Добавьте следующий фрагмент кода под свойством window
.
1
2
3
|
@property (strong, nonatomic) id<SINClient> client;
— (void)sinchClientWithUserId:(NSString *)userId;
|
Мы будем использовать эту ссылку на клиент Sinch, чтобы легко выполнять звонки и обмен сообщениями. Протокол SINClientDelegate
сообщит нам, было ли соединение с бэкэндом Sinch успешным.
Соответствуйте классу SINClientDelegate
протоколу SINClientDelegate
, как показано ниже.
1
2
|
//Conform to SINClientDelegate
@interface AppDelegate : UIResponder <UIApplicationDelegate, SINClientDelegate>
|
Переключитесь на файл AppDelegate
класса AppDelegate
и sinchClientWithUserId:
метод sinchClientWithUserId:
как показано ниже.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
#pragma mark — Sinch
— (void)sinchClientWithUserId:(NSString *)userId
{
if (!_client)
{
_client = [Sinch clientWithApplicationKey:@»<YOUR_APP_KEY>»
applicationSecret:@»<YOUR_APP_SECRET>»
environmentHost:@»sandbox.sinch.com»
userId:userId];
_client.delegate = self;
[_client setSupportCalling:YES];
[_client setSupportMessaging:YES];
[_client start];
[_client startListeningOnActiveConnection];
}
}
|
Мы будем использовать этот метод позже для связи с бэкэндом Sinch после того, как пользователь вошел в систему. Не забудьте ввести ключ и секретный ключ приложения. В противном случае вы не сможете подключиться к бэкэнду Sinch.
После инициализации клиента мы сообщаем Синчу, что будем использовать функции обмена сообщениями и вызова. Затем мы запускаем клиент и устанавливаем соединение со службами Sinch для приема входящих звонков.
Далее, реализуйте следующие методы SINClientDelegate
протокола AppDelegate
классе AppDelegate
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
— (void)clientDidStart:(id<SINClient>)client
{
NSLog(@»Sinch client started successfully (version: %@)», [Sinch version]);
}
— (void)clientDidFail:(id<SINClient>)client error:(NSError *)error
{
NSLog(@»Sinch client error: %@», [error localizedDescription]);
}
— (void)client:(id<SINClient>)client logMessage:(NSString *)message area:(NSString *)area severity:(SINLogSeverity)severity timestamp:(NSDate *)timestamp
{
if (severity == SINLogSeverityCritical)
{
NSLog(@»%@», message);
}
}
|
Эти методы делегата будут регистрировать ошибки на консоли XCode, если они произойдут, и они также сообщают нам, когда мы успешно соединились с Sinch.
7. Инициализация Sinch Client
Откройте FindUsersViewController.m и перейдите к методу beginUserSession
. В этом методе, если сеанс был запущен без ошибок, выполняется блок завершения. Это хорошее время для инициализации клиента Sinch. Замените комментарий //TODO
следующим блоком кода:
1
2
3
|
//Init sinch client
AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
[delegate sinchClientWithUserId:self.curUser.userID];
|
Это вызывает метод для запуска клиента Sinch и позволяет нам начать использовать его функции. Создайте и запустите приложение. После того, как вы получили список пользователей, подтвердите, что соединение с бэкэндом Sinch было успешным, проверив журналы в консоли XCode.
8. Реализация обмена сообщениями
Теперь мы приступаем к интересной части, добавляя компонент обмена сообщениями с очень небольшим количеством кода, используя Sinch SDK. Начните с открытия MessagingViewController.m и импортируйте инфраструктуру Sinch.
1
|
#import <Sinch/Sinch.h>
|
Так же, как протокол SINClientDelegate
предоставляет полезные методы, которые помогают нам отслеживать статус клиента, протокол SINMessageClientDelegate
информирует нас о входящих и исходящих сообщениях, их статусе и SINMessageClientDelegate
другом.
Начните с согласования класса MessagingViewController
с этим протоколом, обновив интерфейс класса.
1
|
@interface MessagingViewController () <UITableViewDataSource, UITableViewDataSource, UITextFieldDelegate, SINMessageClientDelegate>
|
Чтобы реализовать обмен сообщениями, нам также понадобится экземпляр SINMessageClient
для выполнения функций обмена сообщениями. Добавьте свойство для экземпляра SINMessageClient
.
1
|
@property (strong, nonatomic) id<SINMessageClient> sinchMessageClient;
|
Мы можем создать экземпляр клиента обмена сообщениями из клиента Sinch, который у нас уже есть в делегате приложения. Когда контроллер представления загружается, нам нужно инициализировать его и установить контроллер представления в качестве его делегата. Добавьте следующий блок кода в метод viewDidLoad
контроллера представления:
1
2
3
|
//Setup Sinch message client
self.sinchMessageClient = [((AppDelegate *)[[UIApplication sharedApplication] delegate]).client messageClient];
self.sinchMessageClient.delegate = self;
|
Теперь, когда у нас есть объект для отправки и получения сообщений и делегат для их обработки, давайте добавим код для составления сообщения. Найдите sendMessage:
метод в нижней части контроллера представления и добавьте следующую реализацию:
1
2
3
|
[self dismissKeyboard];
SINOutgoingMessage *outgoingMessage = [SINOutgoingMessage messageWithRecipient:self.selectedUser.userID text:self.messageTextField.text];
[self.sinchMessageClient sendMessage:outgoingMessage];
|
Экземпляр SINOutgoingMessage
свяжется с Sinch и передаст сообщение получателю. Мы назначаем получателя, предоставляя его идентификатор пользователя из модели пользователя. Sinch будет знать, куда обращаться с сообщением, направляя его через API, используя комбинацию ключа приложения, секрета и уникального идентификатора пользователя, переданного ему.
Поскольку сообщение может быть либо входящим, либо исходящим, добавьте перечисление в верхней части контроллера представления, чтобы учесть любой сценарий. Добавьте его ниже операторов импорта.
1
2
3
4
5
|
typedef NS_ENUM(int, MessageDirection)
{
Incoming,
Outgoing
};
|
Наконец, нам понадобится клиент обмена сообщениями для фактической обработки и отправки сообщения. В этом контроллере представления нам нужно обрабатывать как отправку, так и получение сообщений. Добавьте следующий фрагмент кода, чтобы получить сообщение:
1
2
3
4
5
6
7
8
|
#pragma mark SINMessageClient
// Receiving an incoming message.
— (void)messageClient:(id<SINMessageClient>)messageClient didReceiveIncomingMessage:(id<SINMessage>)message
{
NSLog(@»Received a message»);
[self.messages addObject:@[message, @(Incoming)]];
[self.tableView reloadData];
}
|
Всякий раз, когда мы получаем сообщение, мы добавляем его в массив messages
и перезагружаем табличное представление, чтобы отобразить его. Важно отметить, что объект SINMessage
состоит из двух элементов: самого объекта сообщения и его направления (входящего или исходящего).
Теперь, когда мы можем отправлять и получать сообщения, нам нужно знать, когда сообщение завершило отправку, если мы являемся автором, чтобы мы могли обновить источник данных и перезагрузить представление таблицы. Для этого мы реализуем messageSent:recipientId:
как показано ниже.
1
2
3
4
5
6
7
8
|
// Finish sending a message
— (void)messageSent:(id<SINMessage>)message recipientId:(NSString *)recipientId
{
NSLog(@»Finished sending a message»);
self.messageTextField.text = @»»;
[self.messages addObject:@[message, @(Outgoing)]];
[self.tableView reloadData];
}
|
Мы также можем реализовать некоторые из оставшихся методов делегирования, чтобы помочь нам устранить неполадки в процессе обмена сообщениями, если что-то пойдет не так. Добавьте следующие методы делегата ниже messageSent:recipientId:
метод.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
// Failed to send a message
— (void)messageFailed:(id<SINMessage>)message info:(id<SINMessageFailureInfo>)messageFailureInfo
{
NSLog(@»Failed to send a message:%@», messageFailureInfo.error.localizedDescription);
}
-(void)messageDelivered:(id<SINMessageDeliveryInfo>)info
{
NSLog(@»Message was delivered»);
}
— (void)message:(id<SINMessage>)message shouldSendPushNotifications:(NSArray *)pushPairs
{
NSLog(@»Recipient not online.\nShould notify recipient using push (not implemented in this tutorial).\nPlease refer to the documentation.»);
}
|
На этом этапе обмен сообщениями с Sinch готов к использованию. Последнее, что нам нужно сделать, это отредактировать tableView:cellForRowAtIndexPath:
метод для отображения сообщений. Вот как должна выглядеть реализация:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
— (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell;
id<SINMessage> message = [self.messages[indexPath.row] firstObject];
MessageDirection direction = (MessageDirection)[[self.messages[indexPath.row] lastObject] intValue];
if (direction == Incoming)
{
cell = [self.tableView dequeueReusableCellWithIdentifier:CELL_ID_RECIPIENT];
((RecipientTableViewCell *)cell).message.text = message.text;
}
else
{
cell = [self.tableView dequeueReusableCellWithIdentifier:CELL_ID_USER];
((UsersTableViewCell *)cell).message.text = message.text;
}
return cell;
}
|
9. Тестовые сообщения
Создайте и запустите приложение, чтобы убедиться в отсутствии ошибок. Вы готовы протестировать функцию обмена сообщениями. Чтобы попробовать, вам понадобится доступ к другой учетной записи Facebook.
На вашем устройстве iOS создайте и запустите приложение и войдите в систему с первой учетной записью. На симуляторе iOS запустите другой экземпляр приложения и войдите под вторым аккаунтом. Перейдите к профилю друг друга на каждом устройстве и нажмите « Сообщение» .
На этом этапе вы можете ввести сообщение и нажать Отправить . Он поступит на устройство получателя практически мгновенно. Вы завершили внедрение работающего клиента обмена сообщениями для вашего приложения для знакомств на платформе Sinch.
Если пользователь не в сети, клиент попытается отправить сообщение, но оно не будет доставлено другому пользователю. В этом случае вы можете реализовать push-уведомления, чтобы получатель знал, что получил сообщение.
10. VoIP Calling
Последняя функция, для которой мы будем использовать Sinch, — это VoIP (Voice over IP) вызовы. Откройте CallingViewController.m и импортируйте инфраструктуру Sinch в последний раз.
1
|
#import <Sinch/Sinch.h>
|
Как и прежде, мы будем использовать некоторые протоколы для обработки вызовов. Протоколы SINCallClientDelegate
и SINCallDelegate
предоставляют нам методы для инициирования и приема вызова, а также для обновления статуса вызова. Обновите интерфейс класса CallingViewController
чтобы он соответствовал обоим протоколам.
1
|
@interface CallingViewController () <SINCallClientDelegate, SINCallDelegate>
|
Для реализации вызова VoIP нам потребуется доступ к клиенту Sinch, а также к объекту SINCall
. Добавьте свойство для экземпляра SINCall
как показано ниже.
1
|
@property (strong, nonatomic) id<SINCall> sinchCall;
|
В viewDidLoad
установите контроллер представления в качестве делегата клиента вызова Sinch.
1
2
3
|
//Assign this VC as Sinch’s call delegate
self.delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
self.delegate.client.callClient.delegate = self;
|
Чтобы лучше понять следующий раздел, он помогает разобраться в сценариях, с которыми сталкиваются пользователи при использовании функции вызова. Пользователь может быть:
- прием звонка
- звонить
- повесить существующий вызов
- отклонение входящего звонка
Давайте сначала рассмотрим, как сделать и принять звонок. Добавьте следующий код в метод makeOrAcceptCallFromSelectedUser:
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
#pragma mark — Initiate or Accept Call
— (IBAction)makeOAccepetCallFromSelectedUser:(UIButton *)sender
{
if ([sender.titleLabel.text isEqualToString:@»Call»])
{
self.sinchCall = [self.delegate.client.callClient callUserWithId:self.selectedUser.userID];
self.sinchCall.delegate = self;
self.lblCallStatus.text = [NSString stringWithFormat:@»- calling %@ -«, self.selectedUser.userName];
self.btnDeny.hidden = NO;
[self.btnDeny setTitle:@»Hang Up» forState:UIControlStateNormal];
}
else
{
[self.sinchCall answer];
self.lblCallStatus.text = [NSString stringWithFormat:@»- talking to %@ -«, self.selectedUser.userName];
}
}
|
Мы проверяем, делаем ли мы вызов или принимаем его. Если мы инициируем вызов, callUserWithId:
запустит вызывающий процесс и инициализирует свойство sinchCall
.
Если мы sinchCall
на вызов, свойство sinchCall
уже будет инициализировано, и мы просто начнем вызов с вызова answer
на него.
Далее мы осуществим отклонение и отбой вызова. Добавьте следующий код в метод rejectOrHangUpCallFromSelectedUser:
:
1
2
3
4
5
6
|
#pragma mark — Deny or Hang Up Call
— (IBAction)rejectOrHangUpCallFromSelectedUser:(UIButton *)sender
{
[self.sinchCall hangup];
self.lblCallStatus.text = [NSString stringWithFormat:@»- call with %@ ended -«, self.selectedUser.userName];
}
|
Этот код легче усваивается, потому что, независимо от ситуации, он обрабатывается путем вызова hangup
sinchCall
свойства sinchCall
.
Нам остается только реализовать необходимые методы делегата. Это будет довольно много кода, но он прост для понимания и имена методов очень наглядны.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
#pragma mark — SINCall/Client
— (void)client:(id<SINCallClient>)client didReceiveIncomingCall:(id<SINCall>)call
{
self.sinchCall = call;
self.sinchCall.delegate = self;
self.lblCallStatus.text = [NSString stringWithFormat:@»- incoming call from %@ -«, self.selectedUser.userName];
[self.btnAcceptOrCall setTitle:@»Accept Call» forState:UIControlStateNormal];
self.btnDeny.hidden = NO;
}
— (void)callDidEstablish:(id<SINCall>)call
{
NSLog(@»Call connected.»);
self.btnDeny.hidden = NO;
[self.btnDeny setTitle:@»Hang Up» forState:UIControlStateNormal];
self.lblCallStatus.text = @»- call connected -«;
self.btnAcceptOrCall.hidden = YES;
}
— (void)callDidEnd:(id<SINCall>)call
{
NSLog(@»Call finished»);
self.sinchCall = nil;
self.lblCallStatus.text = @»- call ended -«;
self.btnDeny.hidden = YES;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.navigationController popViewControllerAnimated:YES];
});
}
— (void)callDidProgress:(id<SINCall>)call
{
// In this method you can play ringing tone and update ui to display progress of call.
}
|
Первый метод didReceiveIncomingCall:
обрабатывает получение вызова и настройку свойства sinchCall
. Мы также узнаем, когда вызов начинается и заканчивается из callDidEstablish:
и callDidEnd:
Последний метод, callDidProgress:
настоящее время пуст, но он будет полезен для обновления пользовательского интерфейса полезной информацией, такой как продолжительность вызова. Вы можете использовать SINCallDetail
и его establishedTime
свойство, чтобы легко сделать этот расчет.
11. Тестовый вызов
Запустите два экземпляра приложения, используя две учетные записи Facebook, как вы делали раньше. Перейдите к профилю друг друга и нажмите « Вызов» .
Нажмите зеленую кнопку вызова на вашем устройстве или в симуляторе iOS. После того, как вызов отправлен в Sinch, пользовательский интерфейс обновится, чтобы сообщить пользователю, что вызов выполняется.
Точно так же, если один из пользователей получает вызов, пользовательский интерфейс будет выглядеть так:
Наконец, когда вызов выполняется, пользовательский интерфейс будет отражать состояние вызова. Кнопка с надписью Hang Up будет видна для завершения вызова.
Подобно обмену сообщениями, попытка позвонить пользователю, который не в сети, не поддерживается приложением. Как и в случае с сообщениями, вы можете решить эту проблему с помощью push-уведомлений, чтобы уведомить пользователя о том, что кто-то пытался вам позвонить.
12. Куда идти дальше
Есть несколько частей приложения, которые можно улучшить. Наиболее заметным является интеграция push-уведомлений. Sinch недавно добавила встроенную поддержку push-уведомлений в свой iOS SDK, снимая бремя с разработчиков. С помощью push-уведомлений вы можете уведомить пользователей, которые в данный момент не используют приложение, о том, что они получили звонок или сообщение.
Еще одним полезным дополнением будет хранение сообщений и разговоров на удаленном сервере. Таким образом, пользователи могут вернуться в прошлое и просмотреть прошлые разговоры, которые у них были. Это также делает обмен сообщениями более плавным, поскольку они могут начать разговор с того места, на котором остановились.
Это всего лишь несколько идей. Я уверен, что вы можете придумать еще несколько, которые сделают приложение более полным и привлекательным.
Вывод
Мощный SDK Sinch делает две пугающие задачи доступными, простыми и доступными. Как вы видели в этом руководстве, Sinch позволяет разработчикам интегрировать обмен сообщениями и вызовы в свои приложения в считанные минуты. Sinch SDK не ограничивается iOS. Если в вашем приложении для знакомств есть, например, компонент веб-сайта, вы можете использовать их API JavaScript для реализации тех же функций.
Если вы застряли на этом пути, не волнуйтесь, вы можете найти готовый проект на GitHub . Теперь вы можете взять то, что вы узнали в этом уроке, и приступить к созданию следующего Tinder.