Skip to main content
Version: 4.x.x

Reactions - iOS

During a livestream, one way to increase interaction between viewers and speakers is to show viewers' reactions to everyone. You might have seen emojis flying around during livestreams on Instagram.

Let's create a similar flying emoji by using the VideoSDK PubSub mechanism.

Step 1: Creating a button to send reaction​

When Send is clicked, we send the emoji name to all the participants using the VideoSDK PubSub mechanism, and also show the emoji to the local participant. To pick the emoji, we use the Swift package at this link: https://github.com/Kelvas09/EmojiPicker.git

import EmojiPicker

struct LiveStreamView: View {
@ObservedObject var controller: LiveStreamViewController
// for pubsub emojis
@State var selectedEmoji: Emoji?
@State var displayEmojiPicker: Bool = false

var body: some View {
VStack {
Button {
displayEmojiPicker = true
} label: {
Image(systemName: "face.smiling")
.font(.system(size: 20))
.frame(width: 45, height: 45)
.background(Color.white.opacity(0.3))
.clipShape(RoundedRectangle(cornerRadius: 10))
.foregroundColor(.white)
}
.sheet(isPresented: $displayEmojiPicker) {
NavigationView {
EmojiPickerView(
selectedEmoji: $selectedEmoji,
selectedColor: .orange)
.navigationTitle("Select Reaction")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(
trailing: Button("Send") {
if let emoji = selectedEmoji {
controller.sendTheReaction(emoji)
}
displayEmojiPicker = false
}
)
}
}
}
}
}

class LiveStreamViewController: ObservableObject {
var room: Room?

func sendTheReaction(_ emoji: Emoji) {
Task {
do {
try await self.room?.pubSub.publish(topic: "REACTION", message: emoji.value)
} catch { print("Error while sendTheReaction: \(error)") }
}
}
}

Step 2: Displaying the Reactions to all the users​

Here we will listen to the onMessageReceived event of PubSubMessageListener to know when someone sends a reaction, and show the flying emoji whenever it is triggered.

To receive the reactions, subscribe to the REACTION topic once you have joined. onRoomJoined() is called only if you add the controller with room?.addEventListener(self) before joining.

import EmojiPicker
import SwiftUI
import VideoSDKRTC

struct Reaction: Equatable, Identifiable {
let id: UUID
let emoji: String
let startX: CGFloat
let horizontalOffset: CGFloat
}

// floats one emoji up the screen
struct FloatingAnimation: ViewModifier {
let finalY: CGFloat
let horizontalOffset: CGFloat

func body(content: Content) -> some View {
content
.offset(x: horizontalOffset, y: 0)
.animation(
Animation.easeOut(duration: 2.5)
.delay(Double.random(in: 0...0.5))
.speed(Double.random(in: 0.8...1.2))
)
.offset(y: finalY)
}
}

struct LiveStreamView: View {

@ObservedObject var controller: LiveStreamViewController
@State private var visibleReactions: [Reaction] = []
private let maxVisibleReactions = 13
private let copiesPerReaction = 8

private var reactionOverlay: some View {
GeometryReader { geometry in
ZStack {
ForEach(visibleReactions) { reaction in
Text(reaction.emoji)
.font(.system(size: 40))
.position(
x: geometry.size.width / 2 + reaction.startX,
y: geometry.size.height - 100
)
.modifier(
FloatingAnimation(
finalY: -geometry.size.height + 100,
horizontalOffset: reaction.horizontalOffset
)
)
}
}
}
}

var body: some View {
VStack {
//...
ZStack {
//...
// Add reaction overlay at the screen level
reactionOverlay
}
}
.onChange(
of: controller.reactions,
{ oldValue, newValue in
if let latestReaction = newValue.last {
addReaction(latestReaction)
}
}
)
}

private func randomOffset() -> (CGFloat, CGFloat) {
let horizontalOffset = CGFloat.random(in: -150...150)
let startX = CGFloat.random(in: -50...50)
return (horizontalOffset, startX)
}

private func addReaction(_ emoji: String) {
for _ in 0..<copiesPerReaction {
let (horizontalOffset, startX) = randomOffset()
let newReaction = Reaction(
id: UUID(),
emoji: emoji,
startX: startX,
horizontalOffset: horizontalOffset
)
withAnimation {
visibleReactions.append(newReaction)
DispatchQueue.main.asyncAfter(
deadline: .now() + Double.random(in: 2.5...3.5)
) {
withAnimation {
visibleReactions.removeAll { $0.id == newReaction.id }
}
}
}
}

if visibleReactions.count > maxVisibleReactions {
visibleReactions.removeFirst(
visibleReactions.count - maxVisibleReactions
)
}
}
}

class LiveStreamViewController: ObservableObject {
//...
@Published var reactions: [String] = []
}

extension LiveStreamViewController: RoomEventListener {
func onRoomJoined() {
Task {
do {
try await room?.pubSub.subscribe(topic: "REACTION", listener: self)
} catch {
print("Error while subscribing to REACTION: \(error)")
}
}
}
}

extension LiveStreamViewController: PubSubMessageListener {
func onMessageReceived(_ message: VideoSDKRTC.PubSubMessage) {
if (message.topic == "REACTION") {
self.reactions.append(message.message)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
if let index = self.reactions.firstIndex(
of: message.message
) {
self.reactions.remove(at: index)
}
}
}
}
}

API Reference​

The API references for all the methods and events utilised in this guide are provided below.

Got a Question? Ask us on discord