Централизованная и развязанная сеть iOS: учебник AFNetworking с классом Singleton
Опубликовано: 2022-03-11Когда дело доходит до шаблонов архитектуры iOS, шаблон проектирования Модель-Представление-Контроллер (MVC) отлично подходит для долговечности и удобства сопровождения кодовой базы приложения. Это позволяет легко повторно использовать или заменять классы для поддержки различных требований, отделяя их друг от друга. Это помогает максимально использовать преимущества объектно-ориентированного программирования (ООП).
Хотя эта архитектура iOS-приложений хорошо работает на микроуровне (отдельные экраны/разделы приложения), вы можете добавлять аналогичные функции в несколько моделей по мере роста вашего приложения. В таких случаях, как сетевое взаимодействие, перенос общей логики из классов моделей в одноэлементные вспомогательные классы может быть лучшим подходом. В этом учебном пособии по AFNetworking для iOS я научу вас, как настроить централизованный одноэлементный сетевой объект, который, отделенный от компонентов MVC микроуровня, можно повторно использовать в приложении с несвязанной архитектурой.
Проблема с сетью iOS
Apple проделала большую работу по абстрагированию многих сложностей управления мобильным оборудованием с помощью простых в использовании SDK для iOS, но в некоторых случаях, таких как работа в сети, Bluetooth, OpenGL и обработка мультимедиа, классы могут быть громоздкими из-за их цели сохранить гибкие SDK. К счастью, богатое сообщество разработчиков iOS создало высокоуровневые фреймворки для упрощения наиболее распространенных вариантов использования, стремясь упростить дизайн и структуру приложений. Хороший программист, использующий лучшие практики архитектуры iOS-приложений, знает, какие инструменты использовать, зачем их использовать и когда лучше писать собственные инструменты и классы с нуля.
AFNetworking — отличный пример работы с сетью и одна из наиболее часто используемых сред с открытым исходным кодом, упрощающая повседневные задачи разработчика. Он упрощает сетевое взаимодействие RESTful API и создает модульные шаблоны запроса/ответа с блоками завершения успеха, прогресса и отказа. Это устраняет необходимость в реализованных разработчиками методах делегата и настраиваемых параметрах запроса/соединения и может быть очень быстро включено в любой класс.
Проблема с AFNetworking
AFNetworking великолепен, но его модульность также может привести к фрагментарному использованию. Общие неэффективные реализации могут включать:
Несколько сетевых запросов с использованием схожих методов и свойств в одном контроллере представления
Почти идентичные запросы в нескольких контроллерах представления, которые приводят к распределенным общим переменным, которые могут не синхронизироваться.
Сетевые запросы в классе для данных, не связанных с этим классом
Для приложений с ограниченным числом представлений, несколькими вызовами API для реализации и приложениями, которые вряд ли будут часто меняться, это может не иметь большого значения. Однако, скорее всего, вы мыслите масштабно и у вас запланированы многолетние обновления. Если ваш случай последний, вам, вероятно, придется обрабатывать:
Управление версиями API для поддержки нескольких поколений приложения.
Добавление новых параметров или изменение существующих параметров с течением времени для расширения возможностей
Внедрение совершенно новых API
Если ваш сетевой код разбросан по всей кодовой базе, это может стать кошмаром. Надеюсь, у вас хотя бы некоторые из ваших параметров определены статически в общем заголовке, но даже тогда вы можете коснуться дюжины классов даже для самых незначительных изменений.
Как мы устраняем ограничения AFNetworking?
Создайте сетевой синглтон, чтобы централизовать обработку запросов, ответов и их параметров.
Одноэлементный объект обеспечивает глобальную точку доступа к ресурсам своего класса. Одиночки используются в ситуациях, когда желательна эта единая точка управления, например, с классами, которые предлагают некоторые общие услуги или ресурсы. Вы получаете глобальный экземпляр из одноэлементного класса с помощью фабричного метода. - Яблоко
Таким образом, синглтон — это класс, экземпляр которого у вас будет только один в приложении, который существует на протяжении всей жизни приложения. Кроме того, поскольку мы знаем, что существует только один экземпляр, он легко доступен любому другому классу, которому требуется доступ к его методам или свойствам.
Вот почему мы должны использовать синглтон для сети:
Он инициализируется статически, поэтому после создания он будет иметь те же методы и свойства, доступные любому классу, который попытается получить к нему доступ. Нет никаких шансов на странные проблемы с синхронизацией или запрос данных из неправильного экземпляра класса.
Вы можете ограничить количество вызовов API, чтобы не превысить лимит скорости (например, когда вам нужно поддерживать количество запросов API менее пяти в секунду).
Статические свойства, такие как имя хоста, номера портов, конечные точки, версия API, тип устройства, постоянные идентификаторы, размер экрана и т. д., могут быть размещены в одном месте, поэтому одно изменение влияет на все сетевые запросы.
Общие свойства можно повторно использовать между многими сетевыми запросами.
Одноэлементный объект не занимает память до тех пор, пока не будет создан его экземпляр. Это может быть полезно для синглтонов с очень специфическими вариантами использования, которые могут никогда не понадобиться некоторым пользователям, например, обработка трансляции видео на Chromecast, если у них нет устройства.
Сетевые запросы могут быть полностью отделены от представлений и контроллеров, поэтому они могут продолжаться даже после уничтожения представлений и контроллеров.
Сетевое ведение журнала может быть централизовано и упрощено.
Общие события сбоя, такие как оповещения, можно повторно использовать для всех запросов.
Основную структуру такого синглтона можно повторно использовать в нескольких проектах с простыми изменениями статических свойств верхнего уровня.
Некоторые причины не использовать синглтоны:
Ими можно злоупотреблять, чтобы обеспечить несколько обязанностей в одном классе. Например, методы обработки видео можно смешивать с сетевыми методами или методами состояния пользователя. Это, вероятно, было бы плохой практикой проектирования и привело бы к трудному для понимания коду. Вместо этого следует создать несколько синглетонов с конкретными обязанностями.
Синглтоны не могут быть подклассами.
Синглтоны могут скрывать зависимости и, таким образом, становиться менее модульными. Например, если синглтон удален, а классу не хватает импорта, импортированного синглтоном, это может привести к будущим проблемам (особенно если есть зависимости от внешних библиотек).
Класс может изменять общие свойства в одиночках во время длительных операций, которые не ожидаются в другом классе. Без должного обдумывания этого результаты могут отличаться.
Утечки памяти в синглтоне могут стать серьезной проблемой, поскольку сам синглтон никогда не освобождается.
Однако, используя лучшие практики архитектуры приложений для iOS, эти негативные моменты можно смягчить. Вот несколько лучших практик:
Каждый синглтон должен обрабатывать одну ответственность.
Не используйте синглтоны для хранения данных, которые будут быстро изменяться несколькими классами или потоками, если вам нужна высокая точность.
Создавайте синглтоны для включения/отключения функций на основе доступных зависимостей.
Не храните большие объемы данных в одноэлементных свойствах, так как они будут сохраняться в течение всего срока службы вашего приложения (если только они не будут управляться вручную).
Простой пример синглтона с AFNetworking
Во-первых, в качестве предварительного условия добавьте AFNetworking в свой проект. Самый простой способ — через Cocoapods, инструкции можно найти на странице GitHub.
Пока вы этим занимаетесь, я предлагаю добавить UIAlertController+Blocks
и MBProgressHUD
(опять же легко добавляется с помощью CocoaPods). Они явно необязательны, но это значительно упростит прогресс и оповещения, если вы захотите реализовать их в синглтоне в окне AppDelegate.
После добавления AFNetworking
начните с создания нового класса Cocoa Touch с именем NetworkManager
в качестве подкласса NSObject
. Добавьте метод класса для доступа к менеджеру. Ваш файл NetworkManager.h
должен выглядеть так, как показано ниже:
#import <Foundation/Foundation.h> #import “AFNetworking.h” @interface NetworkManager : NSObject + (id)sharedManager; @end
Затем реализуйте основные методы инициализации для синглтона и импортируйте заголовок AFNetworking. Реализация вашего класса должна выглядеть следующим образом (ПРИМЕЧАНИЕ. Предполагается, что вы используете автоматический подсчет ссылок):
#import "NetworkManager.h" @interface NetworkManager() @end @implementation NetworkManager #pragma mark - #pragma mark Constructors static NetworkManager *sharedManager = nil; + (NetworkManager*)sharedManager { static dispatch_once_t once; dispatch_once(&once, ^ { sharedManager = [[NetworkManager alloc] init]; }); return sharedManager; } - (id)init { if ((self = [super init])) { } return self; } @end
Здорово! Теперь мы готовим и готовы добавить свойства и методы. В качестве быстрого теста, чтобы понять, как получить доступ к синглтону, давайте добавим в NetworkManager.h
следующее:
@property NSString *appID; - (void)test;
И следующее для NetworkManager.m
:
#define HOST @”http://www.apitesting.dev/” static const in port = 80; … @implementation NetworkManager … //Set an initial property to init: - (id)init { if ((self = [super init])) { self.appID = @”1”; } return self; } - (void)test { NSLog(@”Testing out the networking singleton for appID: %@, HOST: %@, and PORT: %d”, self.appID, HOST, port); }
Затем в нашем основном файле ViewController.m
(или в том, что у вас есть) импортируйте NetworkManager.h
, а затем в viewDidLoad
добавьте:
[[NetworkManager sharedManager] test];
Запустите приложение, и вы должны увидеть следующее в выводе:
Testing our the networking singleton for appID: 1, HOST: http://www.apitesting.dev/, and PORT: 80
Итак, вы, скорее всего, не будете смешивать #define
, static const и @property
сразу, как это, а просто покажете свои варианты для ясности. «static const» — лучшее объявление для безопасности типов, но #define
может быть полезен при построении строк, поскольку позволяет использовать макросы. Что бы это ни стоило, я использую #define
для краткости в этом сценарии. Если вы не используете указатели, на практике между этими подходами к объявлению нет большой разницы.
Теперь, когда вы понимаете #defines
, константы, свойства и методы, мы можем удалить их и перейти к более подходящим примерам.
Пример сети
Представьте себе приложение, в котором пользователь должен войти в систему, чтобы получить доступ к чему-либо. После запуска приложения мы проверим, сохранили ли мы токен аутентификации, и, если да, выполним запрос GET к нашему API, чтобы узнать, истек ли срок действия токена.

