- Add Block Adult Content toggle to all three rule editors, persisted as BlockingRule.blockAdultContent (inline default for clean migration of existing stores) and surfaced in the detail sheet as an 'Adult websites: Blocked/Allowed' row - Engage Screen Time's adult-website filter (webContent.blockedByFilter = .auto()) alongside the rule's shield and clear it when the shield clears - Replace the New Rule sheet's view-swap with a NavigationStack push (navigationDestination(item:)); the editor uses native chrome there (system back button, inline title, toolbar rename), enabling the push animation and edge-swipe back - Tests: 93 passing (+2 unit: draft round-trip and enforcer forwarding; +3 UI: toggle-to-detail flow, default-allowed row, swipe-back as a behavioral proof of native navigation) - Spec updated accordingly (editor sections, behavior, data model, navigation note)
332 lines
16 KiB
Markdown
332 lines
16 KiB
Markdown
# Severed — "Rules" Feature Spec (derived from Opal screen recording)
|
||
|
||
Source: `ScreenRecording_06-12-2026 11-26-19_1.MP4` (iPhone, 4:29). The rules feature
|
||
appears roughly between 1:00–3:00 of the recording, on Opal's "My Apps" tab.
|
||
This spec describes what was observed, then maps it onto an implementation plan
|
||
for Severed.
|
||
|
||
---
|
||
|
||
## 1. Concept
|
||
|
||
A **Rule** is a recurring, automated app-blocking policy. Unlike a one-off
|
||
block/timer session, a rule re-arms itself on a schedule. Three rule types are
|
||
offered (Opal's "New Rule" sheet):
|
||
|
||
| Type | Icon | Example shown | Semantics |
|
||
|------|------|---------------|-----------|
|
||
| **Schedule** | calendar grid | "e.g. 9-5, Daily" | Block selected apps during a daily time window on chosen days |
|
||
| **Time Limit** | hourglass | "e.g. 45m/day" | After N minutes of cumulative use of selected apps per day, block them until a reset point |
|
||
| **Open Limit** | padlock | "e.g. 5 opens/day" | After N opens of selected apps per day, block them (not demoed in video; inferred from card) |
|
||
|
||
Common attributes across all types:
|
||
|
||
- **Name** — user-editable, free text (presets: "Work Time", "Weekend Zen", "Locked In", "Sleep", "Wind Down", "Deep Sleep", "Laser Focus", "Reading Time", "Gym Time"; defaults for new rules: "In the Zone" (schedule), "Time Keeper" (time limit))
|
||
- **Days of week** — 7-day toggle set, summarized as "Weekdays" / "Weekends" / "Every day" / custom
|
||
- **App selection** — apps, categories, and websites; selection mode is either **Block** (block selected) or **Allow Only** (block everything except selected)
|
||
- **Hard Mode** — boolean, PRO-gated in Opal; subtitle "No unblocks allowed". When off, the rule detail shows "Unblocks allowed: Yes"
|
||
- **Enabled/disabled** — a rule can be disabled without deleting ("Disable Rule")
|
||
|
||
Derived status (drives card/detail UI):
|
||
- **Active** → countdown to window end: green pill "6h left"
|
||
- **Inactive** → countdown to next activation: "Starts in 22h" / "Starts in 11h"
|
||
|
||
---
|
||
|
||
## 2. Screen inventory & navigation map
|
||
|
||
```
|
||
Tab bar: [Home] [My Apps] [Timer]
|
||
│
|
||
▼
|
||
Apps screen (large title "Apps")
|
||
├── "Blocked Apps" section
|
||
├── "Rules >" section ──ta p "+ New"──▶ New Rule sheet
|
||
│ │ ├── tap rule-type card ─▶ Rule Editor (blank/default)
|
||
│ │ └── tap preset card ───▶ Rule Editor (pre-filled)
|
||
│ └── tap rule card ─▶ Rule Detail sheet
|
||
│ └── "Edit Rule" ─▶ Rule Editor (edit mode)
|
||
│ └── "Selected Apps >" ─▶ App Picker
|
||
└── "Apps" section (folders: Distracting / Always Allowed / Never Allowed)
|
||
```
|
||
|
||
All rules UI is presented as **sheets stacked over the Apps screen** (dimmed,
|
||
blurred background; grabber at top; circular ✕ or ‹ button top-left). Nothing
|
||
navigates by push except within the sheet stack.
|
||
|
||
---
|
||
|
||
## 3. Screens in detail
|
||
|
||
### 3.1 Apps screen ("My Apps" tab)
|
||
|
||
Dark theme throughout (near-black background, very dark green tint).
|
||
|
||
1. **Large title** "Apps".
|
||
2. **Blocked Apps** — section header; horizontal row of currently-blocked app
|
||
icons. Each icon has a lock badge overlay and a teal/green rounded-rect
|
||
outline; caption "Unblock" under the icon. Tapping unblocks (with friction
|
||
if hard mode — not demoed).
|
||
3. **Rules** — header row: "Rules ›" (leading, tappable to a full list,
|
||
presumably) and "**+ New**" (trailing, green tint) which opens the New Rule
|
||
sheet.
|
||
- Content: horizontally scrolling row of **rule cards** (~2 visible).
|
||
- **Rule card** anatomy (rounded ~24pt corners):
|
||
- Top: icon pair — rule-type icon (calendar) → small arrow → shield icon.
|
||
Active rule: icons in color, card tinted dark green. Inactive: greyscale.
|
||
- Middle: status — active: green capsule pill "6h left"; inactive: plain
|
||
text "Starts in 22h".
|
||
- Bottom: rule **name** (semibold), then a sub-row "Block" + tiny cluster
|
||
of the blocked app icons.
|
||
4. **Apps** — section of folder-style groups: "Distracting (4 items)" showing
|
||
a 2×2 mini icon grid, "Always Allowed", "Never Allowed (Hidden)" with an
|
||
eye-slash glyph. (Out of scope for the rules clone but shares the app
|
||
selection model.)
|
||
|
||
### 3.2 Rule Detail sheet
|
||
|
||
Presented on tapping a rule card. Partial-height card sheet.
|
||
|
||
- Top-left: circular ✕ close button.
|
||
- Centered: icon pair (rule type → shield), then caption
|
||
"`Schedule, Starts in 22h`" (type + live status), then large title
|
||
("Weekend Zen").
|
||
- **Detail rows** (single inset rounded card, label left / value right):
|
||
| Label | Example value |
|
||
|---|---|
|
||
| During this time | `09:00 – 12:00` |
|
||
| On these days | `Weekends` |
|
||
| Block | `[app icons] 1 App` / `3 Apps` |
|
||
| Unblocks allowed | `Yes` (hidden/`No` when Hard Mode) |
|
||
- Bottom: full-width white pill button "**✎ Edit Rule**" → morphs the sheet
|
||
into the Rule Editor in edit mode.
|
||
|
||
### 3.3 New Rule sheet
|
||
|
||
Presented from "+ New". Full-height sheet, scrollable.
|
||
|
||
- Header: ✕ left, centered title "**New Rule**".
|
||
- **Rule type row** — 3 horizontally arranged cards (Schedule / Time Limit /
|
||
Open Limit), each: glyph, bold name, example caption ("e.g. 9-5, Daily",
|
||
"e.g. 45m/day", "e.g. 5 opens/day"). Tapping opens the matching editor with
|
||
defaults.
|
||
- **Preset gallery** — vertically scrolling sections, each with a bold header
|
||
+ grey subtitle, containing a 2-up grid of photo-backed preset cards:
|
||
- **Get More Done** — "Maximize your productivity while staying sane."
|
||
- Work Time (Schedule 09:00–17:00, Block, weekdays)
|
||
- Laser Focus (Schedule 14:00–18:00, Block)
|
||
- **Sleep, Relax and Reset** — "Sleep better, rise refreshed."
|
||
- Wind Down (Schedule 20:00–22:00, Block)
|
||
- Deep Sleep (Schedule 22:00–06:00, Block)
|
||
- **Build Healthy Habits** — "Spend time on important things."
|
||
- Reading Time (Schedule ~19:45–20:45, Block)
|
||
- Gym Time (Schedule 17:30–18:30, Block)
|
||
- **Preset card** anatomy: full-bleed background photo, top row icon pair
|
||
(type → shield), time range caption, name, "Block" + suggested app icons,
|
||
and a circular "+" button bottom-right. Tapping anywhere opens the
|
||
Schedule editor pre-filled with the preset's name/times/days.
|
||
|
||
> **Navigation (Severed):** picking a rule type or preset **pushes** the
|
||
> editor inside the sheet via native SwiftUI navigation (`NavigationStack` +
|
||
> `navigationDestination(item:)`), so the system push animation and
|
||
> edge-swipe-back work; the editor keeps its custom header chrome.
|
||
|
||
### 3.4 Rule Editor — Schedule type
|
||
|
||
Sheet with: ‹ back (top-left), centered **rule name** as title, ✎ pencil
|
||
button (top-right) to rename.
|
||
|
||
Sections (each an inset rounded group with a small icon + caption header):
|
||
|
||
1. **📅 During this time**
|
||
- Rows `From` / `To` with right-aligned time + stepper chevrons (`09:00 ⌃⌄`).
|
||
- A dotted vertical line with ●/○ endpoints visually links From → To.
|
||
- Tapping a row expands an inline wheel time picker (24h, demoed changing
|
||
From 22:00 → 23:00).
|
||
2. **On these days:** — trailing summary label ("Weekdays"/"Weekends"/custom);
|
||
row of 7 circular toggles `S M T W T F S`; selected = filled white circle
|
||
with black letter, unselected = dark circle.
|
||
3. **🛡 Apps are blocked**
|
||
- Row: `Selected Apps` → `N Apps ›` — pushes the App Picker.
|
||
4. **Hard Mode** `⚡PRO` badge — subtitle "No unblocks allowed"; trailing
|
||
toggle.
|
||
5. **Block Adult Content** *(Severed addition — not in the Opal video)* —
|
||
subtitle "Filter adult websites while this rule is active"; trailing
|
||
toggle. Maps to Screen Time's web-content filter
|
||
(`ManagedSettingsStore.webContent.blockedByFilter = .auto(...)`), applied
|
||
and cleared together with the rule's shield. Surfaces in the rule detail
|
||
as an "Adult websites | Blocked/Allowed" row.
|
||
6. **CTA**
|
||
- Creating: full-width gradient pill "**Hold to Commit**" — a press-and-hold
|
||
interaction (deliberate friction) that fills, then saves and dismisses to
|
||
the Apps screen where the new card appears.
|
||
- Editing existing: "**✓ Done**" pill, plus a red text button
|
||
"**⏸ Disable Rule**" beneath it.
|
||
|
||
### 3.5 Rule Editor — Time Limit type ("Time Keeper")
|
||
|
||
Same chrome (back / title / rename). Sections:
|
||
|
||
1. **⏳ When I use** — row `This App` → `Select ›` (app selection).
|
||
2. **For this long** — subtitle "Daily"; right-aligned value with stepper
|
||
`45m ⌃⌄`.
|
||
3. **On these days:** — identical day picker as Schedule.
|
||
4. **🛡 Then block app** — row `Until` with stepper value `Tomorrow ⌃⌄`
|
||
(reset point — e.g. tomorrow/next morning).
|
||
5. **Hard Mode** toggle — same as Schedule.
|
||
6. **Block Adult Content** toggle — same as Schedule.
|
||
7. **Hold to Commit**.
|
||
|
||
### 3.6 Rule Editor — Open Limit type
|
||
|
||
Not demoed beyond its card. Spec by analogy: "When I open [apps]" /
|
||
"More than `N opens ⌃⌄` (Daily)" / day picker / "Then block until …" /
|
||
Hard Mode / Block Adult Content / Hold to Commit.
|
||
|
||
### 3.7 App Picker (shared component — also used in onboarding & timer)
|
||
|
||
Full-height sheet:
|
||
|
||
- Header: ‹ back, centered title "**Selected**", and a circular green **✓**
|
||
confirm button top-right.
|
||
- **Segmented control**: `Block` | `Allow Only`.
|
||
- Top rows: "**+ Add App or Website**", a "Suggested" horizontal row of app
|
||
icons (one-tap add), and a "Never Allowed — 0 Apps" row with footnote
|
||
"Never allowed Apps will also be blocked".
|
||
- Hint text: *Select apps/websites, tap ">" to expand*.
|
||
- **Category list** — each row: circular checkbox (tri-state: empty /
|
||
partially-selected count / checked), emoji glyph, category name, trailing
|
||
selected-count + chevron to expand into individual apps:
|
||
`All Apps & Categories, Social, Games, Entertainment, Creativity, Education,
|
||
Health & Fitness, Information & Reading, Productivity & Finance,
|
||
Shopping & Food`.
|
||
- **Search bar** pinned near bottom (with mic). Typing surfaces app matches
|
||
and website suggestions (e.g. typing "insta" offers `instagram.com`),
|
||
letting users add arbitrary domains.
|
||
- Footer: "**N Apps Selected**" caption + white pill "**Save**" (+ "Cancel").
|
||
|
||
> Implementation note: Opal ships its own app categorization. On iOS, third
|
||
> parties cannot enumerate installed apps; the system-sanctioned route is
|
||
> `FamilyActivityPicker` (FamilyControls), which provides its own
|
||
> category/app/website UI and returns opaque tokens. **v1 of Severed should
|
||
> embed `FamilyActivityPicker`** instead of cloning Opal's custom picker, and
|
||
> keep the `Block`/`Allow Only` segmented control as our own wrapper state.
|
||
|
||
---
|
||
|
||
## 4. Behavioral spec
|
||
|
||
1. **Activation** — a Schedule rule becomes active at `From` on an enabled
|
||
day and deactivates at `To` (windows crossing midnight, e.g. 22:00–06:00,
|
||
must be supported — Deep Sleep preset does this).
|
||
2. **While active** — the rule's app selection is shielded (and, when the
|
||
rule's Block Adult Content toggle is on, Screen Time's adult-website
|
||
filter is engaged for the same span); blocked apps also
|
||
surface in the "Blocked Apps" row on the Apps screen; the card turns green
|
||
with a "Xh left" pill.
|
||
3. **Unblocking** — with Hard Mode off, the user may unblock mid-window
|
||
("Unblocks allowed: Yes"). With Hard Mode on, no unblocks until the window
|
||
ends.
|
||
4. **Time-limit rules** — accumulate usage daily across the selected apps;
|
||
on crossing the threshold, shield until the `Until` reset point
|
||
(e.g. tomorrow), then reset the budget.
|
||
5. **Disable vs delete** — "Disable Rule" pauses scheduling but keeps the
|
||
rule (card presumably shows disabled state). No delete flow was shown;
|
||
add delete via swipe/long-press or a button in the editor.
|
||
6. **Commit friction** — creating/committing a rule uses press-and-hold
|
||
("Hold to Commit"), echoing Opal's philosophy that *starting* a commitment
|
||
should be deliberate. Editing uses a plain "Done".
|
||
7. **Live countdowns** — "Starts in 22h" / "6h left" update over time
|
||
(minute granularity is fine).
|
||
|
||
---
|
||
|
||
## 5. Implementation plan for Severed
|
||
|
||
### 5.1 Frameworks & capabilities
|
||
|
||
- **FamilyControls** — `AuthorizationCenter.shared.requestAuthorization(for: .individual)`;
|
||
`FamilyActivityPicker` + `FamilyActivitySelection` (app/category/web tokens).
|
||
- **ManagedSettings** — `ManagedSettingsStore` per rule
|
||
(`ManagedSettingsStore.Name("rule-<uuid>")`); set
|
||
`store.shield.applications` / `applicationCategories` / `webDomains`.
|
||
- **DeviceActivity** — `DeviceActivityCenter.startMonitoring` with a
|
||
`DeviceActivitySchedule(intervalStart:intervalEnd:repeats:)` per rule;
|
||
a **DeviceActivityMonitor app extension** applies/removes shields in
|
||
`intervalDidStart`/`intervalDidEnd`. Time-limit rules use
|
||
`DeviceActivityEvent(applications:threshold:)` +
|
||
`eventDidReachThreshold`.
|
||
- Requires the **Family Controls entitlement** (works in dev; distribution
|
||
needs Apple approval) and an App Group to share rule data with the monitor
|
||
extension.
|
||
|
||
### 5.2 Data model (SwiftData)
|
||
|
||
Replace the template `AppListProfile` with:
|
||
|
||
```swift
|
||
enum RuleKind: String, Codable { case schedule, timeLimit, openLimit }
|
||
enum SelectionMode: String, Codable { case block, allowOnly }
|
||
|
||
@Model final class BlockingRule {
|
||
var id: UUID
|
||
var name: String
|
||
var kind: RuleKind
|
||
var isEnabled: Bool
|
||
var hardMode: Bool
|
||
var blockAdultContent: Bool // webContent.blockedByFilter = .auto(...)
|
||
var selectionMode: SelectionMode
|
||
var selectionData: Data // encoded FamilyActivitySelection
|
||
var days: [Int] // 1...7, Calendar weekday numbers
|
||
// schedule
|
||
var startMinutes: Int // minutes from midnight, e.g. 540 = 09:00
|
||
var endMinutes: Int // may be < start (crosses midnight)
|
||
// time limit
|
||
var dailyLimitMinutes: Int?
|
||
var resetPoint: String? // "tomorrow" | "nextMorning" …
|
||
// open limit
|
||
var maxOpens: Int?
|
||
var createdAt: Date
|
||
}
|
||
```
|
||
|
||
`FamilyActivitySelection` is `Codable` → store as `Data`. Status
|
||
("active", "starts in Xh", "Xh left") is **derived**, not stored.
|
||
|
||
### 5.3 View inventory
|
||
|
||
| View | Notes |
|
||
|---|---|
|
||
| `AppsView` (tab) | Sections: Blocked Apps, Rules carousel, (later) app folders |
|
||
| `RuleCardView` | Card per §3.1, active/inactive styling |
|
||
| `RuleDetailSheet` | §3.2, rows + Edit Rule |
|
||
| `NewRuleSheet` | §3.3, type cards + preset gallery (`RulePreset` static data) |
|
||
| `ScheduleRuleEditor` | §3.4 |
|
||
| `TimeLimitRuleEditor` | §3.5 |
|
||
| `DayOfWeekPicker` | 7 circle toggles + summary ("Weekdays"/"Weekends"/…) |
|
||
| `AppSelectionView` | wraps `FamilyActivityPicker`, Block/Allow Only segmented control |
|
||
| `HoldToCommitButton` | long-press progress fill, haptics, fires on completion |
|
||
| `RuleScheduler` (service) | translates `BlockingRule` ⇄ DeviceActivity monitoring |
|
||
| `ShieldController` (service) | applies/clears `ManagedSettingsStore` shields |
|
||
|
||
### 5.4 Suggested build order
|
||
|
||
1. Data model + Apps tab with Rules section (cards from seeded sample rules,
|
||
status derivation, detail sheet) — pure UI, no entitlements needed.
|
||
2. New Rule sheet + Schedule editor + day picker + Hold to Commit (CRUD into
|
||
SwiftData; Disable/Done editing path).
|
||
3. FamilyControls authorization + `FamilyActivityPicker` integration
|
||
("Selected Apps" row, "N Apps" counts, icon clusters via `Label(token:)`).
|
||
4. DeviceActivity monitor extension + ManagedSettings shields (real blocking,
|
||
incl. midnight-crossing windows).
|
||
5. Time Limit editor + threshold events; Open Limit last (needs shield
|
||
action extension + open counting).
|
||
6. Preset gallery content + polish (gradients, photos, haptics, live
|
||
countdown timers).
|
||
|
||
### 5.5 Out of scope (seen in video, not part of "rules")
|
||
|
||
- Onboarding flow, paywall ("You know Opal works. Make it permanent."),
|
||
Home tab gem/score UI, Timer tab (one-off focus sessions, "Leave Early?"
|
||
friction screen), notification nudges ("Complete Your Setup").
|