This tutorial takes you through creating a clone of the iOS Messages application’s contacts list. Designing the contacts list will give you the foundations and basic understanding of compositing interfaces in SwiftUI. A follow-up tutorial and its GitHub repository will show you how to implement the list interface created in this tutorial using the Stream SwiftUI SDK.
Setup and Sample Source Code
The source code for the project is hosted on this GitHub repository. It contains several files and folders, but you will need only three of the Swift files to complete this tutorial. These are:
- MessageDataModel.swift: Located in the
Datastore
folder - HeaderView.swift: Located in the
Components
folder - MessagesView.swift: Located in the
UI Design
folder
To get the most out of this tutorial, I recommend watching the video version from the Stream Developers YouTube channel.
Touring the Data Storage
In SwiftUI, you can populate list views using static or dynamic content. To display information in the contacts list, you will store the sample data using a dictionary in the Swift file MessagesDataModel.swift. Download the code as a GitHub gist and explore the list data or follow the steps below to create the data file in your Xcode project:
- Add a new folder in your project’s navigator and name it
DataStore
. To create a new folder, you can right-click on any file or folder in the navigator and click the optionNew Group
. - After right-clicking on
DataStore
, select the optionNew File
and name it as MessagesDataModel.swift. - Below the
import Foundation
declaration, define the data structure calledMessageStructure
as follows:
// Data structure
struct MessagesStructure: Identifiable { // Identifiable protocol - makes it possible to use value types that need to have a stable notion of identity.
var id = UUID() // A universally unique identifier to identify a particular datafield and types
var unreadIndicator: String
var avatar: String
var name: String
var messageSummary: String
var timestamp: String
}
The snippet above creates the structure of the message list using struct and the Identifiable protocol in Swift. A struct
or structure in Swift allows you to specify some properties to store values.
Declaring a structure as identifiable helps you to define data types that need to have a stable notion of identity. Inside the curly braces comes the definition of the structure. Here, you need to define id
as a universally unique identifier (UUID
). This makes it feasible for the data fields you define to be identified.
- Next, define all the data fields such as
unreadIndicator
,avatar
,name
,messageSummary
, andtimestamp
as variables using thevar
keyword. The fields represent text and image views that will display in the contacts list interface. - Finally, assign a dictionary literal to the constant
MessagesData
and use it to store the sample data. In Swift, the elements of a dictionary are stored using “key-value” pairs. Unlike an array, which is an ordered collection, the order of items in a dictionary is not important when you want to retrieve values. Your dictionary literal must contain a “comma-separated” list of “key-value” entries separated by a colon. Surround the keys and their associated values with a square bracket as shown in the code below:
let MessageData = [
MessagesStructure(unreadIndicator: "unreadIndicator", avatar: "jared", name:
"Jared", messageSummary: "That's great, I can help you with that! What type
of product are you...", timestamp: "13:30 AM"),
MessagesStructure(unreadIndicator: "", avatar: "martin", name: "Martin Steed",
messageSummary: "I don't know why people are so anti pineapple pizza. I kind
of like it.", timestamp: "12:40 AM"),
MessagesStructure(unreadIndicator: "", avatar: "jeroen", name: "Zach Friedman",
messageSummary: "(Sad fact: you cannot search for a gif of the word "gif",
just gives you gifs.)", timestamp: "11:00 AM"),
MessagesStructure(unreadIndicator: "", avatar: "carla", name: "Akua Mansa",
messageSummary: "There's no way you'll be able to jump your motorcycle over
that bus.", timestamp: "10:36 AM"),
MessagesStructure(unreadIndicator: "", avatar: "zain", name: "Dee McRobie",
messageSummary: "Tabs make way more sense than spaces. Convince me I'm
wrong. LOL.", timestamp: "9:59 AM"),
MessagesStructure(unreadIndicator: "unreadIndicator", avatar: "nash", name:
"Nash", messageSummary: "(Sad fact: you cannot search for a gif of the word
"gif", just gives you gifs.)", timestamp: "9:26 AM"),
MessagesStructure(unreadIndicator: "", avatar: "fra", name: "Francesco M.",
messageSummary: "I don't know why people are so anti pineapple pizza. I kind
of like it.", timestamp: "9:20 AM"),
MessagesStructure(unreadIndicator: "", avatar: "luke", name: "Luke",
messageSummary: "There's no way you'll be able to jump your motorcycle over
that bus.", timestamp: "9:16 AM"),
MessagesStructure(unreadIndicator: "", avatar: "maren", name: "Ama Aboakye",
messageSummary: "Tabs make way more sense than spaces. Convince me I'm
wrong. LOL.", timestamp: "9:00 AM"),
MessagesStructure(unreadIndicator: "", avatar: "zoey", name: "Zoey",
messageSummary: "That's what I'm talking about!", timestamp: "8:59 AM"),
MessagesStructure(unreadIndicator: "", avatar: "gordon", name: "Gordon Hayes",
messageSummary: "(Sad fact: you cannot search for a gif of the word "gif",
just gives you gifs.)", timestamp: "8:51 AM"),
MessagesStructure(unreadIndicator: "", avatar: "amos", name: "Amos G.",
messageSummary: "Maybe email isn't the best form of communication.",
timestamp: "9:36 AM"),
MessagesStructure(unreadIndicator: "unreadIndicator", avatar: "maren", name:
"Yaa Aso", messageSummary: "There's no way you'll be able to jump your
motorcycle over that bus.", timestamp: "8:50 AM"),
MessagesStructure(unreadIndicator: "", avatar: "dillion", name: "Dillion
Megida", messageSummary: "That's what I'm talking about!", timestamp: "8:45
AM"),
MessagesStructure(unreadIndicator: "", avatar: "adam", name: "Adam Rush",
messageSummary: "(Sad fact: you cannot search for a gif of the word "gif",
ust gives you gifs.)", timestamp: "8:40 AM"),
MessagesStructure(unreadIndicator: "unreadIndicator", avatar: "thierry", name:
"Thierry S.", messageSummary: "Maybe email isn't the best form of
communication.", timestamp: "8:36 AM")
Define the Layout Tree/Structure
The basic structure of the layout you will build in this tutorial is illustrated in the diagram above (the visual appearance is shown on the phone screen). In SwiftUI, you can build layouts by composing views into containment hierarchies. Container views are normally found at the root level of the hierarchy, while child views such as text, images, and shapes are found at the bottom.
In the preview above, there is a root container that has a navigation view and a list container. The list is a collection view made up of rows consisting of the status
, avatar
, and the column views. The column container has a summary and a row view. At the bottom of the hierarchy is the name
view and a horizontal container consisting of the timestamp
and the right arrow icon.
You can watch the Apple Developer video SwiftUI Essentials to learn more about SwiftUI’s layout hierarchy diagrams.
Create the Header View
The header contains the following:
- Search bar
- Screen title
- Edit and compose buttons
The elements of the header are embedded in a Navigation View. Although this tutorial focuses on building a single screen, you will still use the navigation view to compose the header section. In this way, when you have more screens in your app, it will allow users to traverse to the different sections.
To create the header, add a new Swift file HeaderView.swift to the Xcode project. You can also download the code as a GitHub gist or replace the content of the new file with the code below:
//import SwiftUI
struct HeaderView: View {
let accentPrimary = Color( colorLiteral(red: 0.03921568627, green:
0.5176470588, blue: 1, alpha: 1))
@State private var searchText = ""
var body: some View {
NavigationView{
Text("Searching for \(searchText)?")
.searchable(text: $searchText)
.navigationTitle("Messages")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(
leading: Button {
print("Pressed edit button")
} label: {
Text("Edit")
},
trailing: Button {
print("Pressed compose button")
} label: {
Image(systemName: "square.and.pencil")
}
)
}
.frame(height: 80)
}
}
struct HeaderView_Previews: PreviewProvider {
static var previews: some View {
HeaderView()
.preferredColorScheme(.dark)
}
}
Here is how it works:
- You will define a constant as the color of the navigation items using a color literal
let accentPrimary = Color(#colorLiteral(red: 0.03921568627, green: 0.5176470588, blue: 1, alpha: 1))
. Additionally, declare a search field and assign an empty string to it using a state variable@State private var searchText = ""
. - Then, create a text view out of the state variable
searchText
and make it searchable. The searchable modifier is only available in iOS 15. Making a text view searchable automatically converts it into a search bar.
Text("Searching for \(searchText)?")
.searchable(text: $searchText)
- Next, wrap the search bar in a
NavigationView{// Content}
. - Give the page navigation a title using the navigation title modifier
.navigationTitle("Messages")
. This creates a large title, but you can change it to a smaller version by displaying it inline with the navigation buttons using the modifier.navigationBarTitleDisplayMode(.inline)
. - Finally, you should give the navigation view some items to display. Here, you will show the edit and compose buttons. The navigation view allows you to display the items in two locations:
leading
(left edge) andtrailing
(right edge). Use the snippet below to create the navigation bar items.
.navigationBarItems(
leading: Button {
print("Pressed edit button")
} label: {
Text("Edit")
},
trailing: Button {
print("Pressed compose button")
} label: {
Image(systemName: "square.and.pencil")
}
)
As you can see, the leading edge has the edit button and the trailing edge contains the compose button.
Create the Views Compositor
The views compositor is divided into two sections: The header and the scrollable message list. In this Swift file, you will bring the contents of the header created previously in the file called HeaderView.swift and pull the data you created in MessagesDataModel.swift to build the list.
Create a new Swift file and name it MessagesView.swift. In the declaration section of your code, introduce the variable messages
and set it as an empty array using the message structure you defined in MessagesDataModel.swift, var messages: [MessagesStructure] = []
. You will use this variable to populate the message list.
In addition to this, declare the constant readIndicator
using a color literal and set the transparency of the color to zero. The color constant you defined here will be used later for aligning elements in the list, so its visibility is not important.
In the body section of your code, introduce a root column container and use it to hold the header and the list of messages.
var body: some View {
VStack {
// Header content
// List of messages
} // Vertical container for the list and header
}
Importing the Header Contents
When designing the layout of app screens, it is essential to ensure that your code is compact and well organized by placing the various sections and elements of the code in separate files. That is why you created the header section in a different Swift file. You can bring the contents of the header into the MessagesView.swift by stating the file name of the header, followed by parenthesis as shown below.
var body: some View {
VStack {
HeaderView()
// List of messages
} // Vertical container for the list and header
}
Composition of the Message List
The message list contains the following elements:
- online status
- profile image
- name
- message summary
- timestamp.
These items are stored in a data source in the file MessagesDataModel.swift. Use a list to display the data in rows.
A list is a collection view that shows its content in one column with several rows. After populating the list with the data, there will be several rows that will not fit on the visible area of the screen. This will add an automatic scrolling effect to the list.
You should use the snippet below to display the data in the list:
List(messages) { item in
HStack {
ZStack {
readIndicator
.frame(width: 11, height: 11)
Image(item.unreadIndicator)
}
Image(item.avatar)
.resizable()
.clipShape(Circle())
.frame(width: 45, height: 45)
VStack(alignment: .leading){
HStack{
Text("\(item.name)")
Spacer()
Text("\(item.timestamp)")
.font(.headline)
.foregroundColor(.secondary)
Image(systemName: "chevron.forward")
.font(.headline)
.foregroundColor(.secondary)
}
Text("\(item.messageSummary)")
.font(.headline)
.foregroundColor(.secondary)
}
}
}
.listStyle(.plain)
Begin by creating a list instance using a closure and pass the variable messages
you created previously to the list. The list content builder uses item in
to iterate over the entire message data set in the storage.
List(messages) { item in
}
To display the data inside each row, use an ‘HStack’ as a root container for the list. This horizontal parent view displays the small blue circle representing the online status, avatar, and the text elements.
HStack {
// readIndicator
// avatar
// VStack
}
Next, use column and row containers to arrange the name, timestamp, disclosure indicator (right arrow), and the message summary. In the data model, these data fields are stored as string variables. For instance, the name is declared as var name: String
. To display them here, you will use a text view. You can pick the data fields using the element iterator followed by its variable name. For example, to bring the name
field from the database, use Text("(item.name)")
.
The snippet below is used to layout the above elements of the list.
VStack(alignment: .leading){
HStack{
Text("\(item.name)")
Spacer()
Text("\(item.timestamp)")
.font(.headline)
.foregroundColor(.secondary)
Image(systemName: "chevron.forward")
.font(.headline)
.foregroundColor(.secondary)
}
Text("\(item.messageSummary)")
.font(.headline)
.foregroundColor(.secondary)
}
By default, the list you create in SwiftUI has a background with rounded corners. To override this behavior, add the list row modifier and set it to a plain list. This will remove the background.
List(messages) { item in
HStack {
// readIndicator
// avatar
// VStack
}
}.listStyle(.plain)
Putting It All Together
In the Swift file MessagesView.swift you created previously, you should put the list below the header component as shown in the code below, and that completes this tutorial.
// MessagesView.swift
// iMessageClone
import SwiftUI
struct MessagesView: View {
var messages: [MessagesStructure] = []
let readIndicator = Color( colorLiteral(red: 0.3098039329, green:
0.01568627544, blue: 0.1294117719, alpha: 0))
var body: some View {
VStack {
HeaderView()
List(messages) { item in
HStack {
ZStack {
readIndicator
.frame(width: 11, height: 11)
Image(item.unreadIndicator)
}
Image(item.avatar)
.resizable()
.clipShape(Circle())
.frame(width: 45, height: 45)
VStack(alignment: .leading){
HStack{
Text("\(item.name)")
Spacer()
Text("\(item.timestamp)")
.font(.headline)
.foregroundColor(.secondary)
Image(systemName: "chevron.forward")
.font(.headline)
.foregroundColor(.secondary)
}
Text("\(item.messageSummary)")
.font(.headline)
.foregroundColor(.secondary)
}
}
}
.listStyle(.plain)
} // Vertical container for the list and header
}
}
struct MessagesView_Previews: PreviewProvider {
static var previews: some View {
MessagesView(messages: MessageData)
.preferredColorScheme(.dark)
}
}
Where to Go From Here?
In this tutorial, you learned about how to create a clone of the iOS Messages app’s contact list. You can watch the video version of this tutorial from the Stream Developers YouTube Channel. Also, you can get the completed project files from this GitHub repository. If you are new to Stream Chat SwiftUI, check the article iOS Chat With The Stream SwiftUI SDK to get started.