Created
July 31, 2026 08:38
-
-
Save lanserxt/d9c9a8d896cf0fee183250df973f74d8 to your computer and use it in GitHub Desktop.
iOS 27: Suggested Actions for chat messages with dates and locations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // ChatMessage.swift | |
| // WWDC25Demo | |
| // | |
| // Created by Anton Gubarenko on 22.07.2026. | |
| // | |
| import Foundation | |
| import SwiftUI | |
| import SuggestedActions | |
| struct ChatMessage: Identifiable { | |
| let id: UUID | |
| let sender: Participant | |
| let text: String | |
| let imageName: String? | |
| let cinemaLocation: CinemaLocation? | |
| let date: Date | |
| struct Participant: Hashable { | |
| let name: String | |
| let handle: String | |
| let isCurrentUser: Bool | |
| } | |
| struct CinemaLocation: Hashable { | |
| let name: String | |
| let address: String | |
| let distance: String | |
| let showtime: String | |
| } | |
| @available(iOS 27.0, *) | |
| var suggestedActionsMessage: SuggestedActionsMessage { | |
| SuggestedActionsMessage( | |
| id: id, | |
| date: date, | |
| subject: nil, | |
| body: AttributedString(text), | |
| sender: .init( | |
| name: sender.name, | |
| handle: sender.handle, | |
| isUser: sender.isCurrentUser | |
| ), | |
| recipients: [] | |
| ) | |
| } | |
| } | |
| // MARK: - Demo Data | |
| extension ChatMessage { | |
| static let anton = Participant( | |
| name: "Anton", | |
| handle: "anton@example.com", | |
| isCurrentUser: true | |
| ) | |
| static let maya = Participant( | |
| name: "Maya", | |
| handle: "maya@example.com", | |
| isCurrentUser: false | |
| ) | |
| static let demo: [ChatMessage] = [ | |
| ChatMessage( | |
| id: UUID(), | |
| sender: maya, | |
| text: "These are the movies showing this weekend.", | |
| imageName: "movie-posters", | |
| cinemaLocation: nil, | |
| date: .now.addingTimeInterval(-300) | |
| ), | |
| ChatMessage( | |
| id: UUID(), | |
| sender: anton, | |
| text: "Let’s watch The Last Horizon on Saturday.", | |
| imageName: nil, | |
| cinemaLocation: nil, | |
| date: .now.addingTimeInterval(-240) | |
| ), | |
| ChatMessage( | |
| id: UUID(), | |
| sender: maya, | |
| text: "The 19:30 screening at Central Cinema works for me.", | |
| imageName: nil, | |
| cinemaLocation: CinemaLocation( | |
| name: "Central Cinema", | |
| address: "18 Market Street", | |
| distance: "1.2 km away", | |
| showtime: "Saturday, 19:30" | |
| ), | |
| date: .now.addingTimeInterval(-180) | |
| ), | |
| ChatMessage( | |
| id: UUID(), | |
| sender: anton, | |
| text: "Great. Let’s meet there. Will add to notes to bring your favourite popcorn!", | |
| imageName: nil, | |
| cinemaLocation: nil, | |
| date: .now.addingTimeInterval(-120) | |
| ) | |
| ] | |
| } | |
| // MARK: - Message Cell | |
| struct MessageCell: View { | |
| let message: ChatMessage | |
| let previousMessages: [ChatMessage] | |
| var body: some View { | |
| VStack( | |
| alignment: message.sender.isCurrentUser | |
| ? .trailing | |
| : .leading, | |
| spacing: 8 | |
| ) { | |
| if let imageName = message.imageName { | |
| Image(imageName) | |
| .resizable() | |
| .scaledToFill() | |
| .frame(width: 240, height: 150) | |
| .clipShape( | |
| RoundedRectangle(cornerRadius: 18) | |
| ) | |
| } | |
| Text(message.text) | |
| .padding(.horizontal, 14) | |
| .padding(.vertical, 10) | |
| .background( | |
| message.sender.isCurrentUser | |
| ? Color.accentColor | |
| : Color.secondary.opacity(0.15) | |
| ) | |
| .foregroundStyle( | |
| message.sender.isCurrentUser | |
| ? .white | |
| : .primary | |
| ) | |
| .clipShape( | |
| RoundedRectangle(cornerRadius: 18) | |
| ) | |
| if let cinemaLocation = message.cinemaLocation { | |
| CinemaLocationCell( | |
| location: cinemaLocation, | |
| isCurrentUser: message.sender.isCurrentUser | |
| ) | |
| } | |
| if #available(iOS 27.0, *) { | |
| SuggestedActionsView( | |
| message: message.suggestedActionsMessage, | |
| previousMessages: previousMessages | |
| .suffix( | |
| SuggestedActionsMessage.previousMessagesLimit | |
| ) | |
| .map(\.suggestedActionsMessage) | |
| ) | |
| .buttonBorderShape(.capsule) | |
| .font(.callout) | |
| } | |
| } | |
| .frame( | |
| maxWidth: .infinity, | |
| alignment: message.sender.isCurrentUser | |
| ? .trailing | |
| : .leading | |
| ) | |
| } | |
| } | |
| private struct CinemaLocationCell: View { | |
| let location: ChatMessage.CinemaLocation | |
| let isCurrentUser: Bool | |
| var body: some View { | |
| HStack(alignment: .top, spacing: 12) { | |
| Image(systemName: "popcorn.fill") | |
| .font(.title3) | |
| .foregroundStyle(.white) | |
| .frame(width: 42, height: 42) | |
| .background(Color.red) | |
| .clipShape(RoundedRectangle(cornerRadius: 12)) | |
| VStack(alignment: .leading, spacing: 6) { | |
| Text(location.name) | |
| .font(.headline) | |
| Text(location.address) | |
| .font(.subheadline) | |
| .foregroundStyle(.secondary) | |
| HStack(spacing: 12) { | |
| Label(location.showtime, systemImage: "clock") | |
| Label(location.distance, systemImage: "location") | |
| } | |
| .font(.caption) | |
| .foregroundStyle(.secondary) | |
| } | |
| } | |
| .padding(12) | |
| .frame(width: 280, alignment: .leading) | |
| .background( | |
| isCurrentUser | |
| ? Color.accentColor.opacity(0.16) | |
| : Color.secondary.opacity(0.12) | |
| ) | |
| .clipShape(RoundedRectangle(cornerRadius: 18)) | |
| } | |
| } | |
| // MARK: - Chat Screen | |
| struct MovieChatView: View { | |
| private let messages = ChatMessage.demo | |
| var body: some View { | |
| NavigationStack { | |
| ScrollView { | |
| LazyVStack(spacing: 16) { | |
| ForEach( | |
| Array(messages.enumerated()), | |
| id: \.element.id | |
| ) { index, message in | |
| MessageCell( | |
| message: message, | |
| previousMessages: Array( | |
| messages.prefix(index) | |
| ) | |
| ) | |
| } | |
| } | |
| .padding() | |
| } | |
| .navigationTitle("Movie Night Chat") | |
| } | |
| } | |
| } | |
| #Preview { | |
| MovieChatView() | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment