This tutorial will focus on designing the elements that make up the ChannelListView and ChatsView components. In parts two and three, you’ll build on these components by adding interactions and animations, creating a more seamless chat experience.
Resources
You can download the Xcode project containing the SwiftUI source code from GitHub to follow along.
The project contains a ZIP file with several Swift files and folders for the layouts, interactions, and animations for this project. Additionally, you will use SF Symbols 3 as icons for the project. You can find all other assets, such as images and custom icons, in the Xcode assets folder.
Getting Started
Before getting started, download the latest version of Xcode (this tutorial uses v13.0). If you haven’t installed Xcode, download it at Xcode 13 (recommended) or download it from the App Store:
- Launch the Mac App Store.
- Search for Xcode. If you haven’t installed it, you will see a DOWNLOAD button instead of an UPDATE button.
- After downloading Xcode, move it to the Applications folder and follow the instructions to install.
Once you've installed Xcode, you're ready to create a project!
Want to integrate SwiftUI into your project? Check out our new SwiftUI SDK to get started.
Create a New Xcode (SwiftUI) Project
To create a project, launch Xcode:
- From the welcome screen, select Create a new Xcode project.
- In the Application window, select App and then Next.
- Enter the Project Name, then select Next.
- Select a location on your computer where you want to store the project files, then click Create.
After creating the Xcode project and adding your new folders and files, the folder and files structure should look similar to the image below.
In Xcode, you can use new files and folders to organize sections of your designs or compositions in the same way you use groups and layers in design tools.
To add a new file or folder to your project:
- Press Control on the keyboard and click the project folder.
- Then, select New File or New Group.
In the image below, you can see several folders and files have been added to the project to organize various sections of the app screens.
Creating the Navigation Bars and Compose Area
The following sections will show you how to design the header at the top of the screen (HeaderView), a compose area where users can input messages before sending (ComposeAreaView), and the tab bar at the bottom of the screen (TabBarView).
Create the Header View
SwiftUI is a declarative language, which means you describe what and how the interface should be. Then, the SwiftUI system presents the visual representation of what you’ve described.
To create your HeaderView, create a new Swift file HeaderView by right-clicking the folder ChannelList and selecting New File....
Then, enter the code below:
1234567891011121314151617import SwiftUI struct HeaderView: View { var body: some View { // Channel List Header HStack { // Horizontal parent container Image("hamburger") Spacer() Text("Stream Chat") .font(.headline) .fontWeight(.bold) Spacer() Image("start-conversation-icon") } } }
In this snippet, you’re telling SwiftUI to present a horizontal parent container (HStack) consisting of an Image on the left, Text in the middle, a space between the content (Spacer), and another Image on the right.
By resuming the Xcode preview, you will see the following header image:
Note: You can find the hamburger and start conversation icons in the assets folder. You may also upload your own icons to the Xcode assets folder and use them.
Create the Compose Area
The compose area is made up of a TextField, which allows users to input a message. It also has attachment and send buttons.
To create the compose area, create a file called ComposeAreaView and add the following code:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758import SwiftUI struct ComposeAreaView: View { let composeAreaBackground = Color( colorLiteral(red: 0.07058823529, green: 0.07843137255, blue: 0.0862745098, alpha: 1)) let darkDisabled = Color( colorLiteral(red: 0.2980392157, green: 0.3215686275, blue: 0.3607843137, alpha: 1)) @State private var composeText = "Send a message" var body: some View { ZStack(alignment: .top) { HStack { Button { print("Attachment/Paperclip") } label: { Image(systemName: "paperclip") .renderingMode(.original) .foregroundColor(darkDisabled) } Button { print("Bolt") } label: { Image(systemName: "bolt.fill") .foregroundColor(darkDisabled) } TextField("Send a message", text: $composeText) .font(.footnote) .foregroundColor(.secondary) .padding( EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 0)) .background(composeAreaBackground) .cornerRadius(20) .padding() Button { print("Send") } label: { Image(systemName:"arrow.forward.circle.fill") .renderingMode(.original) .foregroundColor(darkDisabled) } } // Compose Area .padding() .frame(width: .infinity, height: 75) Rectangle() .frame(width: .infinity, height: 1) .foregroundColor(Color(.systemGray3)) } } }
In this snippet, you:
- Define the color constant composeAreaBackgroud and, as the variable implies, use it for the background of the compose area.
- Define the constant darkDisabled for the background of the send button when the text field is empty.
- Define the state variable composeText and use it as a value for the text parameter of the textField, which will display a placeholder text in the text field.
- Create the paper clip, lightning, and send (arrow) icons as buttons using San Francisco Symbols (SF), and use a horizontal container to arrange them.
Create the Tab Bar
The tab bar appears at the bottom of the screen and allows users to switch between pages within the application.
To create the tab bar, add a new Swift file called TabBarView and enter the following code:
12345678910111213141516171819202122232425import SwiftUI struct TabBarView: View { var body: some View { ZStack { TabView { if #available(iOS 15.0, *) { MentionsView() .tabItem { Label("Chats", image: "message_icon") } .badge(82) MentionsView() .tabItem { Label("Mentions", systemImage: "at") }.badge(1) } else { // Fallback on earlier versions } }.frame(width: .infinity, height: 73) } } }
In this snippet, you create the tab bar by embedding two tab items (”Chats”
and ”Mentions”
) in a TabView
. Because the tab bar allows you to switch between pages, you need to define the page where each tab item should point to.
From the code, you can see the tab items point to the same page, MentionsView
. This is intentional, but in your application, you could point the Chats
button to a chats page and the Mentions
(or code>@) button to the
To attach a tab item to a page, use the .tabItem
modifier. In each tab item, add a Label
consisting of the text (for example, “Chats”) and an image (SF Symbol).
The tab bar also displays two badges on the top right of each tab item with a notification count. To create the badges, add the .badge
modifier and indicate the badge value.
You can learn more about the tab bar by visiting the Apple documentation or the Human Interface Guidelines
Note: The badge modifier is only available in iOS 15. After adding the badge modifier to the code, you’ll get an error. To resolve the error, click the error button when it appears. This will add the 'if #available' version check to the code as shown below.
Creating the Channel List
The channel list view has four main sections:
- HeaderView
- CustomSearchBarView,
- ChanelListView
- TabBarView
The ChannelListView shows a list of messages containing the user’s avatar, name, message summary, status, timestamp, message count, and delivery receipt.
To create the ChannelListView, you need data. You can find the data for this project in the file ChannelListData.swift (located in this interaction prototyping GitHub.
Create Your Data Source
To get the user data, create a new file and copy the content of ChannelListData.swift to it. Or, you can define the structure of the data and the fields you want to display in the file as seen in the code below:
1234567891011121314151617181920212223// Data Structure struct ChannelListStructure: Identifiable{ var id = UUID() var userAvatar: String var userName: String var userMessageSummary: String var userStatus: String var timestamp: String var unreadMessageCount: String var receipt: String } // Data Storage let ChannelData = [ ChannelListStructure ( userAvatar: "user_luke", userName: "Blushing Drawer", userMessageSummary: "Hi", userStatus: "circle.fill", timestamp: "Today", unreadMessageCount: "12.circle.fill", receipt: "readReceipt") ]
In the code above, there is only one user under ChannelData
. You can find the complete list of users in the file ChannelListData after downloading the project file.
Add a Scrollable List (ChannelListView)
To build your scrollable list, create a new Swift file called ChannelListView.swift and populate it with the data source you created in the file ChannelListData.swift.
Then, add the code in the snippet below:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869import SwiftUI struct ChannelListView: View { let StreamBlue = Color( colorLiteral(red: 0, green: 0.368627451, blue: 1, alpha: 1)) let notificationColor = Color( colorLiteral(red: 1, green: 0.2156862745, blue: 0.2588235294, alpha: 1)) let onlineColor = Color( colorLiteral(red: 0.1254901961, green: 0.8784313725, blue: 0.4392156863, alpha: 1)) let appBarColor = Color( colorLiteral(red: 0.07058823529, green: 0.07843137255, blue: 0.0862745098, alpha: 1)) var messages: [ChannelListStructure] = [] var body: some View { VStack { // Header view HeaderView() CustomSearchBarView() // Populating Message List if #available(iOS 15.0, *) { List(messages) { item in HStack { ZStack(alignment: .topTrailing) { Image(item.userAvatar) //User status: Online or offline Image(systemName: item.userStatus) .font(.system(size: 12)) .foregroundColor(onlineColor) } VStack(alignment: .leading){ Text("\(item.userName)") .font(.body) Text("\(item.userMessageSummary)") .font(.footnote) .lineLimit(1) .foregroundColor(.secondary) } Spacer() VStack(alignment: .trailing) { // Number of unread messages Image(systemName: item.unreadMessageCount) .foregroundColor(notificationColor) .font(.footnote) HStack(spacing: 4) { Image(item.receipt) Text("\(item.timestamp)") .font(.footnote) .foregroundColor(.secondary) } } } }.listStyle(.plain) .refreshable{ print("Pull to refresh") } } else { // Fallback on earlier versions } // Tab bar view TabBarView() } // All Views .padding() } }
Here’s what’s going on this snippet:
- In ChannelListView.swift, you define the color styles you want to use for UI elements (like
StreamBlue
,notificationColor
, andonlineColor
) . - Since the messages will be pulled from a data source, you create a variable called
messages
and set it as an empty array.. Then, create an emptyList
. - Inside the
List
, you use anHStack
to arrange the list items horizontally. - Then, you retrieve the
userAvatar
anduserStatus
defined in the ChannelListData.swift file and place them in a depth stack (ZStack
) so that the status shows on the top-right of the avatar. - Next, you add a vertical container (
VStack
) in the list and place theuserName
anduserMessageSummary
in it. - You add another
HStack
to arrange thetimestamp
andreadReceipt
horizontally. - Then, you embed this horizontal container in a vertical container and place the
unreadMessageCount
on the content. - For the content in the top-level
HStack
, you use aSpacer()
to push it to the left and right edges of the screen.
To learn more about how to retrieve data from another Swift file to populate a list in SwiftUI, watch Apple’s Introduction to SwiftUI.
Combining Header, Search Bar, Channel List, and Tab Bar
To put it all together, embed the scrollable list created in ChannelListView.swift in a vertical container VStack. This will serve as the main layout container for the four sections of the screen: your header, search bar, list, and tab bar.
In the main vertical container, add the header HeaderView() and search bar CustomSearchBarView(), which are contained in another Swift file above the list.
Finally, add the TabBarView(), which is also contained in another Swift file, so that it appears at the bottom of the screen.
Designing Inbound and Outbound Messages
This section shows you how to build the composition of incoming and outgoing messages with the user's avatar, message bubbles, text, timestamp, emojis, and delivery status.
Creating an Inbound Message
Create a new file called InboundSingleLineView.swift and add the following code:
1234567891011121314151617181920212223242526272829303132333435import SwiftUI struct InboundSingleLineView: View { let inboundBubbleColor = Color( colorLiteral(red: 0.07058823529, green: 0.07843137255, blue: 0.0862745098, alpha: 1)) var body: some View { HStack(alignment: .bottom) { Image("user_han") .resizable() .frame(width: 36, height: 36) VStack(alignment: .leading) { ZStack(alignment: .bottomLeading) { RoundedRectangle(cornerRadius: 18) .frame(width: 200, height: 36) .overlay( Text("They ain't in space yet") .foregroundColor(.white) ) Rectangle() .frame(width: 20, height: 21) } // Inbound Message Bubble .foregroundColor(inboundBubbleColor) Text("Space Cadet 9.01 AM") .font(.footnote) .foregroundColor(.secondary) } } } }
In this snippet, you pull the user’s avatar from the assets folder, adding the user’s name, text on the message bubble, and the timestamp:
- Begin by defining the chat bubble color as
inboundBubbleColor
using color literal and stating the red, green, and blue color values. - Next, draw a rectangle for the chat bubble and place a text on the rectangle using the
.overlay
modifier. - Embed the chat bubble with the text in a
VStack
layout container and add the textSpace Cadet 9.01 AM
below the bubble. - Align the
Space Cadet 9.01 AM
and the bubble to the left using the alignment parameter of the VStack and set its value to.leading
. - Finally, put the
VStack
inside an HStack` and add the user avatar to the left. - In the horizontal layout container, set the alignment parameter to
.bottom
so that the avatar appears at the bottom left of the message bubble and the text below it.
Creating an Outbound Message
Next, you will create an outbound message by embedding text, a message bubble, and emojis using stack views, vertical stack (VStack), horizontal stack (HStack), and depth stack (ZStack) in a new Swift file called OutboundSingleLineView.swift.
To create an outbound message, add the following code:
12345678910111213141516171819202122232425262728293031323334353637import SwiftUI struct OutboundSingleLineView: View { let inboundBubbleColor = Color( colorLiteral(red: 0.07058823529, green: 0.07843137255, blue: 0.0862745098, alpha: 1)) let StreamBlue = Color( colorLiteral(red: 0, green: 0.368627451, blue: 1, alpha: 1)) var body: some View { VStack(alignment: .trailing) { ZStack(alignment: .bottomTrailing) { RoundedRectangle(cornerRadius: 18) .frame(width: 144, height: 36) .overlay( Text("😅 😅 😅 😅") .frame(width: 240, height: 56) .foregroundColor(.white) ) Rectangle() .frame(width: 20, height: 21) } // Inbound Message Bubble .foregroundColor(inboundBubbleColor) HStack(spacing: 4) { // Timestamp and read receipt Text("82") .foregroundColor(StreamBlue) Image("readReceipt") Text("18.37 PM") .foregroundColor(.secondary) } .font(.footnote) } } }
When you preview your code, Xcode will present the composition as seen in the image below:
You can see the rectangle has three rounded corners and the bottom right corner is straight.
To give your outbound messages this visual effect:
- Place the rounded rectangle (bubble) in a
ZStack
. - Then, draw another small rectangle with a width of 20 and a height of 21 above the bubble.
- Set the alignment parameter of the
ZStack
to.bottomTrailing
so that the small rectangle appears at the bottom right of the bubble to form a straight edge. - Next, add the emojis using text and place them in front of the bubble with the
.overlay
modifier. You can use the.overlay
modifier orZStack
in SwiftUI to put an element in front of another. - Create a horizontal container
HStack
and place thereadRecept
image and the timestamp as text inside theHStack
. - Finally, put all the content in a
VStack
so that it becomes the root container for all the message views and use the alignment parameter to align the content to the right by setting the value as.trailing
.
Building the Message List in ChatsView
The ChatsView screen has three main sections: MessageListHeader(), ScrollView(), and ComposeAreaView(). It is laid out by composing inbound and outbound messages created in the previous sections as well as other message components found in the folders below.
To get started, download the project file and create a new Swift file called ChatsView.swift
.
With the ChatsView
file created, add the following code:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071import SwiftUI struct ChatsView: View { var body: some View { VStack { Spacer() MessageListHeaderView() ScrollView { // Chat area VStack { HStack { // Left Message //InboundDoubleLineView() InboundMessageView() Spacer() } HStack { // Right Message Spacer() OutboundSingleLineView() } HStack { // Left Message InboundSingleLineView() Spacer() } HStack { // Left Message InboundDoubleLineView() Spacer() } HStack { // Right Message Spacer() ClappingHandsEmojiView() } HStack { // Left Message InboundTrippleLineView() Spacer() } HStack { // Right Message Spacer() OutboundTrippleLineView() } HStack { // Right Message Spacer() OutboundDoubleLineView() } HStack { // Right Message Spacer() OutboundSingleLineView() } HStack { // Right Message Spacer() RevolvingHeartsView() .padding(EdgeInsets(top: 40, leading: 0, bottom: 0, trailing: 40)) } } .padding() } // Chat area ComposeAreaView() } // All Views } }
In this snippet, you bring the inbound and outbound messages together onto one screen.
Here’s a walkthrough of the steps:
- Begin by creating the root
VStack
, which you will use to arrange theMessageListHeaderView
, theScrollView
, and theComposeAreaView
vertically. - In the root
VStack
, add theMessageListHeaderView
andComposeAreaView
as components. - Between the header and the compose area, add a
ScrollView
, which enables scroll functionality. - Inside the
ScrollView
, add aVStack
as a root layout container. - Add horizontal containers
HStack
in the root vertical container to arrange the inbound and outbound messages. - In the top
HStack
, addInboundMessageView()
and place a spacer below it so that it pushes the content to the left of the screen. - Next, add another
HStack
and placeOutboundSingleLineView()
in it. By adding a spacer belowOutboundSingleLineView()
, the content will be pushed to the right edge of the screen.
Repeat the above steps to bring as many inbound and outbound messages to the scrollable section as possible.
Congratulations!
Well done. This tutorial covered how to design the main screens and chats view of the Stream iOS Chat SDK using SwiftUI.
You also learned how to create child or sub-views (like inbound and outbound messages) as components by creating them in new files in Xcode.
You can download the SwiftUI source code for this project as a zip file from GitHub. In part two of this tutorial, you will use the designs you created in part one to design interactions and micro-interactions for the iOS Chat SDK.