```html How to Build a Website Blocker App in SwiftUI (2026)

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.

iOS 17+ · Xcode 16+ · SwiftUI Complexity: Advanced Estimated time: 2–4 weeks Updated: May 12, 2026

Prerequisites

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

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.

```