A small iOS app that blocks apps and websites on a schedule. You pick what to block, which days, and what hours. It runs in the background and gets out of your way.
This is the open-source release of Cactus, made by Kiln.
iOS 26, SwiftUI, and Apple's Screen Time APIs (FamilyControls + ManagedSettings
- DeviceActivity). Five targets:
| Target | What it does |
|---|---|
Cactus |
The app: rules, schedules, breaks, hard mode |
CactusDeviceActivityMonitor |
Applies and lifts blocks when a schedule starts or ends |
CactusShieldConfiguration |
Draws the lock screen shown over a blocked app |
CactusShieldAction |
Handles taps on that lock screen |
CactusShare |
Safari share-sheet extension for adding a website to a rule |
All five share one App Group's UserDefaults.
Cactus/Models/CactusRule.swift, AppGroupStore.swift and ShieldPlan.swift
are compiled into every target — ShieldPlan is the single place the contents
of a block are computed.
You need Xcode 26, an Apple Developer account (the Screen Time APIs need a provisioned App ID — they don't work with a personal free team), and XcodeGen.
brew install xcodegen && xcodegen generate && open Cactus.xcodeprojCactus.xcodeproj is generated from project.yml and is not in git — run
xcodegen generate again whenever you add or remove a source file.
Everything below is marked CHANGE ME in the source.
project.yml—bundleIdPrefix,DEVELOPMENT_TEAM, and thePRODUCT_BUNDLE_IDENTIFIER/APP_GROUP_IDpair in all five targets.- Apple Developer portal — register the five App IDs, enable the Family Controls capability on each, and register the App Group and enable it on all five. Miss one extension and it silently reads empty storage: no crash, just a blocker that never blocks.
Cactus/Models/ShieldPlan.swift→CactusSelfProtection.protectedBundleIDs— your app's own bundle IDs. This is what stops the app shielding itself out of existence; get it wrong and you lock yourself out of the only screen where a rule can be lifted.Cactus/AppConfiguration.swift— support email, website, App Store ID. All optional: leave one empty and the menu row it feeds disappears rather than opening a dead link.Cactus/Info.plist—UIApplicationShortcutItemTypemust matchAppConfiguration.contactShortcutType.
DEVELOPMENT_TEAM is deliberately blank. To keep it out of git, put
DEVELOPMENT_TEAM = ABCDE12345 in a Local.xcconfig (already gitignored) or
set it in Xcode's Signing & Capabilities pane.
The rule engine is the whole of what ships here. The App Store build's artwork, writing, and one of its two modes are not part of the release:
- No artwork at all. No app icons (and no alternate-icon picker), no
illustrations. The home screen's title is the word alone, where the shipping
app puts a mark beside it. Every glyph left in the app is an SF Symbol, and
Assets.xcassetsholds one colour. The build has noAppIconasset — add yours and setASSETCATALOG_COMPILER_APPICON_NAMEinproject.yml. - No welcome screen. The shipping app opens on a pitch — hero image, manifesto, feature list — before it lets you do anything. With the artwork and the voice stripped out there was nothing left of it worth keeping, so a first run lands straight on home, where an empty-state card offers to build the first rule. That card's button is also where the Screen Time permission is requested; if you put a welcome screen back, move the ask into it.
- One quote instead of nine. The shipping app makes you type back a
borrowed line — Woolf, Thoreau, Weil — before it grants a break. Those are
other people's words and aren't ours to relicense, so
Copy.quotesLongcarries a single generic line. Add your own, and check you have the right to ship anything you borrow. - Less voice. The shield used to lead with a line of its own and push "<app> is blocked." into the subtitle; here that status is the whole shield. What's left throughout is functional copy — the app's tone is yours to write.
- No whole-phone mode. The shipping app has a second mode that locks
everything except a short allowlist, with its own count-up tile, midnight
auto-end, and priced exits. It's gone: the tile, the
.all(except:)lockdown policy, the day-stamped state, and the DeviceActivity monitor that ended it. What survives from that work isCactusSelfProtection— the app is always exempt from its own shields, which matters for ordinary rules too, since nothing stops a user picking Cactus in the app picker. - No overflow menu. Home has one toolbar button,
+. The shipping app's ⋯ menu (contact, about, share, icon picker) is gone, and with it the share sheet and the rating prompt.
Grep for ADD YOUR OWN to find every spot.
Cactus is made by Kiln.
GPL-3.0-only. Use it, study it, change it, ship it — but anything you distribute has to carry its complete source under the same license.
One trap specific to iOS: GPL 3 forbids piling extra restrictions on top of the freedoms it grants, and the App Store's DRM and per-device limits are exactly that. A GPL build is not something you can put on the App Store — this is what got VLC pulled from it in 2010. Sideload, or run it on your own devices.
The App Store build of Cactus is not this code, and is not GPL. That isn't a loophole: the license is a grant to everyone else and leaves the copyright holder's own rights untouched, so Kiln ships a proprietary build while this base stays free. Qt and MySQL have worked this way for decades.
Pull requests aren't accepted, for that same reason — merged code would be GPL-only and could never ship in the App Store build. Issues, bug reports and questions are welcome, and what needs fixing gets fixed.
The GPL covers the code, not the brand. The name "Cactus" and the Kiln marks aren't licensed; ship your fork under your own name.
Copyright © 2026 Kiln.