Pattern · Lists and Feeds
NewNotification Inbox SwiftUI Block
An in-app notifications list: All, Mentions and Unread, day groups, inline Accept and Reply, swipe actions and Mark all as read.
The notifications list of a workspace, social or commerce app. Rows group into New, Earlier this week and Older, each with the actor's photo or initials, one sentence with the names in semibold, the time and an unread dot, and invites, mentions and shares carry inline buttons that show working, done and failed in place. Swipe or long-press to mark read, unread or delete, and Mark all as read clears every dot with a rolling count.
- SwiftUI
- iOS 17.0+
- notification-inbox
$199 once, every piece included. See how installing works · Sign in
01
What's included
SwiftUI source you own, built on Apple frameworks only.
- All, Mentions and Unread with the unread count
- New, Earlier this week and Older groups on calm cards
- Accept, Decline, Reply and View with per-row working, done and failed
- Swipe or long-press to mark read, unread or delete
- Mark all as read, empty states per filter and loading shapes
02
Implementation notes
How it behaves, what it handles, and the decisions behind it.
A filter at the top switches between All, Mentions and Unread, the Unread segment carrying its count, and a line under it reads "3 unread" beside Mark all as read. Notifications group into New (the last 24 hours), Earlier this week and Older, each on a surface card. A row leads with the actor's photo or initials, or a symbol for notices from the app, then one sentence with the actor and the subject in semibold ("Maya Chen invited you to Design review"), an optional preview line, the time ("12m", "3h", "Tue", "Oct 12") and an accent dot while it is unread; read rows step their sentence back to the secondary ink. Invites carry Accept and Decline, mentions Reply and shares View: the pressed button shows a spinner while your closure runs and a checkmark when it returns, then Accept and Decline settle into the resolution your model sets ("You're going"); a thrown error shows under the row. Swiping a row left reveals Read or Unread and Delete, and a long press offers the same menu. Mark all as read changes every row at once, rolls the count down to "All caught up" and puts things back with an error line if your closure throws. Each filter has its own empty state ("You're all caught up" under Unread), and passing nil shows grey shapes while the inbox loads.
Notes
NotificationInbox(notifications:initialFilter:onAction:onMarkRead:onDelete:onOpen:)takes the inbox and two required closures. It is a section: no navigation stack, background, padding or scroll view of its own, so place it in your ownScrollViewcolumn with.padding(.horizontal, 20).NotificationInbox.Itemholds anid, aKind(.mention,.reply,.invite,.share,.reaction,.follow,.system), theactor, themessagebetween actor and subject ("invited you to"), thesubject, an optionalpreview, thedate,isRead, the inlineactions, an optionalresolutionand an optionalavatar(.image(URL)throughAsyncImage, or.symbol(name)). Without an avatar the row shows the actor's initials, or the kind's symbol when there is no actor.Item.samplesis the inbox from the preview.- Mentions and replies show under the Mentions filter. A row stays under Unread while its action is running, so it doesn't vanish mid-press.
onAction(item, action)isasync throwsand runs for.accept,.decline,.replyand.view. Accept is the row's one accent button; the others sit onfill. Accept and Decline show a checkmark for a moment, then the row showsitem.resolutionor, until you set one, "Accepted" or "Declined". Reply and View return to rest when the closure returns, so navigate from inside it.CancellationErrorbacks out quietly; any other error shows itserrorDescription(or "That didn't go through. Try again.") under the row.onMarkRead(ids, read)covers a single row (swipe, menu or tap) and Mark all as read. The block shows the change before the closure returns and keeps it until your model agrees; a thrown error puts the old state back and says so.onDeleteremoves the row at once and brings it back with an error if it throws. Leave it out to hide Delete from the swipe and the menu.onOpenmakes rows tappable: the block marks the row read and calls you to open what it points at. A tap on a row that is swiped open closes it instead.- The swipe only claims a drag that is clearly horizontal, so the host's scroll view keeps scrolling.
- Haptics mark a selection when the filter changes, success when an action or Mark all finishes, and an error when one fails.
- VoiceOver reads each row as one sentence with "Unread", the preview and a relative time ("12 minutes ago"), with Mark as read or unread and Delete as actions; results are announced. At accessibility sizes the avatar steps aside and previews no longer truncate.
- Reduce Motion turns every change into a short fade.
NotificationInboxExampleat the end ofNotificationInbox.swiftis the Notifications screen in the preview: a team workspace's inbox where accepting the invite settles on "You're going" and Mark all as read clears every dot. PasswrapsInNavigationStack: falseto push it in your own stack.
Theming
- Reads
SPThemefrom@Environment(\.spTheme)and requires thedesign-systemitem. - Cards are
color.surfacewithradius.lg,SPHairlinedividers inseparatorinset to the text, and groups 24 points apart (space.xl). Group titles aretype.headline. - The filter is a
filltrack atradius.smwith the selected segment onsurfaceElevated. - Sentences are
type.subheadwith names in semiboldtextPrimary; previews and times aretype.footnoteintextSecondaryandtextTertiary. Avatars are 40 point circles onfillwithtextSecondaryinitials or symbols. - The unread dot and Accept are
accent(Accept withaccentOntext); other inline buttons arefillwithtextPrimary. Swipe actions areneutralanddangerwithstatusOn. Errors aredanger. - Changes animate with
motion.smoothandmotion.snappy, and become short fades under Reduce Motion.
03
Keep building
Pieces that work well next to this one.