DuckFence is a privacy-focused web browser demo designed to allow users to navigate across different domains and apply protection rules to prevent tracking built with Swift and SwiftUI, leveraging a modular architecture using The Composable Architecture (TCA). Its primary goal is to block trackers using custom content blocking rules generated from the tds.json
file by DuckDuckGo's Tracker Radar.
- Features/
MainBrowserFeature
: Core browser experience with navigation, history, and tracker protection.UnprotectedDomainsListFeature
: Allows users to manage domains excluded from blocking.
- Infrastructure/
TrackerDataDownloaderKit
: Handles downloading and storing the tracker data (tds.json
).WebProtectionKit
: Builds and applies WKContentRuleLists based on tracker data.StorageKit
: Lightweight persistence layer usingSwiftData
andUserDefaults
.
- Utilities/
Environments
: Dependency injection and mock support.
- SnapshotUtilities/
- Snapshot testing helpers.
-
When the app is opened for the first time, it syncs the JSON file containing all tracking blocking information. Once synced, it is saved to disk.
-
By using Etags, the JSON file will only be re-synced if its content has changed.
-
The information stored on disk is used to generate the
WKContentRuleList
rules. -
Every time the rules change (e.g., a domain is added to the allow list), the
WebView
is recreated with the updated rules. It is possible to add or remove domain protection from the UI. -
Users can browse the history of visited domains within the app.
-
Users can view the list of domains that are unprotected (those in the allow list).
-
When searching for a domain (e.g., duckduckgo.com), the app applies HTTPS by default. There is no additional logic to determine if the domain supports HTTPS or to fall back to HTTP. However, users can still navigate to HTTP domains if they explicitly include the prefix in the search (e.g., http://www.duckduckgo.com).
-
The app currently uses a single WebView to display domains. One possible improvement would be to use multiple WebView instances to build a more complex UI (similar to real browsers) that supports tabbed browsing. This could also prevent certain issues encountered during development — for example, navigating through history before a domain finishes loading in the WebView can cause errors. This was solved by applying a semaphore logic to block navigation until the WebView is fully loaded.
-
An additional feature to clear the browsing history could be included.
- Xcode version: 16.3
- Swift version: 6.1
- Device for snapshot tests: iPhone 16 Pro (18.4)
There are several ways to test whether the tracking blocker is working. One option is to use the developer tools provided by certain browsers, either on the simulator or a physical device, to verify whether specific domains are using third-party trackers. If tracking protection is enabled, these tools will indicate it.
Another option is to inject scripts into the WebView to determine the protection status against certain third-party trackers. For this purpose, I’ve implemented a service (TrackerDetectionService.swift) that injects a script into the WebView to capture the protection status. This injection is only available in DEBUG mode, meaning it will not run in production.
This service logs the protection status to the Xcode console.
How would you leverage existing WKContentRuleListStore API to improve performance of reloading rule lists after allowlist is modified?
To optimize the loading of rule lists, I’ve followed the following principles:
-
When a domain is visited and no rules have been generated, the rules are generated at that moment.
-
Once the rules are generated, the domain is considered protected. If a new domain is visited, and the rules haven't changed, the WebView is simply reloaded (with the new URL and the previously generated rules).
-
If the protection status of the currently visited domain is changed, the rules will change and must be regenerated. In this case, the WebView is also recreated (since it is not possible to update the rules of an existing WebView).
-
Then, if another new domain is visited, the flow returns to the third case.
How would you ensure that users are protected from trackers also if the initial Tracker Data Set fetch fails?
In this case, I’ve implemented a fallback mechanism in case the request to fetch the JSON file from the server fails: the app includes a local version of the JSON embedded within it. If the remote request fails, the local JSON is injected, and the process continues as normal.
In this scenario, no Etag is stored, ensuring that the app will attempt to fetch the remote JSON again the next time it is launched. However, this solution relies on the embedded local file being up to date to ensure that no new rules are missed. One way to mitigate this is to reduce the release cycle (e.g., to a weekly or biweekly release train) and ensure that updating the local JSON with the latest version from the server is a required step in the release process. This way, the embedded file should remain reasonably up to date to handle this fallback scenario.