iOS WKWebView: Особенности и ограничения
iOS WebView — это более предсказуемая, но более ограничительная среда по сравнению с Android. Здесь мы разберём все нюансы работы с WKWebView.
1 UIWebView vs WKWebView: Эволюция
Исторический контекст: Apple использовала два разных WebView-компонента за свою историю. Понимание различий между ними критически важно, так как многие баги и ограничения связаны с переходом между ними.
- Первый WebView-компонент Apple
- Работал в том же процессе, что и приложение
- Медленный JavaScript (SquirrelFish)
- Утечки памяти при длительной работе
- Ограниченная безопасность
- Запрещён в App Store с 2020 года
- Современный WebView-компонент
- Отдельный процесс (Nitro JavaScript engine)
- Быстрый JavaScript (до 3x быстрее)
- Улучшенная безопасность
- Лучшая производительность памяти
- Единственный разрешённый в App Store
Ключевые различия:
| Критерий | UIWebView | WKWebView |
|---|---|---|
| Архитектура | Один процесс с приложением | Отдельный процесс (out-of-process) |
| JavaScript Engine | SquirrelFish (медленный) | Nitro (быстрый, JIT-компиляция) |
| Производительность | Низкая | Высокая (до 3x быстрее) |
| Память | Утечки при длительной работе | Лучшее управление памятью |
| Безопасность | Ограниченная | Строгая (Content Security Policy) |
| Доступ к файлам | Прямой доступ | Только через loadFileURL |
| Cookies | Общие с Safari | Изолированные (с iOS 11+) |
| Статус в App Store | ❌ Запрещён (с 2020) | ✅ Разрешён |
Если вы нашли баг в приложении, которое использует UIWebView — не тратьте время на его локализацию. Приложение должно быть обновлено до WKWebView. Это критическое требование App Store с декабря 2020 года.
Проверить, какой WebView используется:
- В коде приложения: поиск
UIWebViewvsWKWebView - В User-Agent: UIWebView не содержит
AppleWebKit - Через Instruments: UIWebView работает в том же процессе
import WebKit
class WebViewController: UIViewController {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
// Создаём конфигурацию
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
config.mediaTypesRequiringUserActionForPlayback = []
// Настройки предпочтений
config.preferences.javaScriptEnabled = true
config.preferences.javaScriptCanOpenWindowsAutomatically = true
// Создаём WebView
webView = WKWebView(frame: view.bounds, configuration: config)
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
webView.navigationDelegate = self
webView.uiDelegate = self
view.addSubview(webView)
// Загружаем URL
if let url = URL(string: "https://example.com") {
webView.load(URLRequest(url: url))
}
}
}
// Делегаты для обработки событий
extension WebViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("Страница загружена")
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
print("Ошибка загрузки: \(error.localizedDescription)")
}
}
extension WebViewController: WKUIDelegate {
// Обработка JavaScript alert/confirm/prompt
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
completionHandler()
})
present(alert, animated: true)
}
}
2 Архитектура WKWebView: Отдельный процесс
Ключевая особенность WKWebView — это работа в отдельном процессе (out-of-process). Это фундаментальное отличие от UIWebView и Android WebView.
Что это значит на практике:
- Изоляция: Краш WKWebView не всегда убивает всё приложение (но может).
- Безопасность: JavaScript не имеет прямого доступа к памяти приложения.
- Производительность: JavaScript работает быстрее благодаря JIT-компиляции в отдельном процессе.
- Ограничения памяти: WKWebView имеет свой лимит памяти, независимый от приложения.
- Сложность отладки: Нужно подключаться к двум процессам одновременно.
Как это влияет на тестирование:
- Краши: Если WKWebView крашится, вы увидите два краш-репорта — один для приложения, один для WebKit.
- Утечки памяти: Утечка в JavaScript может не повлиять на приложение, но убить WKWebView.
- Производительность: Измеряйте производительность отдельно для приложения и WKWebView.
- Восстановление: После краша WKWebView приложение должно корректно восстановить состояние.
extension WebViewController: WKNavigationDelegate {
// Обработка краша веб-контента
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
print("WKWebView крашнулся!")
// Перезагружаем страницу
webView.reload()
// Или показываем ошибку пользователю
showErrorAlert(message: "Произошла ошибка. Попробуйте обновить страницу.")
}
// Обработка краша процесса
func webView(_ webView: WKWebView, didTerminateWithError error: Error) {
print("Ошибка процесса WKWebView: \(error.localizedDescription)")
// Логируем ошибку и уведомляем аналитику
}
}
Если ваше приложение «падает» при открытии определённой страницы — это может быть краш WKWebView, а не приложения. В Xcode вы увидите два отдельных процесса. Всегда проверяйте, какой именно процесс крашнулся.
3 Ограничения WKWebView
WKWebView имеет ряд строгих ограничений, которых нет в Android WebView. Знание этих ограничений поможет вам понять, почему некоторые фичи «не работают».
Ограничение 1: Лимит памяти
WKWebView крашится без ошибки при загрузке больших страниц или изображений.
Особенно заметно на устройствах с малым объёмом RAM (iPhone SE, старые iPad).
Лимиты памяти (примерные):
- iPhone с 2 ГБ RAM: ~1.2 ГБ для WKWebView
- iPhone с 3 ГБ RAM: ~1.8 ГБ для WKWebView
- iPhone с 4+ ГБ RAM: ~2.5 ГБ для WKWebView
- iPad: зависит от модели
// 1. Оптимизируйте изображения
// Используйте WebP/AVIF вместо PNG/JPEG
// Сжимайте изображения до разумных размеров
// 2. Ленивая загрузка (Lazy Loading)
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy">
// 3. Виртуализация списков
// Используйте библиотеки типа react-window или vue-virtual-scroller
// 4. Мониторинг памяти в нативном коде
func checkMemoryUsage() {
var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
}
}
if kerr == KERN_SUCCESS {
let memoryMB = Double(info.resident_size) / 1024.0 / 1024.0
print("Использование памяти: \(memoryMB) MB")
if memoryMB > 1000 { // Предупреждение при 1 ГБ
print("⚠️ Высокое потребление памяти!")
}
}
}
Ограничение 2: Доступ к локальным файлам
Нельзя загрузить локальный HTML-файл напрямую через file:// URL.
WKWebView блокирует такие запросы из соображений безопасности.
// ❌ Плохо: не работает в WKWebView
let url = URL(fileURLWithPath: Bundle.main.path(forResource: "index", ofType: "html")!)
webView.load(URLRequest(url: url))
// ✅ Хорошо: используем loadFileURL
let url = Bundle.main.url(forResource: "index", withExtension: "html")!
let directoryURL = url.deletingLastPathComponent()
webView.loadFileURL(url, allowingReadAccessTo: directoryURL)
// ✅ Альтернатива: загрузка через data: URL
let htmlString = "<html><body>Hello</body></html>"
webView.loadHTMLString(htmlString, baseURL: nil)
Ограничение 3: Cookies и ITP (Intelligent Tracking Prevention)
Cookies не сохраняются между сессиями.
Cookies из Safari не доступны в WKWebView (с iOS 11+).
Трacking cookies блокируются автоматически.
// Настройка хранилища cookies
let config = WKWebViewConfiguration()
config.websiteDataStore = WKWebsiteDataStore.default()
// Ручное управление cookies
let httpCookieStore = config.websiteDataStore.httpCookieStore
// Установка cookie
let cookie = HTTPCookie(properties: [
.name: "session_id",
.value: "abc123",
.domain: "example.com",
.path: "/",
.expires: Date().addingTimeInterval(86400) // 1 день
])!
httpCookieStore.setCookie(cookie) {
print("Cookie установлен")
}
// Получение всех cookies
httpCookieStore.getAllCookies { cookies in
for cookie in cookies {
print("\(cookie.name): \(cookie.value)")
}
}
// Очистка cookies
let dataTypes = WKWebsiteDataStore.allWebsiteDataTypes()
WKWebsiteDataStore.default().removeData(ofTypes: dataTypes, modifiedSince: Date.distantPast) {
print("Все данные очищены")
}
Ограничение 4: Автовоспроизведение видео и аудио
Видео и аудио не воспроизводятся автоматически без взаимодействия пользователя.
Это ограничение iOS для экономии батареи и трафика.
// В нативном коде (Swift)
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
config.mediaTypesRequiringUserActionForPlayback = [] // Разрешаем автовоспроизведение
// В HTML
<video autoplay muted playsinline>
<source src="video.mp4" type="video/mp4">
</video>
// Важно: видео должно быть muted для автовоспроизведения!
// Или пользователь должен взаимодействовать со страницей (клик, тап)
Ограничение 5: JavaScript alert/confirm/prompt
JavaScript alert(), confirm(), prompt() не работают по умолчанию.
Нужно реализовать их нативно через WKUIDelegate.
extension WebViewController: WKUIDelegate {
// JavaScript alert
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
completionHandler()
})
present(alert, animated: true)
}
// JavaScript confirm
func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
completionHandler(false)
})
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
completionHandler(true)
})
present(alert, animated: true)
}
// JavaScript prompt
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
let alert = UIAlertController(title: nil, message: prompt, preferredStyle: .alert)
alert.addTextField { textField in
textField.text = defaultText
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
completionHandler(nil)
})
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
completionHandler(alert.textFields?.first?.text)
})
present(alert, animated: true)
}
}
Если JavaScript alert не появляется — это не баг, это ограничение WKWebView. Разработчики должны реализовать нативные алерты через WKUIDelegate. Если они этого не сделали — заведите баг на разработчиков.
4 SFSafariViewController: Альтернатива WKWebView
SFSafariViewController — это альтернатива WKWebView от Apple. Это «облегчённая» версия Safari, которая интегрируется в ваше приложение, но использует полноценный движок Safari.
Когда использовать SFSafariViewController:
- Открытие внешних ссылок: Когда нужно открыть сайт, не связанный с вашим приложением.
- Авторизация через OAuth: Apple рекомендует использовать SFSafariViewController для OAuth.
- Контент с cookies Safari: SFSafariViewController имеет доступ к cookies пользователя из Safari.
- Простой просмотр: Когда не нужна кастомизация и JS Bridge.
Преимущества SFSafariViewController:
- Общие cookies: Пользователь уже авторизован в сервисах Apple.
- Знакомый UX: Пользователь видит привычный интерфейс Safari.
- Безопасность: Автоматические обновления через iOS.
- Все фичи Safari: Reader Mode, AutoFill, iCloud Keychain.
- Простота: Не нужно настраивать WKWebView.
Недостатки SFSafariViewController:
- Нет JS Bridge: Нельзя вызывать нативные методы из JavaScript.
- Ограниченный контроль: Нельзя кастомизировать UI (только цвет тулбара).
- Нет доступа к нативным API: Камера, геолокация — только через веб-API.
- Только iOS 9+: Не работает на старых устройствах.
import SafariServices
class ViewController: UIViewController {
func openURL() {
guard let url = URL(string: "https://example.com") else { return }
let safariVC = SFSafariViewController(url: url)
// Настройка цвета тулбара
safariVC.preferredBarTintColor = .systemBlue
safariVC.preferredControlTintColor = .white
// Показывать кнопку "Готово"
safariVC.dismissButtonStyle = .done
present(safariVC, animated: true)
}
}
// Делегат для обработки событий
extension ViewController: SFSafariViewControllerDelegate {
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
print("Safari закрыт")
}
func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) {
if didLoadSuccessfully {
print("Страница загружена")
} else {
print("Ошибка загрузки")
}
}
}
| Критерий | WKWebView | SFSafariViewController |
|---|---|---|
| Контроль над UI | ✅ Полный | ⚠️ Ограниченный (только цвет) |
| JS Bridge | ✅ Поддерживается | ❌ Не поддерживается |
| Cookies Safari | ❌ Изолированы (с iOS 11+) | ✅ Общие с Safari |
| Reader Mode | ❌ Нет | ✅ Есть |
| AutoFill | ⚠️ Ограниченный | ✅ Полный |
| iCloud Keychain | ❌ Нет | ✅ Есть |
| Кастомизация JavaScript | ✅ Полная | ❌ Нет |
| Доступ к нативным API | ✅ Через JS Bridge | ❌ Только веб-API |
Используйте WKWebView, если:
- Нужен JS Bridge для связи с нативным кодом
- Нужна полная кастомизация UI
- Нужен доступ к нативным API (камера, геолокация)
- Это часть вашего приложения (не внешняя ссылка)
Используйте SFSafariViewController, если:
- Нужно открыть внешнюю ссылку
- Нужна авторизация через OAuth
- Нужны cookies из Safari
- Не нужна кастомизация
5 Специфичные баги iOS WKWebView
iOS WKWebView имеет ряд специфических багов, которые не встречаются в Android WebView. Знание этих багов поможет вам быстрее локализовать проблемы.
Баг 1: Проблема с 100vh
На iOS 100vh включает адресную строку Safari, что приводит к обрезке контента.
Особенно заметно при использовании position: fixed или height: 100vh.
/* ❌ Плохо: на iOS контент обрежется */
.fullscreen {
height: 100vh;
}
/* ✅ Хорошо: используем dvh (dynamic viewport height) */
.fullscreen {
height: 100dvh; /* iOS 15.4+ */
}
/* ✅ Альтернатива: JS-решение для старых iOS */
.fullscreen {
height: calc(var(--vh, 1vh) * 100);
}
<script>
// Устанавливаем CSS-переменную
function setVH() {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
setVH();
window.addEventListener('resize', setVH);
window.addEventListener('orientationchange', setVH);
</script>
Баг 2: position: fixed и клавиатура
При открытии клавиатуры фиксированные элементы (footer, кнопки) могут "уезжать" или скрываться.
Особенно заметно на iPhone с физической кнопкой Home.
/* Используем flexbox вместо fixed */
.container {
display: flex;
flex-direction: column;
height: 100vh;
height: 100dvh;
}
.content {
flex: 1;
overflow-y: auto;
}
.footer {
flex-shrink: 0; /* Не сжимается */
}
/* Или используем sticky position */
.footer {
position: -webkit-sticky;
position: sticky;
bottom: 0;
}
Баг 3: Проблема с input type="file"
На iOS input type="file" открывает стандартный file picker, но:
- Нельзя выбрать несколько файлов (без accept="multiple")
- Ограниченный доступ к файловой системе
- Нет доступа к Camera Roll без разрешения
<!-- Разрешаем выбор нескольких файлов -->
<input type="file" accept="image/*" multiple>
<!-- Разрешаем доступ к камере -->
<input type="file" accept="image/*" capture="camera">
<!-- В нативном коде (Swift) нужно обработать UIDelegate -->
func webView(_ webView: WKWebView, runOpenPanelWith parameters: WKOpenPanelParameters, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping ([URL]?) -> Void) {
let picker = UIImagePickerController()
picker.delegate = self
picker.sourceType = .photoLibrary
picker.allowsEditing = false
// Сохраняем completionHandler для später использования
self.filePickerCompletionHandler = completionHandler
present(picker, animated: true)
}
// В UIImagePickerControllerDelegate
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let url = info[.imageURL] as? URL {
filePickerCompletionHandler?([url])
}
picker.dismiss(animated: true)
}
Баг 4: Проблема с overflow: scroll и инерцией
На iOS скролл внутри контейнера может быть "дёрганым" или "застревать".
Особенно заметно при использовании overflow: auto без -webkit-overflow-scrolling.
/* ✅ Хорошо: плавная прокрутка с инерцией */
.scrollable {
overflow-y: auto;
-webkit-overflow-scrolling: touch; /* Включает инерцию */
}
/* Для iOS 13+ можно использовать scroll-behavior */
.scrollable {
scroll-behavior: smooth;
}
/* Избегайте вложенных скроллов */
/* ❌ Плохо: скролл внутри скролла */
.outer {
overflow-y: auto;
}
.inner {
overflow-y: auto;
}
/* ✅ Хорошо: один уровень скролла */
.outer {
overflow-y: auto;
}
.inner {
overflow-y: visible;
}
Баг 5: Проблема с autoplay для видео
Видео не воспроизводится автоматически без взаимодействия пользователя.
Это ограничение iOS для экономии батареи и трафика.
<!-- Видео должно быть muted для автовоспроизведения -->
<video autoplay muted playsinline>
<source src="video.mp4" type="video/mp4">
</video>
<!-- В нативном коде (Swift) -->
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
config.mediaTypesRequiringUserActionForPlayback = [] // Разрешаем автовоспроизведение
// Или используйте JavaScript для запуска после взаимодействия
<button onclick="playVideo()">Play</button>
<video id="myVideo">
<source src="video.mp4" type="video/mp4">
</video>
<script>
function playVideo() {
const video = document.getElementById('myVideo');
video.play();
}
</script>
Баг 6: Проблема с transform и fixed элементами
Элементы с position: fixed и transform: translateZ(0) могут "прыгать" при скролле.
Особенно заметно на старых версиях iOS (10-12).
/* ❌ Плохо: может прыгать */
.fixed-element {
position: fixed;
transform: translateZ(0);
}
/* ✅ Хорошо: используем sticky */
.sticky-element {
position: -webkit-sticky;
position: sticky;
top: 0;
}
/* Или избегайте transform на fixed элементах */
.fixed-element {
position: fixed;
/* Не используйте transform */
}
Если вы нашли баг, который воспроизводится только на iOS, но не на Android:
- Проверьте версию iOS (может, это баг конкретной версии).
- Проверьте модель устройства (iPhone vs iPad, старый vs новый).
- Проверьте, используется ли WKWebView или SFSafariViewController.
- Поищите баг на WebKit Bug Tracker — возможно, он уже известен.
6 Практика тестирования iOS WebView
Тестирование iOS WKWebView имеет свою специфику. Вот полный чек-лист для QA.
Матрица тестирования iOS WebView:
| Версия iOS | Устройство | Приоритет | Особенности |
|---|---|---|---|
| iOS 17 | iPhone 15 Pro | 🔴 Критический | Последняя версия, Dynamic Island |
| iOS 16 | iPhone 14 | 🔴 Критический | Lock Screen widgets |
| iOS 15 | iPhone 13 | 🟡 Высокий | Focus modes |
| iOS 14 | iPhone 12 | 🟡 Высокий | App Library |
| iOS 13 | iPhone 11 | 🟢 Средний | Dark Mode |
| iOS 12 | iPhone 8 | 🟢 Средний | Последняя версия для старых устройств |
| iPadOS 17 | iPad Pro | 🟡 Высокий | Stage Manager |
Чек-лист для тестирования iOS WKWebView:
- Safari Web Inspector: Для отладки DOM, CSS, JavaScript, Network
- Xcode Instruments: Для профилирования памяти, CPU, энергии
- Charles / Proxyman: Для перехвата и модификации трафика
- Xcode Simulator: Для тестирования на разных версиях iOS
- TestFlight: Для бета-тестирования на реальных устройствах
- На iPhone: Настройки → Safari → Дополнительно → Включить «Web Inspector»
- На Mac: Safari → Настройки → Дополнительно → Показать меню «Разработка»
- Подключите iPhone к Mac через USB
- Откройте Safari на Mac → Разработка → [Ваш iPhone] → [Ваше приложение]
- Теперь вы можете отлаживать WebView как обычный сайт
- Тестирование только на Simulator: Simulator не воспроизводит многие баги (память, производительность, камера).
- Игнорирование старых iOS: Многие пользователи всё ещё на iOS 14-15.
- Непроверка iPad: iPad имеет другой User-Agent и может отображать десктопную версию.
- Непроверка Low Power Mode: В этом режиме отключаются многие фичи (автовоспроизведение, фоновая загрузка).