В AppDelegate.m
зарегистрируем значение по умолчанию для нашего токена:
+ (void)initialize { NSDictionary *defaults = [NSDictionary dictionaryWithObjectsAndKeys:@"", @"token", nil]; [[NSUserDefaults standardUserDefaults] registerDefaults:defaults]; }
Мы добавим проверку токена в NetworkManager и получим отзыв о проверке через блоки завершения. Вы можете спроектировать эти блоки завершения так, как вам нравится. В этом примере я использую успех с данными объекта ответа и отказ со строкой ответа об ошибке и кодом состояния. Примечание. Сбой можно не учитывать, если он не имеет значения для принимающей стороны, например, увеличение значения в аналитике.
NetworkManager.h
Выше @interface
:
typedef void (^NetworkManagerSuccess)(id responseObject); typedef void (^NetworkManagerFailure)(NSString *failureReason, NSInteger statusCode);
В @интерфейсе:
@property (неатомарное, сильное) AFHTTPSessionManager *networkingManager;
- (void)tokenCheckWithSuccess:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure;
NetworkManager.m:
Определите наш BASE_URL:
#define ENABLE_SSL 1 #define HOST @"http://www.apitesting.dev/" #define PROTOCOL (ENABLE_SSL ? @"https://" : @"http://") #define PORT @"80" #define BASE_URL [NSString stringWithFormat:@"%@%@:%@", PROTOCOL, HOST, PORT]
Мы добавим несколько вспомогательных методов, чтобы упростить аутентифицированные запросы, а также синтаксический анализ ошибок (в этом примере используется веб-токен JSON):
- (AFHTTPSessionManager*)getNetworkingManagerWithToken:(NSString*)token { if (self.networkingManager == nil) { self.networkingManager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:BASE_URL]]; if (token != nil && [token length] > 0) { NSString *headerToken = [NSString stringWithFormat:@"%@ %@", @"JWT", token]; [self.networkingManager.requestSerializer setValue:headerToken forHTTPHeaderField:@"Authorization"]; // Example - [networkingManager.requestSerializer setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; } self.networkingManager.requestSerializer = [AFJSONRequestSerializer serializer]; self.networkingManager.responseSerializer.acceptableContentTypes = [self.networkingManager.responseSerializer.acceptableContentTypes setByAddingObjectsFromArray:@[@"text/html", @"application/json", @"text/json"]]; self.networkingManager.securityPolicy = [self getSecurityPolicy]; } return self.networkingManager; } - (id)getSecurityPolicy { return [AFSecurityPolicy defaultPolicy]; /* Example - AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone]; [policy setAllowInvalidCertificates:YES]; [policy setValidatesDomainName:NO]; return policy; */ } - (NSString*)getError:(NSError*)error { if (error != nil) { NSData *errorData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; NSDictionary *responseObject = [NSJSONSerialization JSONObjectWithData: errorData options:kNilOptions error:nil]; if (responseObject != nil && [responseObject isKindOfClass:[NSDictionary class]] && [responseObject objectForKey:@"message"] != nil && [[responseObject objectForKey:@"message"] length] > 0) { return [responseObject objectForKey:@"message"]; } } return @"Server Error. Please try again later"; }
Если вы добавили MBProgressHUD, его можно использовать здесь:
#import "MBProgressHUD.h" @interface NetworkManager() @property (nonatomic, strong) MBProgressHUD *progressHUD; @end … - (void)showProgressHUD { [self hideProgressHUD]; self.progressHUD = [MBProgressHUD showHUDAddedTo:[[UIApplication sharedApplication] delegate].window animated:YES]; [self.progressHUD removeFromSuperViewOnHide]; self.progressHUD.bezelView.color = [UIColor colorWithWhite:0.0 alpha:1.0]; self.progressHUD.contentColor = [UIColor whiteColor]; } - (void)hideProgressHUD { if (self.progressHUD != nil) { [self.progressHUD hideAnimated:YES]; [self.progressHUD removeFromSuperview]; self.progressHUD = nil; } }
И наш запрос на проверку токена:
- (void)tokenCheckWithSuccess:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSString *token = [defaults objectForKey:@"token"]; if (token == nil || [token length] == 0) { if (failure != nil) { failure(@"Invalid Token", -1); } return; } [self showProgressHUD]; NSMutableDictionary *params = [NSMutableDictionary dictionary]; [[self getNetworkingManagerWithToken:token] GET:@"/checktoken" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) { [self hideProgressHUD]; if (success != nil) { success(responseObject); } } failure:^(NSURLSessionTask *operation, NSError *error) { [self hideProgressHUD]; NSString *errorMessage = [self getError:error]; if (failure != nil) { failure(errorMessage, ((NSHTTPURLResponse*)operation.response).statusCode); } }]; }
Теперь в методе viewWillAppear ViewController.m мы вызовем этот одноэлементный метод. Обратите внимание на простоту запроса и крошечную реализацию на стороне контроллера представления.
[[NetworkManager sharedManager] tokenCheckWithSuccess:^(id responseObject) { // Allow User Access and load content //[self loadContent]; } failure:^(NSString *failureReason, NSInteger statusCode) { // Logout user if logged in and deny access and show login view //[self showLoginView]; }];
Вот и все! Обратите внимание, как этот фрагмент можно использовать практически в любом приложении, которому необходимо проверять аутентификацию при запуске.
Точно так же мы можем обработать запрос POST для входа в систему: NetworkManager.h:
- (void)authenticateWithEmail:(NSString*)email password:(NSString*)password success:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure;
NetworkManager.m:
- (void)authenticateWithEmail:(NSString*)email password:(NSString*)password success:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure { if (email != nil && [email length] > 0 && password != nil && [password length] > 0) { [self showProgressHUD]; NSMutableDictionary *params = [NSMutableDictionary dictionary]; [params setObject:email forKey:@"email"]; [params setObject:password forKey:@"password"]; [[self getNetworkingManagerWithToken:nil] POST:@"/authenticate" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) { [self hideProgressHUD]; if (success != nil) { success(responseObject); } } failure:^(NSURLSessionTask *operation, NSError *error) { [self hideProgressHUD]; NSString *errorMessage = [self getError:error]; if (failure != nil) { failure(errorMessage, ((NSHTTPURLResponse*)operation.response).statusCode); } }]; } else { if (failure != nil) { failure(@"Email and Password Required", -1); } } }
Здесь мы можем проявить фантазию и добавить оповещения с помощью AlertController+Blocks в окно AppDelegate или просто отправить объекты сбоя обратно в контроллер представления. Кроме того, мы могли бы сохранить учетные данные пользователя здесь или вместо этого позволить контроллеру представления обрабатывать это. Обычно я реализую отдельный синглтон UserManager, который обрабатывает учетные данные и разрешения, которые могут напрямую взаимодействовать с NetworkManager (личное предпочтение).
Еще раз, сторона контроллера представления очень проста:
- (void)loginUser { NSString *email = @"[email protected]"; NSString *password = @"SomeSillyEasyPassword555"; [[NetworkManager sharedManager] authenticateWithEmail:email password:password success:^(id responseObject) { // Save User Credentials and show content } failure:^(NSString *failureReason, NSInteger statusCode) { // Explain to user why authentication failed }]; }
Ой! Мы забыли указать версию API и отправить тип устройства. Кроме того, мы обновили конечную точку с «/checktoken» на «/token». Поскольку мы централизовали нашу сеть, ее очень легко обновить. Нам не нужно копаться в нашем коде. Поскольку мы будем использовать эти параметры во всех запросах, мы создадим помощник.
#define API_VERSION @"1.0" #define DEVICE_TYPE UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ? @"tablet" : @"phone" - (NSMutableDictionary*)getBaseParams { NSMutableDictionary *baseParams = [NSMutableDictionary dictionary]; [baseParams setObject:@"version" forKey:API_VERSION]; [baseParams setObject:@"device_type" forKey:DEVICE_TYPE]; return baseParams; }
В будущем к этому можно легко добавить любое количество общих параметров. Затем мы можем обновить наши методы проверки токена и аутентификации следующим образом:
… NSMutableDictionary *params = [self getBaseParams]; [[self getNetworkingManagerWithToken:token] GET:@"/checktoken" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) { … … NSMutableDictionary *params = [self getBaseParams]; [params setObject:email forKey:@"email"]; [params setObject:password forKey:@"password"]; [[self getNetworkingManagerWithToken:nil] POST:@"/authenticate" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) {
Завершение нашего руководства по AFNetworking
На этом мы остановимся, но, как видите, мы централизовали общие сетевые параметры и методы в единственном менеджере, что значительно упростило реализацию нашего контроллера представления. Будущие обновления будут простыми и быстрыми, и, что наиболее важно, они отделят нашу сеть от взаимодействия с пользователем. В следующий раз, когда команда дизайнеров попросит о капитальном ремонте UI/UX, мы будем знать, что наша работа уже сделана на стороне сети!
В этой статье мы сосредоточились на сетевом синглтоне, но те же принципы можно применить и ко многим другим централизованным функциям, таким как:
- Обработка состояния пользователя и разрешений
- Маршрутизация сенсорных действий для навигации по приложению
- Управление видео и аудио
- Аналитика
- Уведомления
- Периферия
- и многое многое другое…
Мы также сосредоточились на архитектуре приложения для iOS, но ее можно было бы легко распространить на Android и даже на JavaScript. В качестве бонуса, создавая строго определенный и функционально-ориентированный код, он значительно ускоряет перенос приложений на новые платформы.
Подводя итог, можно сказать, что если вы потратите немного больше времени на раннем этапе планирования проекта, чтобы установить ключевые одноэлементные методы, такие как приведенный выше сетевой пример, ваш будущий код может быть чище, проще и удобнее в сопровождении.