We develop Safari extensions for iOS using the WebExtension API, accounting for the strict limitations of the mobile platform. Unlike desktop browsers, the extension is packaged inside a native iOS app, distributed via the App Store, and manually activated by the user in Safari settings. You can't directly port a Chrome extension — half of the desktop logic breaks due to iOS peculiarities. Our experience: 5 years in iOS development, over 50 successful projects, guaranteed App Store Review passage. On average, 80% of JS code ports without changes; the rest requires manual adaptation.
Why is an iOS Safari extension more complex than its desktop counterpart?
Desktop browsers allow background scripts to run persistently, intercept network requests via webRequest, and access all tabs. On iOS, these capabilities are unavailable or work differently. Let's examine the key differences.
Background scripts don't work
In Chrome, background.js lives constantly. In iOS Safari, a Service Worker (MV3) is unloaded as soon as it finishes its task. Any state that a desktop extension keeps in the background script's memory must be persisted via browser.storage.local or passed through the native host.
The webRequest API is absent
Intercepting and modifying network requests on iOS is done via Content Blockers (WKContentRuleList), not through webRequest. If your extension blocks ads using webRequest.onBeforeRequest, that code cannot be ported directly.
Tab access is limited
browser.tabs.query() returns only the active tab. Iterating over all open tabs, as on desktop, is not possible.
Content scripts inject with a delay
On iOS, the page may fully load before the content script starts executing. Code that relies on intercepting DOMContentLoaded may be too late.
How we adapt the extension for iOS
We use Swift 5.9+ for the native part and JavaScript (ES2020) for the web part. Each extension undergoes refactoring: migration from webRequest to WKContentRuleList, replacement of background.js with a Service Worker that persists state in browser.storage.local. Here's an example of a native message handler from JS:
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
}
// process message from JS
let response = NSExtensionItem()
response.userInfo = [SFExtensionMessageKey: ["status": "ok"]]
context.completeRequest(returningItems: [response], completionHandler: nil)
}
}
Desktop vs iOS Safari Extension comparison
| Capability |
Desktop (Chrome) |
iOS Safari |
| Background scripts |
Constantly active |
Service Worker unloaded |
| Request interception |
webRequest |
WKContentRuleList |
| Tab access |
All tabs |
Only active tab |
| Content injection |
At DOMContentLoaded |
With delay |
| Native messaging |
chrome.runtime.connectNative |
browser.runtime.sendNativeMessage |
Typical porting challenges
| Problem |
Solution |
Adaptation time |
| Background script with state |
Move to storage.local |
2 to 5 days |
| Use of webRequest |
Replace with WKContentRuleList |
1 to 3 days |
| Access to multiple tabs |
Rework for active tab only |
1 to 2 days |
Typical scenario: password manager
Suppose autofill must be implemented via a content script. JS is injected into the page, finds <input type="password">, and sends a message to the native host via browser.runtime.sendNativeMessage(). The native host accesses the keychain through Security.framework and returns the data. The content script fills the field.
Problem: sendNativeMessage on iOS works only when the native Extension target is launched. If the user hasn't opened the app after device reboot, the Extension Host may not start in time. A retry mechanism on the JS side is needed.
How to port a Chrome extension to Safari?
- Run the converter: execute
xcrun safari-web-extension-converter --bundle-name YourExt path/to/extension. The utility creates an Xcode project with a native wrapper.
- Analyze incompatible APIs: check for use of
webRequest, background.js, unlimited tab access. These sections need replacement.
- Adapt the background: replace the persistent background script with a Service Worker that persists state in
storage.local. Rewrite event handlers.
- Migrate request blocking: if using
webRequest, rewrite to WKContentRuleList. Note that Content Blocker supports a limited set of rules (up to 50,000 rules per blocker).
- Test on a real device: use TestFlight to install the build on an iPhone. Verify content script behavior across different sites.
What's included in the work
- Analysis of your desktop extension (if any) and mapping of incompatible APIs
- Development of a native Extension target in Swift with message handlers
- Adaptation of JS logic for iOS: replacement of
webRequest, background scripts, persistence
- Manifest and permission configuration (justification for App Store)
- Integration with App Store Connect, TestFlight, Provisioning Profile
- Documentation for building and publishing
- Support for 30 days after delivery
Timeline and cost
Development timeline: from 1 to 3 weeks depending on JS logic complexity and native interaction volume. Cost is calculated individually. We will evaluate the project after agreeing on requirements.
What permissions are required for App Store?
The App Store requires justification for each permission in the manifest. tabs — for what purpose? storage — what exactly is stored? Vague answers in the submission form lead to a 4.0 Design rejection asking for functional clarification.
Extensions with <all_urls> in host_permissions undergo enhanced review. Apple may request a video demonstration of the extension working on a real device.
Why us?
Native handlers in Swift are 10-20 times faster than JS for cryptography and file processing tasks. If your extension performs heavy computations, moving logic to native code provides a noticeable performance boost. Over 50 ported projects confirm this approach.
Contact us for a project evaluation — we'll prepare a proposal within 2 business days. Get a consultation on adapting your extension today.
More details in the Safari Web Extensions documentation.
Development of Widgets, App Clips, and Live Activities: Entry Points Outside the App
We understand that users see your app not only when they open it. A widget on the home screen, a live score in Dynamic Island, a mini experience without installation — these are separate entry points that we implement within platform constraints. Over 5 years, we have developed more than 50 extensions for mobile apps, from simple informational widgets to App Clips with payment scenarios, saving clients up to 30% of time on repeat visits.
What entry points should you consider for your app?
WidgetKit Widget Development: Why You Can't Just "Add a Widget"
WidgetKit works via a Timeline Provider — the widget doesn't stay in memory continuously; it requests data snapshots in advance. The most common mistake: developers try to show real-time data via URLSession directly from getTimeline(). Apple doesn't prohibit this, but with aggressive updates, the system starts throttling requests, and the widget gets stuck on outdated data.
The correct approach: the main app updates data via WidgetCenter.shared.reloadTimelines(ofKind:) — after receiving a push notification or when the user returns to the foreground. The widget reads data from a shared App Group container using UserDefaults(suiteName:) or file storage. No direct network requests in the provider in production.
In the latest iOS versions, AppIntent-based interactive widgets have emerged — buttons and toggles directly on the widget without opening the app. This is implemented via Button(intent:) in the SwiftUI widget layout. Only works for simple actions; complex logic should transition to the app via widgetURL.
How Live Activities Change User Experience?
Live Activities are a mechanism for displaying live data on the Lock Screen and Dynamic Island (iPhone 14 Pro+). They are launched via ActivityKit, updated via push notifications of type liveactivity with a payload up to 4KB.
Architecturally, it's a separate SwiftUI target with two views: compact (Dynamic Island) and expanded (Lock Screen). Data is passed via ActivityAttributes — a strictly typed structure. The dynamic part is ContentState, while the static part (unchanged during the activity) is directly in ActivityAttributes.
A typical issue: Live Activity doesn't update on the device even though push is sent. The reason is that the app doesn't have permission for background push or apns-push-type is set incorrectly. In production, you need apns-push-type: liveactivity and a token from activity.pushToken. According to Apple documentation, without a correct push token, the Activity won't receive updates.
When to Use App Clips vs Instant Apps?
App Clips (iOS) and Instant Apps (Android) solve a similar problem — provide functionality without installing the full app. But the implementation is fundamentally different.
App Clip is a separate target in Xcode, max 15MB, launched via NFC tag, QR code, Safari Smart App Banner, or a link in Messages. Data access is limited: no Keychain sharing with the main app without explicit setup, no access to HealthKit, no push notifications (only ephemeral). The App Clip Card is configured in App Store Connect, and metadata errors are a common reason for rejection.
Android Instant Apps are built on a modular architecture: the app is divided into feature modules, each of which can be downloaded separately via Play Feature Delivery. An Instant App is a feature module with <dist:module dist:instant="true">. The limitation is no more than 15MB total for instant delivery.
Comparison shows that App Clips win in payment scenarios due to Apple Pay integration — conversion is 20% higher compared to Instant Apps in similar cases. Instant Apps are better suited for game demos and services requiring quick access via Google Search.
| Parameter |
App Clips |
Instant Apps |
| Max size |
15 MB |
15 MB |
| Launch triggers |
NFC, QR, URL, Safari |
URL, Google Search, Play Store |
| Shared Keychain |
Via App Group |
Via SharedPreferences/Keystore |
| Recommended scenario |
Payment, boarding, demo |
Game demo, one-time services |
What Does Our Work Include?
-
Audit of current architecture: determine which entry points your app needs — widget, Live Activity, App Clip, Instant App.
-
Prototyping: visual model of the extension following platform guidelines (Apple HIG, Material Design).
-
Development: implementation in Swift (iOS) or Kotlin (Android) using WidgetKit, ActivityKit, App Clip API, Play Feature Delivery.
-
Integration: setting up App Group, Keychain sharing, push certificates, provisioning profiles.
-
Testing: on real devices (iPhone, iPad, Android) and simulators. For Live Activities, test via
xcrun simctl push.
-
Publication: preparing metadata for App Store Connect (App Clip Card) and Google Play Console (Instant App configuration).
-
Documentation and training: architecture description, widget update instructions, push notification troubleshooting.
How Does Our Development Process Work?
-
Analytics: which app features are truly needed outside the app, and which mechanism fits. Widget for forecast — WidgetKit. Real-time delivery tracking — Live Activity. Payment at checkout — App Clip.
-
Design: choosing stack, data update schemes (Timeline, push), UI layouts for compact and expanded views.
-
Implementation: writing code in Swift/Kotlin, configuring App Group, push certificates, test schemes.
-
Testing: each extension is tested in isolation. WidgetKit rendering is verified via Xcode Widget Gallery, Live Activities via simulator with forced push.
-
Deployment: publishing to stores, monitoring metrics (update frequency, App Clip launch count).
Estimated Timeframes
| Extension Type |
Timeframe (business days) |
| Simple informational widget |
5 to 10 |
| Interactive widget (AppIntent) |
10 to 15 |
| Live Activity with push |
10 to 20 |
| App Clip with payment |
20 to 30 |
| Instant App (Android) |
15 to 25 |
Cost is calculated individually after audit. An estimate is provided within 2 business days.
What Are Typical Mistakes in Extension Development?
-
Too frequent widget updates — leads to throttling and empty state. We recommend an interval of at least 15 minutes (see Apple Human Interface Guidelines in WidgetKit documentation).
-
Ignoring shared container — the widget doesn't see data because it uses its own
UserDefaults instead of App Group.
-
Lack of fallback for Live Activities — if push isn't delivered, the user sees outdated data. A periodic polling mechanism via
Activity.update with pushType: nil is needed.
-
Incorrect App Clip Card metadata — a common reason for rejection in App Store Review. For example, incorrect URL or missing icon.
Contact us to assess which extension fits your app. Order an audit of current entry points — we'll find non-obvious scenarios for widgets and App Clips. Get an engineer consultation on architecture today.