Облегчаем поддержку iOS приложения. Часть 2 — локация и сеть

в 23:55, , рубрики: iOS, xcode, отладка, разработка под iOS

Добрый день, хаброжители,

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

  1. Облегчаем поддержку iOS приложения. Часть 1 — не отрываясь от Xcode
  2. Облегчаем поддержку iOS приложения. Часть 2 — локация и сеть
  3. Облегчаем поддержку iOS приложения. Часть 3 — падение и логи

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

Внимательный читатель первой статьи мог догадаться, что долгое время я проработал в аутсорсе(не надо ставить на мне крест). И поэтому мои статьи описывают ситуации, где есть заказчик, который далеко от вас.

Подвид второй — невероятные условия локации

«Вот у меня тут, в Париже, данные не так отображаются»

Бывает, что многое в приложении завязано на локацию, и у вас все отлично, а вот у клиента данные не так отображаются и он в Париже… ну хорошо ему, что еще могу сказать. А я в Сибири, и у меня все ОК с данными, каждому свое. Что же делать? Симулировать локацию. И так, рассмотрим наши варианты:
1) Это простая подмена локации в симуляторе. Как нечего делать — он поставит вас в нужную точку. «iOS Simulator»→Debug→Location.
2) Если точки мало, можно начать симулировать путь, есть файлы pgx, добавляются в xcode через правый значок
Облегчаем поддержку iOS приложения. Часть 2 — локация и сеть - 1
И локации будут перемещать вас по пути, но… Если мне не изменяет память, это можно делать, только присоединив приложение к Xcode, и нельзя настроить время между прыжками. Что делать тестировщикам? Сидеть у маков? Хорошая компания, если всем купили MAC. И всегда подключив приложение Xcode? А если нужны задержки большие, больше, чем сделает Xcode?
3) Вот тут на помощь неожиданно приходят волшебные методы runtime, о них много написано, к примеру тут, но вот реально зачем оно такое может пригодиться… бывает не совсем понятно. Однако, вот вам вариант, который поможет нам решить проблему с подменой локации или даже навигации. FakeGPSUtility строго не судить, несколько раз он пригодился и сыграл важную роль, но это тот еще велосипед, и тут он больше чтобы не быть голословным и показать вам направление. Свою цель этот проект выполняет — вы можете подменять локацию и прыжки межу ними не фиксированы. Это может делать кто угодно, даже клиент, не нужен Xcode и главное для меня (для кого-то вторичное) мы не меняем ничего в коде проекта, все подменяется в момент запуска приложения. То есть, вы присоединяете его к проекту, код проекта остается прежний, фиксаете баг/тестируете и удаляете, или выставляете нужные #define. Вуаля, полезная тулза под рукой, и никогда этот код не попадет в Release, исключительно для тестов.
С iOS 8, карты начали получать локацию (реальную локацию девайса), даже при симуляции, так что если дойдут руки — допилю тулзу.
А пока она может вот такое сделать с картой. Красная булавка это то, что прилетает в методе делегата NSLocationManager, остальное — нативное поведение карты. Симуляция шла — карта велась.
Облегчаем поддержку iOS приложения. Часть 2 — локация и сеть - 2

Подвид третий — проблемы с http request

— При edge соединении приложение плохо работает
— Именно поэтому я и тестирую его на WiFi, зачем же себя мучить-то!
Рассмотрим еще один случай — проблема проявилась из-за плохой сети. Вообще, полезно просто симулировать плохое соединение с интернетом и посмотреть, как работает приложение, потому что, пока вы на WiFi, а может и сервис на localhost, не поймешь как оно ведет себя на 3g (а, не дай Бог, еще и на edge). Так что, ради интереса, идем на устройстве в SettingsDeveloperNetwork Link ConditionerEnable и выбираем, насколько плохое соединение мы хотим. После этого вы можете резко поменять точку зрения о быстродействии приложения.
И так, вы можете сделать интернет медленнее, но что если вам надо сломать какой-то один определенный response, чтобы получилась та же проблема, что и у клиента/тестировщика? Тогда придется делать еще один велосипед (дзынь-дзынь).
Как и с FakeGPSUtility, я против допиливать что-то внутри наших рабочих классов для тестирования и баговоспроизведения, код добавляется исключительно вокруг проекта. NSURProtocol нам в помощь. Как по мне, 3 наиболее вероятных места, где может понадобится сломать request — это NSURLSessionTask, UIWebView и WKWebView. Есть и другие соединения по сети, но тут я вам не помогу — ничего хорошего не посоветую, только сидеть и портить код проекта ради тестов.

NSURLSessionTask

Чтобы сломать его response, надо, чтобы наш протокол попал во все NSURLSessionConfiguration, для этого переопределим

