How to Build a Website Blocker App in SwiftUI
A website blocker lets users define lists of distracting domains and schedule focused work sessions where those sites are completely inaccessible. It's aimed at knowledge workers, students, and parents who want screen-time enforcement without relying on router-level controls.
Prerequisites
- Mac with Xcode 16+
- Apple Developer Program ($99/year) — required for TestFlight and App Store
- Basic Swift/SwiftUI knowledge
- Real iPhone or iPad for testing — FamilyControls authorization cannot run in Simulator
- App entitlements:
com.apple.developer.family-controls(must be requested from Apple before submission) - Familiarity with
ManagedSettings,FamilyControls, andDeviceActivityframeworks
Architecture overview
The app is split into three targets: the main SwiftUI app (block list management, scheduling UI, StoreKit paywall), a DeviceActivityMonitor extension (wakes on schedule start/end), and a ShieldConfigurationExtension (renders the custom block screen). SwiftData stores BlockList models in a shared App Group container so both the app and extensions read the same data. FamilyControls authorization is requested once at onboarding; after that, ManagedSettingsStore applies and removes domain shields in response to DeviceActivity events.
WebBlocker/
├── App/
│ ├── WebBlockerApp.swift
│ ├── BlockListView.swift
│ ├── ScheduleView.swift
│ └── Models/BlockList.swift ← SwiftData @Model
├── ActivityMonitorExtension/
│ └── ActivityMonitor.swift ← DeviceActivityMonitor
├── ShieldExtension/
│ └── ShieldConfig.swift ← ShieldConfigurationExtension
└── Shared/
└── BlockingService.swift ← ManagedSettingsStore wrapper
Step-by-step
1. Data model
Define a BlockList SwiftData model that holds an array of domain strings, a schedule window, and an isActive flag the app and extensions both observe via the shared App Group store.
import SwiftData
import Foundation
@Model
final class BlockList {
var name: String
var domains: [String] // e.g. ["reddit.com", "twitter.com"]
var scheduleStart: Date
var scheduleEnd: Date
var isActive: Bool
var createdAt: Date
init(
name: String,
domains: [String] = [],
scheduleStart: Date = .now,
scheduleEnd: Date = .now.addingTimeInterval(3600),
isActive: Bool = false
) {
self.name = name
self.domains = domains
self.scheduleStart = scheduleStart
self.scheduleEnd = scheduleEnd
self.isActive = isActive
self.createdAt = .now
}
}
// App Group container shared with extensions
let sharedModelContainer: ModelContainer = {
let schema = Schema([BlockList.self])
let config = ModelConfiguration(
schema: schema,
url: FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.com.yourco.webblocker")!
.appending(path: "blocklists.sqlite")
)
return try! ModelContainer(for: schema, configurations: [config])
}()
2. Core UI
Build a BlockListView that lists saved block profiles, lets users toggle them on/off, and navigates to an editor for adding domains.
import SwiftUI
import SwiftData
struct BlockListView: View {
@Environment(\.modelContext) private var context
@Query(sort: \BlockList.createdAt) private var lists: [BlockList]
@State private var showingEditor = false
var body: some View {
NavigationStack {
List {
ForEach(lists) { list in
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(list.name).font(.headline)
Text("\(list.domains.count) domains")
.font(.caption).foregroundStyle(.secondary)
}
Spacer()
Toggle("", isOn: Binding(
get: { list.isActive },
set: { active in
list.isActive = active
BlockingService.shared.apply(list)
}
))
.labelsHidden()
}
}
.onDelete { offsets in
offsets.map { lists[$0] }.forEach { context.delete($0) }
}
}
.navigationTitle("Block Lists")
.toolbar {
Button { showingEditor = true } label: { Image(systemName: "plus") }
}
.sheet(isPresented: $showingEditor) {
BlockListEditor()
}
}
}
}
3. Distraction blocking with Screen Time API
Request FamilyControls authorization once, then use ManagedSettingsStore to shield the chosen domains whenever a block list is activated.
import FamilyControls
import ManagedSettings
import DeviceActivity
@MainActor
final class BlockingService: ObservableObject {
static let shared = BlockingService()
private let store = ManagedSettingsStore()
private let center = AuthorizationCenter.shared
func requestAuthorization() async throws {
try await center.requestAuthorization(for: .individual)
}
func apply(_ list: BlockList) {
guard list.isActive else {
store.clearAllSettings()
return
}
let tokens: Set = Set(
list.domains.compactMap { WebDomain(domain: $0) }
)
store.webContent.blockedByFilter = .enabled(
.init(includeCategories: [], includeWebDomains: tokens, includeSearchTokens: [])
)
scheduleDeviceActivity(for: list)
}
private func scheduleDeviceActivity(for list: BlockList) {
let schedule = DeviceActivitySchedule(
intervalStart: Calendar.current.dateComponents([.hour, .minute], from: list.scheduleStart),
intervalEnd: Calendar.current.dateComponents([.hour, .minute], from: list.scheduleEnd),
repeats: true
)
let center = DeviceActivityCenter()
try? center.startMonitoring(
DeviceActivityName(list.name),
during: schedule
)
}
}
4. Privacy Manifest setup
Apple requires a PrivacyInfo.xcprivacy in every target; for a website blocker you must declare NSPrivacyAccessedAPICategoryUserDefaults and any network-extension usage reasons.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key><false/>
<key>NSPrivacyTrackingDomains</key><array/>
<key>NSPrivacyCollectedDataTypes</key><array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
</array>
</dict>
</plist>
Common pitfalls
- Forgetting the FamilyControls entitlement: You must request
com.apple.developer.family-controlsfrom Apple via the developer portal before submitting. Apps that try to use the Screen Time API without it will crash at runtime — and your App Store review will be rejected if the entitlement file is missing. - Testing on Simulator: FamilyControls authorization always fails in Simulator. Budget time to test every blocking flow on a real device enrolled under a personal Apple ID — not a managed MDM account, which blocks the API differently.
- ManagedSettings state survives app deletion: If your app is deleted, shield settings may linger until the user re-pairs via Screen Time settings. Always call
store.clearAllSettings()in your app's termination path and document this behavior for reviewers. - App Review scrutiny: Apple reviewers test parental-control and blocking apps carefully. Provide detailed review notes explaining how FamilyControls is used, include a demo Apple ID, and avoid marketing language like "unbreakable" — it triggers extra scrutiny.
- DeviceActivity extension memory limit: Extensions have a ~6 MB memory cap. Avoid loading your full SwiftData stack inside the monitor extension; read only from UserDefaults (App Group) or a lightweight JSON file instead.
Adding monetization: Subscription
Use StoreKit 2's ProductView and SubscriptionStoreView (iOS 17+) to present a paywall gating the scheduler and unlimited block lists behind a monthly or annual subscription. Define your subscription group in App Store Connect first, then load products with Product.products(for:) at app launch. Gate the BlockingService.apply(_:) call behind an @AppStorage entitlement flag you set after a verified Transaction.currentEntitlement check — never rely solely on local state, since subscriptions can lapse. For free users, allow one active block list with no schedule to demonstrate core value before the paywall.
Shipping this faster with Soarias
Soarias scaffolds all three targets (main app, DeviceActivityMonitor extension, ShieldConfigurationExtension) with the correct App Group identifiers and entitlement files pre-wired. It auto-generates the PrivacyInfo.xcprivacy for every target, configures fastlane lanes for device testing and ASC submission, and fills in the required review notes template explaining FamilyControls usage — the section most developers forget until their first rejection.
For an advanced app like this one, the manual setup alone (entitlement request, multi-target project config, shared container, Privacy Manifest ×3) typically costs 3–5 days. Soarias collapses that to an afternoon, so your 2–4 week estimate lands closer to 1.5 weeks of focused feature work rather than infrastructure wrestling.
Related guides
FAQ
Do I need a paid Apple Developer account?
Yes. The com.apple.developer.family-controls entitlement, TestFlight distribution, and App Store submission all require an active Apple Developer Program membership ($99/year). Unlike most apps, you also need to separately request the FamilyControls entitlement via the developer portal before Xcode will let you build for a real device.
How do I submit this to the App Store?
Archive all three targets together in Xcode (Product → Archive), then upload via Xcode Organizer or xcrun altool. In App Store Connect, attach your subscription IAP before submitting for review and include detailed reviewer notes explaining how Screen Time authorization is used and how to test the blocking flow with a demo account. Soarias handles the fastlane submission lane and pre-fills the review notes template for you.
Can users bypass the block if they uninstall the app?
When your app is deleted, the ManagedSettingsStore configuration is eventually cleared by the OS, so yes — a determined user can uninstall to escape. This is a known limitation of the individual-authorization (non-parental) Screen Time mode. For stricter enforcement you'd need to target Family Sharing with parental authorization, which changes the FamilyControls authorization flow and your App Store category classification significantly.
Last reviewed: 2026-05-12 by the Soarias team.