In part two of this series, you’ll use our iOS Chat SDK sample application to prototype several gestures that you’ll use for refreshing page content, adding seamless swiping and pagination to message lists and photos, revealing in-app actions to messages in message channels, and more.
You’ll also apply modifiers to these gestures so you can control every aspect of your app’s touch interactions, resulting in a truly customized app with a seamless user experience.
If you haven’t yet, review part one of this series, Prototyping Stream’s iOS Chat SDK With SwiftUI: Part 1. After you’ve caught up, feel free to dive into part two, or check out our SwiftUI Chat Application to learn more.
Ready? Let’s get started.
Setup
You need to download and install Xcode (13+) to run the project files. If you don’t have Xcode installed, download it from the Mac App Store.
You can also download the project files from this GitHub repo and follow the sections below to begin creating the interactions in this project.
There are several files and folders in the Xcode project, but the following are the Swift files you’ll need:
- ChannelListView.swift
- ContextMenuView.swift
- SwipeToDeleteView.swift
- PhotoGalleryZoom.swift
- PhotoGallery.swift
- ReactionsView.swift
Want to learn more about SwiftUI? Check out our SwiftUI Chat tutorial to see how you can get started and integrate it into your project.
How to Make the Channel List Scrollable and Refreshable
In SwiftUI, you can make interface elements automatically scrollable by embedding them in a List layout container. A list view displays data in several rows and arranges the rows in a single column.
After creating your Xcode project, create a new Swift file called ChannelList.swift and enter the following code:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475// // ContentView.swift // Stream iOS Chat SDK Prototyping // // Created by Amos from getstream.io on 14.10.2021. // import 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 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") } TabBarView() } // All Views .padding() } } struct ChannelListView_Previews: PreviewProvider { static var previews: some View { ChannelListView(messages: ChannelData) .preferredColorScheme(.dark) } }
You can also find the code of the list by downloading the project from GitHub. In the project’s folder structure, look for the folder ChannelList and the Swift file, ChannelListView.swift.
The list pulls data from another Swift file called ChannelListData.swift, which can also be found in this project.
The ChannelListData file creates the composition of the list using the layout containers HStack
, ZStack
, VStack
, and Spacer
. By wrapping the container views in the List
view, the content becomes scrollable by default.
You can change the appearance of the list using list styles. For example, to convert the list to a plain list, add the .listStyle
modifier and set its parameter to .plain
as seen in the code above.
Since this is a long scrolling list, you can improve the user experience so that when users perform a standard drag gesture on the list, it displays a visual cue that shows the content is updating.
You can do this with the refreshable
modifier in SwiftUI. To allow users to refresh the contents of the list, apply the refreshable modifier to it, creating the “pull-to-refresh” effect.
Display the Context Menus and Reactions Using the Tap-and-Hold Gesture
To display the context menu for chat messages, you need to attach a long-press gesture to the message bubbles. The context menu is used to show additional information and actions such as Reply, Copy, Message, Edit Message, and Delete Message.
In this section, you will show the context menu by tapping and holding the message bubble. Here’s how:
- Add the .contextMenu modifier to any view to display its contextual information or actions related to it. In this example, you need to attach the modifier to the container for the user avatar, the message bubble, and the text below it.
- In the
.contextMenu
, use a label consisting of text and an SF Symbol to represent each menu item as shown in the code below.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859// // ContextMenuView.swift // Stream iOS Chat SDK Prototyping // Longpressing inbound message // Created by Amos from getstream.io on 14.10.2021. // import SwiftUI struct ContextMenuView: View { let inboundBubbleColor = Color(#colorLiteral(red: 0.2419013083, green: 0.2265482247, blue: 0.2486716509, alpha: 1)) var body: some View { ZStack { VStack(alignment: .leading) { // Container for Reactions, Inbound Message, Context Menu HStack(alignment: .bottom) { Image("luke") VStack(alignment: .leading) { ZStack(alignment: .bottomLeading) { RoundedRectangle(cornerRadius: 21) .frame(width: 120, height: 42) .overlay( Text("Hey Hey!!!") .foregroundColor(.white) ) Rectangle() .frame(width: 20, height: 21) } // Inbound Message Bubble .foregroundColor(inboundBubbleColor) Text("Is that Stream Chat?. Fabulous 18.37") .font(.subheadline) .foregroundColor(.secondary) } } .contextMenu{ Label("Reply", systemImage: "arrow.turn.up.left") Label("Tread Reply", systemImage: "arrowshape.turn.up.left.2") Label("Copy Message", systemImage: "doc.on.doc") Label("Edit Message", systemImage: "pencil") Label("Pin to conversation", systemImage: "pin.fill") Label("Delete Message", systemImage: "trash") } } } } struct ContextMenuView_Previews: PreviewProvider { static var previews: some View { ContextMenuView() .preferredColorScheme(.dark) } } }
Building the Swipe Actions
SwiftUI’s .swipeActions() modifier allows you to swipe a list row to reveal actions related to the row.
To make the list row swipeable and display its associated actions, add the .swipeActions()
modifier to the list row consisting of the user avatar, user name, message summary delivery receipt, and timestamp.
You can specify which side of the list the actions belong to with the edge
parameter. Add .swipeActions(edge: .trailing)
to the .swipeActions()
modifier to show the actions on the right side of the list row.
Additionally, you can display the actions on the left side of the list row by specifying .swipeActions(edge: .leading)
.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180// // SwipeToDeleteView.swift // StreamiOSChatSDKPrototyping // // Created by Amos from getstream.io on 14.10.2021. // import SwiftUI struct SwipeToDeleteView: View { var messages: [ChannelListStructure] = [] 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)) var body: some View { VStack { HeaderView() CustomSearchBarView() List { HStack { ZStack(alignment: .topTrailing) { Image("user_han") } VStack(alignment: .leading){ Text("R2-V2") .font(.body) Text("This is awesome!!!") .font(.footnote) .lineLimit(1) .foregroundColor(.secondary) } Spacer() VStack(alignment: .trailing) { // Number of unread messages Image(systemName: "2.circle") .foregroundColor(notificationColor) .font(.footnote) HStack(spacing: 4) { Image("deliveredReceipt") Text("Yeaterday") .font(.footnote) .foregroundColor(.secondary) } } }// List 1: Swipe action with mute and delete icons .contextMenu{ Label("Reply", systemImage: "arrow.turn.up.left") Label("Tread Reply", systemImage: "arrowshape.turn.up.left.2") Label("Copy Message", systemImage: "doc.on.doc") Label("Mute User", systemImage: "speaker.slash") } .swipeActions(allowsFullSwipe: false) { Button(role: .destructive) { print("Deleting conversation") } label: { Label("Delete", systemImage: "trash.fill") } Button { print("Mute user") } label: { Label("Mute", systemImage: "speaker.slash") } .tint(.indigo) } // List 2: Swipe action with text button HStack { ZStack(alignment: .topTrailing) { Image("user_chew") //User status: Online or offline Circle() .frame(width: 12, height: 12) .foregroundColor(onlineColor) } VStack(alignment: .leading){ Text("Test User Lando") .font(.body) Text("This is awesome!!!") .font(.footnote) .lineLimit(1) .foregroundColor(.secondary) } Spacer() VStack(alignment: .trailing) { // Number of unread messages Image(systemName: "2.circle") .foregroundColor(notificationColor) .font(.footnote) HStack(spacing: 4) { Image("readReceipt") Text("Yeaterday") .font(.footnote) .foregroundColor(.secondary) } } } .contextMenu{ Label("Reply", systemImage: "arrow.turn.up.left") Label("Tread Reply", systemImage: "arrowshape.turn.up.left.2") Label("Copy Message", systemImage: "doc.on.doc") Label("Mute User", systemImage: "speaker.slash") } .swipeActions { Button("Delete") { print("Right on!") } .tint(.red) } // List 3: Swipe actions for both left and right HStack { ZStack(alignment: .topTrailing) { Image("user_luke") //User status: Online or offline } VStack(alignment: .leading){ Text("Amos Gyamfi") .font(.body) Text("This is awesome!!!") .font(.footnote) .lineLimit(1) .foregroundColor(.secondary) } Spacer() VStack(alignment: .trailing) { // Number of unread messages Image(systemName: "36.circle") .foregroundColor(notificationColor) .font(.footnote) HStack(spacing: 4) { Image("deliveredReceipt") Text("Yeaterday") .font(.footnote) .foregroundColor(.secondary) } } } .swipeActions(edge: .leading) { Button { print("Pinn message") } label: { Label("", systemImage: "pin.fill") } .tint(onlineColor) } .swipeActions(edge: .trailing) { Button(role: .destructive) { print("Delete message") } label: { Label("Trash", systemImage: "trash.fill") } } }.listStyle(.plain) TabBarView() } } } struct SwipeToDeleteView_Previews: PreviewProvider { static var previews: some View { SwipeToDeleteView(messages: ChannelData) .preferredColorScheme(.dark) } }
How to Create the Photo-Zoom Effect
When you attach an image and send it as a message, you can zoom in and out by applying a double-tap gesture to the image. To create the image zooming interaction, add the following code to any image uploaded to the assets folder.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117// // PhotosGalleryZoom.swift // This creates Page Scrolling Style Interaction for Photos (with index display mode) // Swipping or flicking through photos in a photo gallery // ALWAYS: Use the "page" tabview style and set the index tab view style to "always" // Created by Amos from getstream.io on 17/12/2021. // // INTERACTION STYLE // 1. Double tap to transition between fit mode and fill/fullscreen mode // 3. In the fullscreen mode, tap the xmark (x) to transition from fill/fullscreen mode to fit mode // 4. Drag the sheet downwards to dismiss it import SwiftUI struct PhotosGalleryZoom: View { @State private var zoom = false @State private var showSheet = false var body: some View { VStack { HStack { Image(systemName: "xmark") .onTapGesture { zoom = false } Spacer() VStack { Text("Count Dooku") // Follows Human Interface Guidelines .font(.headline) .fontWeight(.bold) Text("Last seen one hour ago") .font(.caption) .foregroundColor(.secondary) } Spacer() } .font(.title2) TabView{ Image("iceland2") .resizable() .cornerRadius(10) .aspectRatio(contentMode: zoom ? .fill : .fit) .onTapGesture(count: 2) { withAnimation(.interpolatingSpring(stiffness: 170, damping: 30)){ zoom.toggle() } } Image("iceland7") .resizable() .aspectRatio(contentMode: zoom ? .fill : .fit) .onTapGesture(count: 2) { withAnimation(.interpolatingSpring(stiffness: 170, damping: 30)){ zoom.toggle() } } Image("iceland3") .resizable() .aspectRatio(contentMode: zoom ? .fill : .fit) .onTapGesture(count: 2) { withAnimation(.interpolatingSpring(stiffness: 170, damping: 30)){ zoom.toggle() } } Image("iceland4") .resizable() .aspectRatio(contentMode: zoom ? .fill : .fit) .onTapGesture(count: 2) { withAnimation(.interpolatingSpring(stiffness: 170, damping: 30)){ zoom.toggle() } } Image("iceland5") .resizable() .aspectRatio(contentMode: zoom ? .fill : .fit) .onTapGesture(count: 2) { withAnimation(.interpolatingSpring(stiffness: 170, damping: 30)){ zoom.toggle() } } Image("iceland6") .resizable() .aspectRatio(contentMode: zoom ? .fill : .fit) .onTapGesture(count: 2) { withAnimation(.interpolatingSpring(stiffness: 170, damping: 30)){ zoom.toggle() } } } // This creates paging interaction .tabViewStyle(.page) .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always)) Spacer() HStack { Image(systemName: "square.and.arrow.up") Spacer() } } // All views .padding() } } struct PhotosGalleryZoom_Previews: PreviewProvider { static var previews: some View { PhotosGalleryZoom() .preferredColorScheme(.dark) } }
This example uses the image iceland2
taken from the Xcode assets library. After adding your image:
- Apply the .resizable() modifier to allow the image to automatically resize to fill the available space.
- Define the state variable
@State private var fullscreen = false
at the top of your code where you can define variables. - Add the aspect ratio modifier
.aspectRatio(contentMode: fullscreen ? .fill : .fit)
to the image and use thefullscreen
state variable along with the ternary operator so that the image can switch between fullscreen (.fill
) and normal (.fit
) modes. - Finally, add the tap gesture
.onTapGesture(count: 2)
to the image with the count parameter set to two so that you can double tap the image to switch between the fill and fit modes. To get the spring effect when the image transitions from the fill to fit mode, add an explicit animation usingwithAnimation
with an interpolating spring and set the spring parameters as seen above.
Visit Hacking with Swift to learn more about how to create an explicit animation in SwiftUI.
Building the Flicking/Swiping Interaction
When you send a message containing two or more images, you can cycle through the images using a flick or a swipe gesture.
To create a flicking/swiping interaction, pick two or more images from your Xcode assets library and create a new file called PhotosGalleryZoom.
Then, add the following code:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117// // PhotosGalleryZoom.swift // This creates Page Scrolling Style Interaction for Photos (with index display mode) // Swipping or flicking through photos in a photo gallery // ALWAYS: Use the "page" tabview style and set the index tab view style to "always" // Created by Amos from getstream.io on 17/12/2021. // // INTERACTION STYLE // 1. Double tap to transition between fit mode and fill/fullscreen mode // 3. In the fullscreen mode, tap the xmark (x) to transition from fill/fullscreen mode to fit mode // 4. Drag the sheet downwards to dismiss it import SwiftUI struct PhotosGalleryZoom: View { @State private var zoom = false @State private var showSheet = false var body: some View { VStack { HStack { Image(systemName: "xmark") .onTapGesture { zoom = false } Spacer() VStack { Text("Count Dooku") // Follows Human Interface Guidelines .font(.headline) .fontWeight(.bold) Text("Last seen one hour ago") .font(.caption) .foregroundColor(.secondary) } Spacer() } .font(.title2) TabView{ Image("iceland2") .resizable() .cornerRadius(10) .aspectRatio(contentMode: zoom ? .fill : .fit) .onTapGesture(count: 2) { withAnimation(.interpolatingSpring(stiffness: 170, damping: 30)){ zoom.toggle() } } Image("iceland7") .resizable() .aspectRatio(contentMode: zoom ? .fill : .fit) .onTapGesture(count: 2) { withAnimation(.interpolatingSpring(stiffness: 170, damping: 30)){ zoom.toggle() } } Image("iceland3") .resizable() .aspectRatio(contentMode: zoom ? .fill : .fit) .onTapGesture(count: 2) { withAnimation(.interpolatingSpring(stiffness: 170, damping: 30)){ zoom.toggle() } } Image("iceland4") .resizable() .aspectRatio(contentMode: zoom ? .fill : .fit) .onTapGesture(count: 2) { withAnimation(.interpolatingSpring(stiffness: 170, damping: 30)){ zoom.toggle() } } Image("iceland5") .resizable() .aspectRatio(contentMode: zoom ? .fill : .fit) .onTapGesture(count: 2) { withAnimation(.interpolatingSpring(stiffness: 170, damping: 30)){ zoom.toggle() } } Image("iceland6") .resizable() .aspectRatio(contentMode: zoom ? .fill : .fit) .onTapGesture(count: 2) { withAnimation(.interpolatingSpring(stiffness: 170, damping: 30)){ zoom.toggle() } } } // This creates paging interaction .tabViewStyle(.page) .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always)) Spacer() HStack { Image(systemName: "square.and.arrow.up") Spacer() } } // All views .padding() } } struct PhotosGalleryZoom_Previews: PreviewProvider { static var previews: some View { PhotosGalleryZoom() .preferredColorScheme(.dark) } }
Let’s discuss what’s happening in this code block:
Notice we’re using a TabView
as the parent layout container for the six images. To get the paginated scrolling effect we want from flicking/swiping an image, set the .tabViewStyle
to .page
using the .tabViewStyle(.page)
modifier.
You can show or hide the index (the small dots used for cycling through the images) using the .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
modifier.
To learn more about how to use a tab view, visit the Apple Developer documentation.
To get our zoom effect, add another Swift file called PhotosInGallery to create a gallery of four images.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889// // PhotosGallery.swift // INTERACTION STYLE // 1. Single tap to fit the photo to the center of the screen using tab view and sheet // 2. Double tap to transition between fit mode and fill/fullscreen mode // 3. In the fullscreen mode, tap the xmark (x) to transition from fill/fullscreen mode to fit mode // 4. Drag the sheet downwards to dismiss it import SwiftUI struct PhotosGallery: View { @State private var showSheet = false let inboundBubbleColor = Color(#colorLiteral(red: 0.07058823529, green: 0.07843137255, blue: 0.0862745098, alpha: 1)) @State private var unzoomed = true // Single tap to fit to center @State private var fullscreen = false // Double tap to transition sbetween fit center and fullscreen var body: some View { HStack(alignment: .bottom) { Image("user_chew") .resizable() .frame(width: 36, height: 36) // Photo VStack(alignment: .leading) { ZStack { VStack(spacing: 1) { HStack(spacing: 1) { Image("iceland2") .resizable() .frame(width: 126, height: 94) .preferredColorScheme(.dark) .sheet(isPresented: $showSheet) { PhotosGalleryZoom() } .onTapGesture { showSheet.toggle() } Image("iceland3") .resizable() .frame(width: 126, height: 94) .sheet(isPresented: $showSheet) { PhotosGalleryZoom() } .onTapGesture { showSheet.toggle() } } HStack(spacing: 1) { Image("iceland5") .resizable() .frame(width: 126, height: 94) .sheet(isPresented: $showSheet) { PhotosGalleryZoom() } .onTapGesture { showSheet.toggle() } Image("iceland7") .resizable() .frame(width: 126, height: 94) .sheet(isPresented: $showSheet) { PhotosGalleryZoom() } .onTapGesture { showSheet.toggle() } } } } .frame(width: 252, height: 188) .cornerRadius(16) Text("Toronto time 21.00 PM") .font(.footnote) .foregroundColor(.secondary) } } } } struct PhotosGallery_Previews: PreviewProvider { static var previews: some View { PhotosGallery() .preferredColorScheme(.dark) } }
To present the expanded images over the image gallery, you need to use the sheet modifier in SwiftUI. The sheet modifier also allows you to dismiss the images using a drag gesture.
To use the sheet presentation:
- Add the modifier called
.sheet(isPresented: $showSheet) { PhotosGalleryZoom() }
to the first image in the galleryiceland2
. - Next, give the sheet some content to display. This can be an image view or a text view. Use
PhotosGalleryZoom()
as the content to show and add the boolean calledisPresented
that states whether the expanded image views (detailed views) should be displayed or not. - Finally, add the
.onTapGesture { showSheet.toggle() }
modifier to one of the images so that you can tap it to show the modal sheet.
How to Trigger the “Like” Animation Using a Tap Gesture
In SwiftUI, you can make a view recognize one or more user taps using the .onTapGesture
modifier.
In this example, you should add the tap gesture to the heart icon to trigger the animation and change the icon’s state from “unliked” to “liked” as shown in the code below. You can do this using conditional visibility (‘if and else’) statements in SwiftUI.
To trigger the “like” animation, create a new Swift file called ReactionsView.swift and replace its content with the code below. The code presents the heart animation seen above when the icon is tapped.
In part 3 of this tutorial, you will learn how to build the animation from scratch so don’t worry about it now. This section aims at showing you how to use a tap gesture to initiate animations in SwiftUI.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697// // ReactionsView.swift // Stream iOS Chat SDK Prototyping // // Created by Amos from getstream.io on 09.01.2022. // import SwiftUI struct ReactionsView: View { let reactionsBGColor = Color(#colorLiteral(red: 0.07058823529, green: 0.07843137255, blue: 0.0862745098, alpha: 1)) // Like Animation States @State private var notLiked = true @State private var removeInnerStroke = 14 @State private var chromaRotate = 0 @State private var animateTopPlus = 1 @State private var animateMiddlePlus = 1 @State private var animateBottomPlus = 1 var body: some View { ZStack { RoundedRectangle(cornerRadius: 28) .frame(width: 216, height: 40) .foregroundColor(reactionsBGColor) HStack(spacing: 20) { ZStack{ // When the heart icon is not tapped if notLiked { Image("like") } else { Image(systemName: "heart.fill") .font(.system(size: 24)) .frame(width: 24, height: 21) .foregroundColor(Color(.systemPink)) ZStack { Circle() .strokeBorder(lineWidth: CGFloat(removeInnerStroke)) .frame(width: 28, height: 28) .foregroundColor(Color(.systemPink)) .hueRotation(.degrees(Double(chromaRotate))) VStack { Image(systemName: "heart.fill") .scaleEffect(CGFloat(animateTopPlus)) .foregroundColor(Color(.systemPink)) Image(systemName: "plus") .scaleEffect(CGFloat(animateMiddlePlus)) Image(systemName: "heart.fill") .scaleEffect(CGFloat(animateBottomPlus)) .foregroundColor(Color(.systemPink)) } } } } .onTapGesture { withAnimation(.easeInOut(duration: 0.25)){ notLiked.toggle() } withAnimation(.easeOut(duration: 0.5)){ removeInnerStroke = 0 chromaRotate = 270 } withAnimation(.easeOut(duration: 0.5).delay(0.1)){ animateTopPlus = 0 } withAnimation(.easeInOut(duration: 0.5).delay(0.2)){ animateMiddlePlus = 0 } withAnimation(.spring()){ animateBottomPlus = 0 } } Image("thumbs_up") Image("thumbs_down") Image("lol") Image("wut_reaction") } } // All reaction views } } struct ReactionsView_Previews: PreviewProvider { static var previews: some View { ReactionsView() .preferredColorScheme(.dark) } }
As you can see from the code above, the .onTapGesture
is attached to the heart icons, which are embedded in a ZStack
layout container. This allows the pink heart icon to appear on the top of the gray heart icon.
The visibility of the heart icons is controlled using ‘if and else’ statements. So, if the user has not tapped the heart icon, the gray one is presented. When the user taps the gray heart icon, it becomes hidden and the pink version is then presented.
To see the “like” animations when the heart icon is tapped, you have to embed the easing equations of the animation as well as the final states of the animation inside the .onTapGesture
.
Conclusion
Well done! This tutorial covered how to prototype interaction styles for the Stream iOS Chat SDK using SwiftUI.
You learned how to make SwiftUI list views refreshable and scrollable, how to use swipe actions, how to add in paginated scrolling, and how to trigger animations with human-initiated gestures.
You can download the SwiftUI source codes for this project from this GitHub repository.
In part three of this tutorial, you will learn how to create chat messaging related animations with SwiftUI.