Prototyping With SwiftUI: Creating Complex Interactions Using Gestures and Modifiers

Gestures make it easy to give your app’s touch interactions a bit of personality and color. In this post, you’ll learn how to create your own gestures and modifiers and apply them to interactions throughout your app using SwiftUI.

Amos G.
Amos G.
Published January 17, 2022 Updated February 18, 2022
Creating Complex Interactions Using Gestures and Modifiers

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:

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// // 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:

  1. 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.
  2. In the .contextMenu, use a label consisting of text and an SF Symbol to represent each menu item as shown in the code below.
swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// // 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).

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// // 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

Building your own app? Get early access to our Livestream or Video Calling API and launch in days!

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.

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// // 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:

  1. Apply the .resizable() modifier to allow the image to automatically resize to fill the available space.
  2. Define the state variable @State private var fullscreen = false at the top of your code where you can define variables.
  3. Add the aspect ratio modifier .aspectRatio(contentMode: fullscreen ? .fill : .fit) to the image and use the fullscreen state variable along with the ternary operator so that the image can switch between fullscreen (.fill) and normal (.fit) modes.
  4. 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 using withAnimation 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:

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// // 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 .tabViewStyleto .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.

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// // 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 gallery iceland2.
  • 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 called isPresented 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.

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// // 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.

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->