Pattern · AI Chat

New

Chat History SwiftUI Block

Past assistant chats: live search, Pinned and date groups, swipe and context actions, inline rename and delete that confirms.

The list of past conversations. A search field filters by title and last line as you type, and chats sit under Pinned, Today, Yesterday, Previous 7 days and Earlier. Swipe a row for Pin and Delete or hold it for Rename, Pin, Share and Delete; rename edits the title in place, and delete confirms, fades the row while your closure runs and restores it with the error if it fails.

  • SwiftUI
  • iOS 17.0+
  • chat-history
Get SwiftPieces Pro

$199 once, every piece included. See how installing works · Sign in

01

What's included

SwiftUI source you own, built on Apple frameworks only.

  • Live search by title and last line, with a no-results state
  • Pinned, Today, Yesterday, Previous 7 days and Earlier
  • Swipe for Pin and Delete, or hold for Rename, Pin, Share and Delete
  • Rename in place; Delete confirms, fades the row, and restores it on failure
  • No chats yet with New chat, and ghost rows while loading

02

Implementation notes

How it behaves, what it handles, and the decisions behind it.

A search field filters the list as you type, by title and by last line, and says so when nothing matches. Pinned chats come first, then Today, Yesterday, Previous 7 days and Earlier, each a card of rows with the title, a one-line snippet, when the chat was last active and, when pinned, a pin. Swipe a row left for Pin and Delete, or press and hold it for Rename, Pin, Share and Delete. Pin moves the row into Pinned at once and runs your closure; Rename turns the title into a field in place and saves it when you press Done; Delete asks first, then fades the row while your closure runs and removes it, or brings it back with the error under it if it throws. With no chats at all the block shows No chats yet and New chat, and isLoading draws grey rows in the list's layout.

Notes

  • ChatHistoryBlock(conversations:isLoading:onOpen:onNewChat:onRename:onPin:onDelete:) takes a binding to every chat, in any order. It is a section: no navigation stack, background, padding or scroll view of its own.
  • ChatHistoryBlock.Conversation has an id, title, snippet (the last line), updated (sets its group and time), isPinned and an optional shareURL, which adds Share to the menu. Conversation.sample is the nine chats from the preview.
  • onOpen runs when a row is tapped. A tap on any row while one is swiped open closes it instead.
  • onRename saves a new title. The title changes at once; throw and the old title comes back with the error under the row. Empty or unchanged titles are ignored.
  • onPin saves a pin change after the row has moved. Throw to move it back.
  • onDelete runs after the person confirms in a dialog ("This can't be undone."). The row fades to 40% with a spinner until it returns, then leaves the list. Throw to bring it back with the error's errorDescription, or "Couldn't delete this chat. Try again." Leave it out to hide Delete everywhere.
  • onNewChat starts a chat from the empty state. Leave it out to hide New chat.
  • Times read the time for today and yesterday, the weekday within a week, else the date.
  • Haptics mark a pin, a finished delete and any failure.
  • VoiceOver reads each row as its title (with "pinned") and its time and snippet, offers Pin, Rename and Delete as actions, and announces results. Rows grow with Dynamic Type.
  • Reduce Motion turns the swipe and every move into short fades.
  • ChatHistoryBlockExample at the end of ChatHistoryBlock.swift is the Chats screen from the preview, with stubs that take a moment and succeed. Pass wrapsInNavigationStack: false to push it in your own stack.

Theming

  • Reads SPTheme from @Environment(\.spTheme) and requires the design-system item.
  • The search field is 52 points on fill at radius.md with a separator stroke that turns textPrimary when focused.
  • Group titles are type.subhead semibold in textSecondary. Cards are color.surface at radius.lg with SPHairline dividers inset to the text; groups sit 24 points apart.
  • Titles are type.body in textPrimary, snippets type.subhead in textSecondary, times type.footnote and the pin type.caption in textTertiary.
  • Swipe actions are neutral (Pin) and danger (Delete) with statusOn labels. Errors are danger. New chat in the empty state is the one accent.
  • Moves use motion.smooth and presses motion.snappy.