This guide defines the strict domain-in-library, adapters-in-targets
architecture for Cookle.
Related documents:
| Layer | Owns | Must not own |
|---|---|---|
Domain (CookleLibrary) |
SwiftData schema, public *Operations facades, internal mutation/query collaborators, predicates, route helpers, validation, canonical mutations, canonical search, mutation effect hints |
Widget reloads, notification registration, review prompts, deep-link delivery, App Intent result shaping, SwiftUI presentation state |
Adapter (Cookle, Widgets, Watch, App Intents) |
Parameter parsing, platform API calls, dependency wiring, route intake, follow-up orchestration after shared mutations, App Intent result mapping | Re-implementing recipe, diary, tag, or reset mutation rules |
| View (SwiftUI) | Focus state, sheets, dialogs, navigation state, screen-scoped @Observable models, display formatting, view composition |
Canonical business validation, mutation rules, notification scheduling, widget reload coordination |
Cookle follows the Incomes source-layout direction by making architecture
areas visible in paths instead of keeping reusable app code in a broad
Common folder.
Cookle/Sources/Appcontains app entry points, app-level App Intents, app-wide support types, and generic mutation workflow adapters.Cookle/Sources/Featurescontains product feature surfaces such as recipe, diary, photo, tag, search, settings, notifications, debug, and main navigation.Cookle/Sources/Platformcontains app-side Apple framework and package integration such as runtime assembly, app group route storage, WidgetKit reloads, WatchConnectivity delivery, logging, monetization, and image processing.Cookle/Sources/SharedUIcontains reusable app-target UI components, modifiers, styles, navigation environment helpers, text support, and TipKit definitions.CookleLibrary/Sources/<Capability>contains shared-library capabilities such asRecipe,Diary,Photo,Tag,Navigation,Persistence,Preferences,Mutation,DataManagement,Notification,CookingSession, andWidgets.Widgets/Sources/Appcontains widget entry wiring, whileWidgets/Sources/Featurescontains individual widget implementations.Watch/Sources/Appcontains watch app entry wiring,Watch/Sources/Featurescontains watch UI, andWatch/Sources/Platformcontains WatchConnectivity transport.CookleLibrary/Tests/Default/<Capability>mirrors the shared-library capability split.
Cookle adopts shared packages by target responsibility, not by superficial symmetry with sibling apps.
Cookleis the full-appMHPlatformadopter because the app target owns runtime bootstrap, ads, StoreKit, license presentation, review flow, mutation follow-up, and route delivery wiring.CookleLibraryadoptsMHPlatformCorefor core-safe route, preference, persistence-maintenance, and logging contracts. It must not depend onMHPlatform,MHAppRuntime, app-runtime split products, MHUI, or MHDesign.Cookleadopts fullMHUIon2.3.0..<3.0.0for its root theme and selected presentation primitives. It applies the neutral standard theme and its native appearance once at startup, preserving the app-owned orange accent as the native control tint. Existing metrics use MHUI's MHDesign re-export, without a separate direct MHDesign product link. ADR 0009 records the accepted main-app presentation boundary.Widgets,Watch, and App Intents call Cookle shared APIs first. They stay off app-runtime umbrellas. The companion presentation evaluation in ADR 0010 retains native WidgetKit composition and defers Watch MHUI adoption because its standard surface failed the watchOS readability comparison.- Cookle does not keep a generic utility package dependency. Small app-owned helper behavior stays local, while generic utilities and thin host-app presentation shortcuts remain outside MHUI.
Recipe Detail places the photo, compact facts, and cooking entry before the ingredients and unframed instructions. MHUI supplies screen spacing, reading headers, grouped rows, adaptive ingredient values, and semantic action styles. Dates and secondary actions occupy the closing area. Cookle retains content, routes, action handlers, and native presentations. Shared recipe sections default to native presentation for Intent snippets.
Recipe browsing, the Diary landing screen, Search results, cooking, and the photo collection use MHUI screen composition. Search keeps the system searchable field and activation behavior. Editing, Diary detail and selection, tags, settings, photo metadata, and diagnostics use native List and Form chrome where the container supplies selection, swipe actions, fields, focus, keyboard behavior, or another concrete system interaction benefit. Native controls can otherwise live within app-owned MHUI composition. Detached recipe editors use input chrome on their native keyboard-resizing canvas. Cooking presents one progress indicator and an unframed current instruction, followed by step navigation and the grouped timer controls. Standard text sizes retain native paging; accessibility sizes use natural-height text in the screen scroll view and return to the instruction after a step change. Media, system, and package-owned presentations retain their native appearance. Core app journeys use MHUI presentation primitives where they improve a product-owned surface. Specialized and less-traveled system surfaces inherit the root theme while retaining native containers and controls rather than forcing an MHUI treatment that obscures platform behavior. Ordinary content actions retain MHUI's non-glass default. Liquid Glass is an explicit opt-in reserved for a bounded floating functional layer. The main-app rollout record documents these choices and their verification limits. App-owned composition and behavior remain local; MHUI is a presentation dependency, not a home for business logic or generic utilities.
- Keep repository-owned unit tests in
CookleLibrary/Tests/Default. - Do not maintain a separate unit test target for
Cookle,Widgets, orWatch. - App-owned adapters should stay responsibility-thin enough to verify through
Cooklebuilds plusCookleLibrarytest coverage. - If an adapter or screen model needs durable coverage, first move the reusable
rule into
CookleLibraryand test it there instead of growing target-local test suites.
Allowed in views:
- focus and keyboard behavior
- sheet, alert, and navigation presentation
- small screen-scoped
@Observablemodels owned by the root view - display-only formatting
Not allowed in views:
- re-implementing recipe, diary, tag, or reset mutation rules
- direct notification synchronization or widget reload orchestration
- App Intent-only success semantics
- review prompting decisions that belong in adapters
When a screen grows beyond trivial local state, keep a screen-scoped
@Observable model in the root view's @State and pass it with @Bindable.
Current examples:
Cookle/Sources/Features/Main/State/MainNavigationRouter.swiftCookle/Sources/Features/Recipe/Models/RecipeFormModel.swiftCookle/Sources/Features/Recipe/Services/RecipeFormSaveCoordinator.swiftCookle/Sources/Features/Diary/Models/DiaryFormModel.swiftCookle/Sources/Features/Diary/Services/DiaryFormSaveCoordinator.swiftCookle/Sources/Features/Settings/Models/SettingsScreenModel.swift
Prefer this over ObservableObject, EnvironmentObject, or pushing
screen-local sequencing into a broader router.
View or App Intent -> app adapter/service -> CookleLibrary Operations ->
MutationOutcome<Value> -> app-side follow-up
The current app-side mutation adapters are:
Cookle/Sources/Features/Recipe/Services/RecipeActionService.swiftCookle/Sources/Features/Diary/Services/DiaryActionService.swiftCookle/Sources/Features/Photo/Services/PhotoActionService.swiftCookle/Sources/Features/Tag/Services/TagActionService.swiftCookle/Sources/Features/Settings/Services/SettingsActionService.swift
Those adapters may coordinate widget reloads, notification refreshes, and
review prompting after a shared mutation succeeds, but the mutation rules and
effect hints belong in CookleLibrary.
App Intents are adapters, not a second domain layer.
Preferred flow:
App Intent parameter parsing -> same app adapter/service ->
same CookleLibrary Operations API
App Intent files may:
- resolve entities and parameters
- convert domain and adapter failures into intent-facing errors
- return dialogs, values, and route-based navigation
App Intent files must not:
- become the only implementation of a user-facing mutation
- return success results after blocking preflight or primary mutation failures
- duplicate shared validation, search, or mutation branching
Shared mutations express follow-up hints through:
CookleLibrary/Sources/Mutation/MutationOutcome.swiftCookleLibrary/Sources/Mutation/MutationEffect.swift
Current effect hints are:
recipeDataChangeddiaryDataChangednotificationPlanChangedreviewPromptEligible
reviewPromptEligible stays app-owned. CookleLibrary returns domain-owned
effects, and app adapters may append review eligibility when the initiating
surface should attempt it.
Adapter-owned mutation and destructive-reset paths must classify failures by phase instead of relying on assertions or success sentinel values.
- preflight and primary mutation failures block success and must stay visible to the current caller
- UI flows must keep the current form or destructive confirmation context on blocking failures
- App Intents must throw on blocking failures instead of returning success dialogs
- post-commit follow-up failures are degraded-success cases and must not claim that the committed mutation was rolled back
See ADR 0005 for the repository-level contract.
Keep in CookleLibrary:
@Modeltypes- predicates and
FetchDescriptorbuilders - public Operations facades and internal mutation or validation collaborators
- route parsing and execution helpers that are reusable across surfaces
Keep in target adapters:
ModelContainerconstruction- iCloud enablement policy
MHAppRuntimeBootstrapandMHAppRoutePipelineassemblyUNUserNotificationCenterintegration- WidgetKit reload coordination
- WatchConnectivity snapshot delivery and watch companion interaction state
- review prompt orchestration
API style decision:
- Keep accepting
ModelContextin library APIs. - Rationale: Cookle is already
SwiftDataand@Querycentered, and the current migration goal is clearer boundaries rather than a persistence actor rewrite.
-
Route assembly should stay separate from navigation state mutation. Files:
Cookle/Sources/Features/Main/Services/MainRouteService.swiftCookle/Sources/Features/Main/State/MainNavigationRouter.swiftMinimal plan:- keep
MainRouteServicefocused on pipeline assembly, parsing, and inbox sources - keep
MainNavigationRouterfocused on navigation state application
-
Form screens should keep state in screen models instead of large view-local mutation code. Files:
Cookle/Sources/Features/Recipe/Views/RecipeFormView.swiftCookle/Sources/Features/Diary/Views/DiaryFormView.swiftCookle/Sources/Features/Settings/Views/SettingsSidebarView.swiftMinimal plan:- keep views focused on UI composition and error presentation
- keep form state, tip priority, and save sequencing in dedicated models and coordinators
-
Mutation follow-up hints must stay shared while platform side effects stay app-owned. Files:
CookleLibrary/Sources/Recipe/RecipeFormOperations.swiftCookleLibrary/Sources/Diary/DiaryOperations.swiftCookleLibrary/Sources/Tag/TagOperations.swiftCookleLibrary/Sources/DataManagement/DataMaintenanceOperations.swiftMinimal plan:- keep effect-hint decisions in
CookleLibrary - keep notification sync, widget reload, and review flow wiring in app-side adapters
-
Notification route delivery should stay adapter-owned without duplicating route meaning. Files:
Cookle/Sources/Features/Notification/Services/NotificationService.swiftCookleLibrary/Sources/Navigation/*Minimal plan:- keep payload decoding and delivery in notification adapters
- keep route vocabulary and parsing shared in
CookleLibrary
Cooking on iPhone and iPad is reading-first: show the surrounding numbered steps together, with materials alongside them when space permits. Selecting a step supplies timer and Watch context; it is not a completion checkbox or a requirement to reveal the next step. A focused presentation can still be useful on constrained surfaces such as Apple Watch. Evaluate each surface separately.
Keep screen-only state in memory: the material list copied when a cooking view opens, pending Diary prefill, presentation flags, and unsaved quick-registration input. Do not add a persisted cooked status or per-step completion flags for these interactions. The material copy may differ from an older resumed step snapshot; closing and reopening the view refreshes materials from the recipe. It is not a new cross-device material snapshot contract.
Cooking session state remains separate from SwiftData and persists in UserDefaults on both devices before delivery through WatchConnectivity. Session identities combine an installation identifier and a start sequence; logical revisions order edits without relying on device clocks. Retirement records prevent delayed active updates from reviving an ended session. Ending a session also clears its timer.
Independently started sessions require an explicit keep-or-switch choice. Choice records resolve concurrent decisions deterministically without treating both sessions as ended. An explicit end still takes precedence over a choice. The shared reducer owns these rules; phone and Watch adapters own persistence, delivery, and presentation. A locally saved legacy snapshot can be recovered, but legacy peer payloads cannot participate in the new synchronization protocol. Both devices must run compatible versions for session synchronization.
The phone also maintains a recent-recipe history and sends up to five usable recipes for offline Watch starts. This bounded cache contains recipe identifiers, titles, and complete steps, with a 24 KB encoded catalog limit; oversized recipes are omitted rather than truncated. Opening a recipe, saving local changes, returning to the foreground, or activating WatchConnectivity refreshes the catalog. Delivery is eventual, and the Watch labels these recipes as saved copies. Starting from a copy is explicit. Updating the catalog does not replace the steps of an active session. One composed application context carries the catalog and session state so sending either cannot discard the other. Neither this cache nor session synchronization changes the SwiftData schema or backup format.
Registering a recipe from Diary selection is an explicit independent save. Before saving, explain that the recipe survives cancellation of the later Diary. Registration cancellation creates nothing. The post-cooking Diary form also keeps its unsaved draft in memory so it cannot overwrite or clear a separately saved Diary draft. Ordinary Diary entry retains its existing draft persistence; an explicit Diary save still creates the normal database record.
Ordinary launch and the home route open Diary. Diary is the place to record and revisit meals; its presentation can evolve without changing that entry policy. Do not persist a last-selected tab as a side effect of visual improvements.
Explicit destinations take precedence over the ordinary launch default:
| Entry | Destination and behavior |
|---|---|
| Recipe tab, recipe-list link | Browse saved recipes without creating a Diary |
| Recipe detail link, Recipe Widget, Open Recipe intent | Open the requested recipe |
| Cooking view | Read steps; optionally end and review a Diary draft |
| Diary tab, Diary link or Widget | Open the Diary list or requested entry |
| Photo tab or photo link | Browse saved photos or open the requested photo |
| Search or tag link | Open search or the requested category/ingredient |
| Settings link | Open the requested settings destination |
Photos remains a browsing surface. Its recipe-creation entry must say that it adds a recipe; a name-only recipe can be enriched with photos later. Independent photo capture and attach-later flows need a separate product decision.
Keep contextual recipe discovery and optional Diary recording available without requiring either activity before the other. Route vocabulary and execution stay in CookleLibrary; MainNavigationRouter selects the app destination. Changes to system entry behavior require their own compatibility verification.
The Diary landing screen separates today's saved record, recipe inspiration, and chronological history. No record does not mean a meal is undecided, and a recipe candidate is not evidence of a meal eaten. Opening a candidate navigates to Recipe; only an explicit Diary save records a meal.
Candidates are a presentation window of up to three saved recipes, ordered by modification date descending and name. Other Ideas advances the window; its position stays in memory and resets when the ordered identifiers change. There is no inferred nutrition, ingredient availability, meal plan, or cooked status.
Today uses the current calendar and refreshes on appearance, app activation, and the system significant-time-change notification (including midnight). Past records appear once in reverse-chronological month groups, so the newest entry naturally leads the history without a duplicate recent section. Future-dated records remain accessible in those groups. Existing draft restoration and explicit save behavior stay in the normal Diary form.