Привет, Хабр. Меня зовут Антон Логинов, я iOS-разработчик в компании FINCH.
Недавно мы столкнулись с проблемой использования web-интерфейсов для азартных игр. В очередном обновлении AppStore Review Guidelines коллеги из Купертино опять ужесточили правила. Если конкретнее, то теперь Apple может зареджектить приложение, если какой-либо из web-интерфейсов будет классифицирован как азартная игра на реальные деньги.
Одно наше приложение на 90% состоит из азартных игр, а оставшиеся 10% используются, чтобы эти игры рекламировать. Часть из них работает через webView, поэтому нам нужно было любым образом обезопасить себя от реджекта.
Что можно было сделать:
Я не нашел ни одной статьи на Хабре, где описывалось бы как запустить сервер на телефоне. Решив, что кейс достаточно редкий и интересный, решил рассказать об этом здесь, на Хабре.
Пока наш бравый фронтмен пытался уменьшить размер игр в 16 раз (80 Mb -> 5 Mb) и менял внутренние пути на относительные, я определился с библиотекой, выбрав GCDWebServer. Это легковесный фреймворк, с помощью которого можно поднять HTTP-сервер в несколько строк кода.
После выбора библиотеки наступили долгие часы изучения и понимания того, как работает сервер под капотом, что в какой момент времени происходит, как настроить сервер так, чтобы он не тратил ресурсы системы впустую. Наш сервер учился ловить переходы, обрабатывать их, а я учился работать с сервером с другой стороны баррикад.
Собственно прописываем параметры для старта нашего сервера и запускаем:
Модуль внутри себя общается с API, но использует для этого свой собственный baseURL. В нашем случае — localhost. Следовательно, нужно было научить сервер определять те запросы, которые должны идти к API, и менять их baseURL.
Исходя из вышенаписанного, необходимо было сконфигурировать хендлеры для конкретных задач:
Let's do it:
Было весело и нервно. Во-первых, я никогда ничего подобного до этого не делал. Во-вторых, мы до последнего не понимали, как наши задумки повлияют на родительское приложение.
На реализацию у нас ушло около 32 часов: 8 на оптимизацию размера сайта, 24 на проектирование и написание данного функционала.
Пока писал статью, я пришел к выводу, что более популярным способом использования этой технологии является разработка нативки без ожидания готовности бэкенда.
Ну и подытожим плюсами выбранного подхода:
Спасибо за внимание. Если у вас был подобный опыт, то расскажите о нем в комментариях.
Недавно мы столкнулись с проблемой использования web-интерфейсов для азартных игр. В очередном обновлении AppStore Review Guidelines коллеги из Купертино опять ужесточили правила. Если конкретнее, то теперь Apple может зареджектить приложение, если какой-либо из web-интерфейсов будет классифицирован как азартная игра на реальные деньги.
Одно наше приложение на 90% состоит из азартных игр, а оставшиеся 10% используются, чтобы эти игры рекламировать. Часть из них работает через webView, поэтому нам нужно было любым образом обезопасить себя от реджекта.
Что можно было сделать:
- Вынести эти игры за пределы основного приложения.
Если сказать другими словами — просто отсрочить неизбежное. - Использовать некий контейнер для игр, который можно будет безболезненно обновлять.
Звучит неплохо → попытался вникнуть → убил несколько дней на изучение React и React Native → понял, что «лыжи не едут» → звучит уже не так хорошо.
Это очень дорогое решение, поскольку времени было мало, а игры пришлось бы переписывать с нуля. Все дело во внутреннем роутинге — он целиком завязан на urlPathComponents
- Реализовать игры нативно.
Долго, дорого, и еще раз долго. В дальнейшем пришлось бы их поддерживать на постоянной основе, а у нас таких возможностей не было. - Имитировать поведение сервера, который бы отдавал лежащий локально сайт с играми.
Звучит безумно, но именно этот вариант я выбрал. Это быстро, так как требуются минимальные доработки легаси игр.
К оценочным минусам можно отнести: увеличение размера сборки за счет лежащего локально сайта, увеличение нагрузки на устройство за счет запуска сервера.
Я не нашел ни одной статьи на Хабре, где описывалось бы как запустить сервер на телефоне. Решив, что кейс достаточно редкий и интересный, решил рассказать об этом здесь, на Хабре.
Подготовка
Пока наш бравый фронтмен пытался уменьшить размер игр в 16 раз (80 Mb -> 5 Mb) и менял внутренние пути на относительные, я определился с библиотекой, выбрав GCDWebServer. Это легковесный фреймворк, с помощью которого можно поднять HTTP-сервер в несколько строк кода.
После выбора библиотеки наступили долгие часы изучения и понимания того, как работает сервер под капотом, что в какой момент времени происходит, как настроить сервер так, чтобы он не тратил ресурсы системы впустую. Наш сервер учился ловить переходы, обрабатывать их, а я учился работать с сервером с другой стороны баррикад.
Настройка
func initWebServer() {
// Инициализация
let webServer = GCDWebServer()
// В таких хендлерах обрабатываются обращения к серверу, например, этот будет обрабатывать все GET/POST запросы:
webServer.addDefaultHandler(
forMethod: HTTPMethod.get.rawValue,
request: GCDWebServerDataRequest.self) { [weak self] request in
return self?.handle(request: request)
}
}
Старт
Собственно прописываем параметры для старта нашего сервера и запускаем:
do {
try webServer.start(options: [GCDWebServerOption_BindToLocalhost: true,
GCDWebServerOption_Port: 8080])
} catch {
assertionFailure(error.localizedDescription)
webServer.start(withPort: 8080, bonjourName: "PROJECT_NAME Web Server")
}
Прокси
Модуль внутри себя общается с API, но использует для этого свой собственный baseURL. В нашем случае — localhost. Следовательно, нужно было научить сервер определять те запросы, которые должны идти к API, и менять их baseURL.
// MARK: - в это время я узнал, насколько полезной может быть панель разработчика в Safari
Исходя из вышенаписанного, необходимо было сконфигурировать хендлеры для конкретных задач:
- Отдать сайт. (Ну тут все просто);
- Отдать какую-нибудь статику из Bundle. (Разобрали url запроса, поменяли baseUrl на bundleUrl, отдали контент (js/медиа);
- Получить актуальные данные. (Разобрали url, поменяли baseUrl, запросили, отдали);
- Отправить новые данные. (А POST запросы-то мы не обрабатывали, прикрутили, настроили, отправили);
Let's do it:
private func handle(request: GCDWebServerRequest) -> GCDWebServerResponse? {
// 1) Отдаем сайт
if request.url.pathComponents.contains(Endpoint.game.rawValue) {
guard let indexURL = bundle.url(forResource: "index", withExtension: "html") else {
return sendError(.noHTML(nil))
}
do {
let data = try Data(contentsOf: indexURL)
let htmlString = String(data: data, encoding: .utf8) ?? ""
return GCDWebServerDataResponse(html: htmlString)
} catch {
return sendError(.noHTML(error))
}
// 2) Отдаем статику (js etc)
} else if request.url.pathComponents.contains(Endpoint.nstatic.rawValue) {
guard let resoursePath = bundle.resourcePath else {
return sendError(.noJS(nil))
}
let relativePath = request.url.pathComponents.joined(separator: "/")
let absolutePath = resoursePath + relativePath.dropFirst()
let staticURL = URL(fileURLWithPath: absolutePath)
do {
let data = try Data(contentsOf: staticURL)
return GCDWebServerDataResponse(data: data, contentType: ContentType.js.description)
} catch {
return sendError(.noJS(error))
}
// 3) Делаем редирект для API
} else if request.url.pathComponents.contains(Endpoint.api.rawValue) {
var proxyRequest = request
// Меняем url, дописываем хедеры, тело, и отправляем этот запрос
let output = URLSession.shared.synchronousDataTask(with: proxyRequest)
// Инициализируем ответ от сервера, используя данные полученные из запроса
let response = GCDWebServerDataResponse(data: outputData, contentType: ContentType.url.description)
// и возвращаем его пользователю
return response
}
}
Заключение
Было весело и нервно. Во-первых, я никогда ничего подобного до этого не делал. Во-вторых, мы до последнего не понимали, как наши задумки повлияют на родительское приложение.
На реализацию у нас ушло около 32 часов: 8 на оптимизацию размера сайта, 24 на проектирование и написание данного функционала.
Пока писал статью, я пришел к выводу, что более популярным способом использования этой технологии является разработка нативки без ожидания готовности бэкенда.
Ну и подытожим плюсами выбранного подхода:
- Экономия времени бэкенда за счет того, что сами даем ему модель данных
- Возможность протестировать любое поведение сервера
- Для перехода с Mock на реальное API нам просто нужно выключить конкретные хендлеры
Спасибо за внимание. Если у вас был подобный опыт, то расскажите о нем в комментариях.