Разработка расширения для Safari (iOS Safari Extension)
Safari Web Extension на iOS — это тот же WebExtension API, что и в desktop-браузерах, но с жёсткими ограничениями мобильной платформы. Расширение упаковывается внутри нативного iOS-приложения, распространяется через App Store и активируется пользователем вручную в настройках Safari. Это не «просто портировать Chrome-расширение» — есть тонкости, которые ломают половину desktop-логики.
Структура и точки входа
Расширение состоит из двух частей: нативный Extension target (Swift/ObjC) и веб-часть (JS/HTML/CSS). Нативная часть — SafariWebExtensionHandler, реализующий NSExtensionRequestHandling. Через него JS-код расширения общается с нативным приложением через browser.runtime.sendNativeMessage().
Манифест: Safari поддерживает Manifest V2 и V3. Apple рекомендует MV3, но browser.action в MV3 на iOS работает иначе, чем в Chrome — toolbar popup не поддерживается напрямую. Вместо него используется SFSafariExtensionViewController на нативной стороне.
class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
func beginRequest(with context: NSExtensionContext) {
guard let item = context.inputItems.first as? NSExtensionItem,
let message = item.userInfo?[SFExtensionMessageKey] else {
context.completeRequest(returningItems: nil, completionHandler: nil)
return
}
// обрабатываем message от JS
let response = NSExtensionItem()
response.userInfo = [SFExtensionMessageKey: ["status": "ok"]]
context.completeRequest(returningItems: [response], completionHandler: nil)
}
}
Ограничения iOS, которые ломают desktop-логику
Background scripts не работают. В desktop Chrome background.js живёт постоянно. В iOS Safari — нет. Service Worker (MV3) выгружается сразу после выполнения задачи. Любое состояние, которое desktop-расширение хранит в памяти background script, на iOS нужно персистировать через browser.storage.local или передавать через нативный хост.
webRequest API отсутствует. Перехват и модификация сетевых запросов — через Content Blockers (отдельный тип расширения, WKContentRuleList), а не через webRequest. Если desktop-расширение блокирует рекламу через webRequest.onBeforeRequest, этот код не перенести напрямую.
Доступ к вкладкам ограничен. browser.tabs.query() возвращает только активную вкладку. Итерация по всем открытым вкладкам, как на desktop, недоступна.
Content scripts инжектируются с задержкой. На iOS страница может полностью загрузиться до того, как content script начнёт выполняться. Код, который рассчитывает на перехват DOMContentLoaded, может не успеть.
Типичный сценарий: расширение-менеджер паролей
Допустим, нужно автозаполнение через content script. JS инжектируется в страницу, находит <input type="password">, сообщает нативному хосту через browser.runtime.sendNativeMessage(). Нативный хост обращается к keychain через Security.framework и возвращает данные. Content script заполняет поле.
Проблема: sendNativeMessage на iOS работает только когда нативное Extension target запущено. Если пользователь не открывал приложение после перезагрузки устройства — Extension Host может не успеть стартовать. Нужен retry-механизм на стороне JS.
Инструмент миграции от Apple
Для конвертации существующего Chrome/Firefox расширения: xcrun safari-web-extension-converter. Он создаёт Xcode-проект с правильной структурой, но не исправляет несовместимые API — только предупреждает о них. После конвертации всегда остаётся ручная доработка.
Ревью и разрешения
App Store требует обоснования каждого разрешения в манифесте. tabs — для чего? storage — что именно хранится? Размытые ответы в форме подачи приводят к 4.0 Design rejection с просьбой уточнить функциональность.
Расширения с <all_urls> в host_permissions проходят усиленную проверку. Apple может запросить видеодемонстрацию работы расширения на реальном устройстве.
Срок разработки: 1-3 недели в зависимости от сложности JS-логики и объёма нативного взаимодействия. Стоимость рассчитывается индивидуально.







