Reactions during Live Stream - iOS
To enhance interaction between viewers and speakers during a livestream, a creative approach is to display viewers' reactions to everyone. This can be achieved by creating a lively atmosphere with flying emojis, similar to the experience seen in livestreams on platforms like Instagram.
This guide explains how to implement this engaging feature using the VideoSDK PubSub mechanism.
Step 1: Add Models and Animation to LiveStreamView.swift​
Add these models at the top of your file, next to your other model definitions:
// Add this alongside your other struct definitions
struct Reaction: Equatable, Identifiable {
let id: UUID
let emoji: String
let startX: CGFloat
let horizontalOffset: CGFloat
}
// Add the animation modifier
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)
}
}
Step 2: Add State Variables to LiveStreamView​
Add these state variables to your LiveStreamView
struct:
// For emoji reactions
@State private var visibleReactions: [Reaction] = []
private let maxVisibleReactions = 13
private let copiesPerReaction = 8 // Number of copies for each reaction
Step 3: Add the Reaction Helper Functions to LiveStreamView​
Add these functions inside your LiveStreamView
struct:
// Add a function to generate random offset
private func randomOffset() -> (CGFloat, CGFloat) {
let horizontalOffset = CGFloat.random(in: -150...150)
let startX = CGFloat.random(in: -50...50)
return (horizontalOffset, startX)
}
// Add the addReaction function
private func addReaction(_ emoji: String) {
// Create multiple copies of the same reaction with different paths
for _ in 0..<copiesPerReaction {
let (horizontalOffset, startX) = randomOffset()
let newReaction = Reaction(
id: UUID(),
emoji: emoji,
startX: startX,
horizontalOffset: horizontalOffset
)
withAnimation {
visibleReactions.append(newReaction)
// Remove this specific reaction after animation
DispatchQueue.main.asyncAfter(deadline: .now() + Double.random(in: 2.5...3.5)) {
withAnimation {
visibleReactions.removeAll { $0.id == newReaction.id }
}
}
}
}
// Limit total visible reactions
if visibleReactions.count > maxVisibleReactions {
visibleReactions.removeFirst(visibleReactions.count - maxVisibleReactions)
}
}
Step 4: Add the Reaction Overlay View​
Add this to your LiveStreamView
struct:
// Define the reaction overlay view
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
))
}
}
}
}
Step 5: Add the Reactions to Your ZStack​
Locate the main ZStack in your body property and add the reactionOverlay:
ZStack {
// Your existing video content
// ...
// Controls and other UI
// ...
// Add reaction overlay at the screen level
reactionOverlay
}
Step 6: Modify Your Emoji Button​
Find or add an emoji button for audience members in your controls section:
// Emoji button for audience mode
if isAudienceMode {
Button {
displayEmojiPicker = true
} label: {
Image(systemName: "face.smiling")
.font(.system(size: 20))
.frame(width: 45, height: 45)
.background(Color.gray.opacity(0.8))
.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 {
liveStreamViewController.sendTheReaction(emoji)
}
displayEmojiPicker = false
})
}
}
}
Step 7: Add Reaction Observer​
Add this to your view modifiers:
.onChange(of: liveStreamViewController.reactions) { newReactions in
if let latestReaction = newReactions.last {
addReaction(latestReaction)
}
}
Step 8: Update LiveStreamViewController​
Ensure your LiveStreamViewController has:
- A reactions array:
@Published var reactions: [String] = []
- A sendTheReaction method:
func sendTheReaction(_ emoji: Emoji) {
print("Sending reaction: \(emoji.value)")
self.meeting?.pubsub.publish(topic: "REACTION", message: emoji.value, options: [:])
}
- PubSub handling in the onMessageReceived method:
func onMessageReceived(_ message: VideoSDKRTC.PubSubMessage) {
print("Message Received:= " + message.message)
if message.topic == "REACTION" {
DispatchQueue.main.async {
print("Received reaction: \(message.message)")
self.reactions.append(message.message)
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if let index = self.reactions.firstIndex(of: message.message) {
self.reactions.remove(at: index)
}
}
}
}
// Your other message handlers...
}
- PubSub subscription in onMeetingJoined:
func onMeetingJoined() {
// Your existing code...
Task {
await meeting?.pubsub.subscribe(topic: "REACTION", forListener: self)
// Your other subscriptions...
}
}
How It Works​
- When a user selects an emoji in audience mode, it's sent via PubSub
- All clients (including the sender) receive the emoji via PubSub
- When received, multiple copies of the emoji are created with randomized paths
- Emojis float upward with randomized animations
- Emojis are automatically removed after their animation completes
- The total number of visible emojis is limited to prevent performance issues
Customization​
You can customize the animations by modifying:
- Number of copies per reaction (
copiesPerReaction
) - Maximum visible reactions (
maxVisibleReactions
) - Animation duration in the
FloatingAnimation
modifier - Emoji size and starting position
- Random offset ranges for horizontal movement
Got a Question? Ask us on discord