- (NSArray *)protocolClasses

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

шутка, вот код

static char kAssociatedObjectKey;

- (NSArray *)protocolClasses
{
  NSMutableArray *result = objc_getAssociatedObject(self, &kAssociatedObjectKey);
  if (result == nil) {
    result = [self customeProtocolsArray];
    objc_setAssociatedObject(self, &kAssociatedObjectKey, result, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  }

  return result;
}

- (void)setProtocolClasses:(NSArray *)protocolClasses
{

  NSMutableArray *result = [self customeProtocolsArray];
  if (protocolClasses.count > 0) {
    [result addObjectsFromArray:protocolClasses];
  }

  objc_setAssociatedObject(self, &kAssociatedObjectKey, result, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSMutableArray *)customeProtocolsArray
{
  return [NSMutableArray arrayWithObject:[MyURLProtocol class]];
}

Но, для bacgkound session это не сработает. пруф

UIWebView

Тут все еще проще — просто зарегистрируйте класс. Например так:

- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
{
    [NSURLProtocol registerClass:[MyURLProtocol class]];
    return YES;
}
WKWebView

А вот тут все плохо, не получится подменять извне, придется все-таки втыкать костыли для этого в код проекта.

А вот, собственно, и сам протокол

@implementation MyURLProtocol

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
#if !defined(STAGE_SERVER) || !STAGE_SERVER
  return NO;
#endif
  NSString *urlString = [[request URL] absoluteString];

  NSRange randge = [urlString rangeOfString:@"http://yandex.ru"];
  if (randge.location == 0) {
    return YES;
  }

  return NO;
}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
  return request;
}

+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
{
  return NO;
}

- (void)startLoading
{
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSError *error = [NSError errorWithDomain:@"MyErrorDomain"
                                         code:42
                                     userInfo:@{ NSLocalizedDescriptionKey: @"We fail it!"}];
    [self.client URLProtocol:self didFailWithError:error];
  });
}

- (void)stopLoading
{
  // stop request if you can
}
@end

Подвид четвертый — для тестов нужен специфический/устаревший response

Нам сообщают, что вчера было воспроизведено некорректное поведение, а сегодня уже не получается, от сервиса не выходит получить тот же ответ. Сказать, что теперь все работает, неправильно, так как по закону жанра эта ситуация повторится в первый же день релиза. И повлиять на server-side team вы не можете по целому ряду причин: они в другом городе, вы их не знаете, они в отпуске.
В этой ситуации NSURLProtocol снова спешит вам на помощь. Нам надо лишь поменять + (BOOL)canInitWithRequest:(NSURLRequest *)request, чтобы при нужном запросе мы возвращали нужные данные.
Вот так может выглядеть наш MyURLProtocol, чтобы просимулировать нужный response

MyURLProtocol

@implementation MyURLProtocol

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
  NSString *urlString = [[request URL] absoluteString];

  NSRange randge = [urlString rangeOfString:@"http://localhost:1984/oldresponse"];
  if (randge.location == 0) {
    return YES;
  }

  return NO;
}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
  return request;
}

+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
{
  return NO;
}

- (void)startLoading
{
  NSString *path = [[NSBundle mainBundle] pathForResource:@"unexistd_response" ofType:@"json"];
  NSData *data = [NSData dataWithContentsOfFile:path];
  [self.client URLProtocol:self didLoadData:data];

  [self.client URLProtocol:self didFailWithError:nil];
}

- (void)stopLoading
{
  // stop request if you can
}
@end

Тут мы с файловой системы читаем unexistd_response.json, но никто не запрещает сделать редирект на рабочую машину, поднять на ней небольшой сервис и с него получить ответ. Это отличный вариант, если есть время и не хочется складывать странные файлы в проект, даже если они только на время и для тестов.
Лично я часто создаю NSURLProtocol для работы на первых порах над проектом, потому что зачастую сервис есть только в планах и ТЗ, а реально у вас его нет. Но уже хочется писать, как будто приложение работает как надо, получает данные, и, в зависимости от них, показывает содержимое пользователю. Я делаю себе #define STAGE_SERVER 1 и, пока мне не дадут рабочий сервис, я работаю с локальными файлами. Они же меня потом спасают, когда разработчик, уходя домой, выключит компьютер, который по совместительству был developer server (до боли смешная ситуация, которая повторялась несколько раз).

Заключение

Сегодня мы рассмотрели случаи, когда проблемы очень тесно связаны с http запросами или локациями. NSURLProtocol и FakeGPSUtility герои сегодняшней статьи, они очень сильно упростят тестирование и воспроизведение некорректного поведения приложения.

Автор: NikolayJuly

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js