Astradial iOS — Native UI Specification¶
Goal: a user who picks up the app must believe it is the iOS Phone app. This page is the screen-by-screen contract. Reference screenshots are from the stock iOS Phone app (iOS 26, "Liquid Glass" design language) on the founder's iPhone — the canonical source of truth when this text and a screenshot disagree is the current stock Phone app on the latest iOS.
Design-system ground rules¶
These apply to every screen. They are what makes the app feel native — getting layouts right but these wrong still reads as fake.
| Aspect | Rule |
|---|---|
| Framework | SwiftUI, iOS native controls only. No custom-drawn lookalikes where a system control exists. |
| Typography | System font (SF Pro) via SwiftUI text styles only (.largeTitle, .title, .body, .caption…). Never hard-code font names or sizes. |
| Icons | SF Symbols exclusively (star.fill, clock.fill, person.crop.circle.fill, circle.grid.3x3.fill, recordingtape, phone.fill, info.circle, mic.fill, plus, chevron.backward, line.3.horizontal.decrease). |
| Colors | Semantic system colors only (Color(.systemBackground), .secondary, .systemGray5, system green for the call button, system blue for selection/links, system red for missed calls). Automatic dark-mode support comes free — never hard-code hex. |
| Tab bar | Standard SwiftUI TabView with 5 tabs. On iOS 26 this renders as the floating "liquid glass" pill automatically — do not custom-build the pill. Order: Favourites, Recents, Contacts, Keypad, Voicemail. |
| Navigation | NavigationStack with large titles where the stock app uses them (Recents, Favourites, Voicemail). Contacts pushed detail uses inline title with back chevron. |
| Search | .searchable modifier → native search bar with the mic (dictation) affordance. |
| Haptics | System defaults; keypad presses use the same click feedback as the stock dialer (UIImpactFeedbackGenerator light / keyboard click sound per stock behaviour). |
| Animations | System defaults. No custom transition curves. |
| Accessibility | Dynamic Type, VoiceOver labels on all controls — the stock app supports these, so must we. |
Tab 1 — Keypad¶
Reference: screenshot IMG_6863 (stock keypad).
Layout, top to bottom:
- Line/identity chip (top-left): small rounded-square avatar with the account's initial + up/down chevrons. In the stock app this is the dual-SIM line selector; in Astradial it selects the SIP account / outbound identity when more than one account is configured. Hidden if only one account exists? No — stock shows it with one line too; match stock. Tapping presents the account picker sheet.
- Number display area: large, initially empty. As digits are entered they appear centred in large light type, shrinking as the number grows; an "Add Number" / matched-contact hint appears under it exactly as in stock. Backspace (
delete.backward) appears to the right of the number once non-empty. - Keypad grid: 4 rows × 3 columns of circular
systemGray5buttons. Digit large and centred; letters (ABC,DEF…) in small spaced caps below;0shows+beneath (long-press 0 →+);*and#have no sub-label. Long-press and multi-touch behave like stock. - Call button: large green circle, white
phone.fill, centred below the grid. Tap with empty number recalls the last dialled number into the display (stock behaviour).
Function mapping: digits feed liblinphone's outgoing-address builder; during an active call the same keypad sends DTMF (RFC 4733).
Tab 2 — Recents (call logs)¶
Reference: screenshot IMG_6865 (stock Recents).
- Header row:
Editbutton (left, glass capsule), All / Missed segmented pill (centre), filter icon (line.3.horizontal.decrease, right). - Large title
Recents, then a search bar (.searchable). - List rows, each:
- Avatar: contact photo, or initials in the system gradient circle, or generic person silhouette for unknown numbers.
- Line 1: contact name or raw number, bold. Missed calls in red.
- Line 2 (secondary): direction arrow (
arrow.down.leftincoming /arrow.up.rightoutgoing) + line badge (the small "H" square in stock = which line; for us, which SIP account) + number label (mobile,phone, or carrier/location string). - Right side: relative timestamp (
Yesterday,09:41, weekday, date — same formatting rules as stock) +info.circlebutton → pushes the call-detail / contact card. - Consecutive calls with the same party/direction collapse into one row with a
(3)count, as stock does.
- Edit mode: swipe-to-delete per row,
Edit→ select/delete,Clearall.
Data source: liblinphone CallLog history (core.callLogs), merged and rendered with the rules above. Missed-call filter = logs with status missed/declined.
Tab 3 — Contacts¶
Reference: screenshot IMG_6864 (stock Contacts).
- Nav bar: back chevron in glass circle (left), inline title
Contacts,+in glass circle (right) → native new-contact form. - Search bar with mic icon.
- My Card row: user photo + name +
My Cardsubtitle. - Alphabetical sections with sticky section headers and the A–Z index rail on the right edge.
- Rows: initials-avatar or photo + name with surname bolded (respect the iOS name-display setting).
- Contact detail: native contact card; phone numbers tap-to-call via Astradial (liblinphone), with the standard actions row (message, call, video, mail) — non-call actions may hand off to system apps.
Data source: the iOS address book (CNContactStore) exactly like stock, plus Linphone SIP-address contacts merged in (Linphone's Friend list / remote directory if configured). Permission prompt on first launch, with the standard limited-access flow on iOS 18+.
Tab 4 — Favourites¶
Superseded (June 2026): the Favourites tab was replaced by the MD Analytics tab (Apple Health-style dashboard) at Hari's request — see MD Analytics Dashboard. The spec below is kept in case favourites return as a sub-feature.
(No screenshot yet — match stock.)
- Large title
Favourites,+to add (picker over contacts → choose number + action),Editto reorder/remove. - Rows: avatar + name + label of the favourited number; tap = call immediately via Astradial.
Tab 5 — Voicemail¶
- Large title
Voicemail. Stock shows a list with inline playback (scrubber, speaker, delete) and transcription. - Astradial backend: Asterisk voicemail (
app_voicemail). v1 may ship as: list via VM APIs if/when exposed, or a "Call Voicemail" action dialling*97-style feature code while the full visual- voicemail backend is designed. UI must still be the native layout — an empty state ("No Voicemail") is acceptable for v1 if the backend isn't ready. Decision tracked in iOS App → Open questions.
In-call screen¶
Use CallKit's native in-call UI (as Linphone already does when CallKit is enabled). This gives the system call screen, lock-screen answering, call waiting, and the green status pill for free — the strongest possible "native" signal. The in-app call screen (reached by tapping the green pill) must mirror the stock layout: large name/number top-centre, call timer, 6-button grid (mute, keypad, speaker / add call, FaceTime-slot → hidden or transfer, contacts), red end-call button bottom-centre.
Onboarding — "Connect Your Line" (SIP line setup)¶
Reached after Firebase sign-in (owners then land on Analytics), and reused verbatim from Settings → Connect Line. Screen (OnboardingSIPView / OnboardingSIPQuickView, AstraCall/Screens/OnboardingUI.swift):
- Connect Type segmented control at the top: Quick (default) · Manual · Scan QR.
- Quick — a native, greyed search bar below the toggle, then a single rounded card containing all mobile-enabled SIP users, rows separated by hairline dividers (Contacts/Call-Logs style). Each row: initials
InitialsAvatar(blue-grey gradient) + name +Extension ####; the selected row shows a trailing blue checkmark (no ring/box). A fixed "Proceed with ####" solid-accent capsule floats pinned at the bottom (SolidCapsuleButtonStyle— opacity-only press feedback, never scales/shifts). Default selection = the signed-in user's own extension, else the first row. - Visibility gate: a user appears iff
sip_port='wss'and has anasterisk_endpoint— never gated on call routing. See Mobile App Access. - Manual — Username / Password / Domain / Transport + Connect. Scan QR — camera scan.
- Later (toolbar) or connecting a line dismisses the screen. Owners with a single line auto-select it; no password needed for Quick.
Settings¶
Native iOS grouped Form. Relevant sections:
- SIP Account: Status (Registered dot), Line, Server URL, and
Connect Line→Choose ›— a nav row pushing the "Connect Your Line" screen above; it auto-pops once a line registers. (The old inline "SIP Users" picker + Connect Type dropdown were removed.) - General →
Startup Page: picker for which tab opens on launch — Keypad · Tickets · Analytics. Role-aware default (owner/manager → Analytics, agent → Tickets, signed-out → Keypad), user-overridable, persisted device-locally (astradial_launch_page). - Accounts: current account row (name +
Org · Role, with a 🗑 trash to remove), other saved accounts (tap to switch), Add Account, and Sign Out. Sign Out is a full reset — clears Firebase, saved accounts (Keychain), all app prefs/caches, and returns to the first onboarding screen (fresh-install state).
What we deliberately do NOT carry over from Linphone's UI¶
Linphone 6's own SwiftUI UI (sidebar/assistant/chat/meetings styling, orange branding, its own tab structure) is entirely discarded. Keep the view-model ↔ core wiring where convenient, but no Linphone visual element may survive into Astradial builds: app icon, splash, colors, layouts, fonts, the assistant onboarding — all replaced. Provisioning is Astradial QR scan + manual entry only.
Acceptance test for "100% native"¶
Put the app side-by-side with the stock Phone app on the same iPhone:
- Same five tabs, same icons, same order, same selected-state look.
- Keypad digits, letter labels, spacing, and call-button geometry indistinguishable at a glance.
- Recents rows: same two-line structure, arrows, badges, timestamp wording, info button.
- Contacts: same search, My Card, section index rail, surname bolding.
- Dark mode, Dynamic Type (largest size), and VoiceOver all behave like stock.
- A failed item = a bug, not a nice-to-have.