当前位置: 首页 > news >正文

iOS 直播弹幕礼物功能详解

弹幕礼物功能是直播平台的重要互动方式,它结合了弹幕和礼物打赏的特性。下面我将详细介绍如何在iOS应用中实现弹幕礼物功能。

核心实现方案

  1. 礼物弹幕数据模型
struct GiftBarrageModel {let giftId: String          // 礼物ID let giftName: String        // 礼物名称 let giftIconURL: String     // 礼物图标URL let giftPrice: Double       // 礼物价格 let senderName: String      // 发送者昵称 let senderAvatar: String    // 发送者头像 let count: Int             // 礼物数量 let comboId: String        // 连击ID let timestamp: TimeInterval // 时间戳 let effectType: GiftEffectType // 特效类型 enum GiftEffectType: Int {case none = 0          // 无特效 case smallAnimation   // 小动画 case fullScreen       // 全屏特效 case special          // 特殊特效 }
}
  1. 礼物弹幕视图
class GiftBarrageView: UIView {private var giftBarrageQueue: [GiftBarrageModel] = []private var isDisplaying = false func addGiftBarrage(_ model: GiftBarrageModel) {giftBarrageQueue.append(model)if !isDisplaying {displayNextGiftBarrage()}}private func displayNextGiftBarrage() {guard !giftBarrageQueue.isEmpty else {isDisplaying = false return }isDisplaying = true let model = giftBarrageQueue.removeFirst()let barrageView = createGiftBarrageView(model)addSubview(barrageView)// 初始位置在屏幕右侧外 barrageView.frame.origin.x = bounds.width UIView.animate(withDuration: 0.5, animations: {barrageView.frame.origin.x = self.bounds.width - barrageView.frame.width - 20 }) { _ in // 停留3秒 DispatchQueue.main.asyncAfter(deadline: .now() + 3) {UIView.animate(withDuration: 0.5, animations: {barrageView.frame.origin.x = -barrageView.frame.width }) { _ in barrageView.removeFromSuperview()self.displayNextGiftBarrage()}}}}private func createGiftBarrageView(_ model: GiftBarrageModel) -> UIView {let container = UIView()container.backgroundColor = UIColor.black.withAlphaComponent(0.5)container.layer.cornerRadius = 15 container.clipsToBounds = true // 发送者头像 let avatarView = UIImageView()avatarView.contentMode = .scaleAspectFill avatarView.layer.cornerRadius = 12 avatarView.clipsToBounds = true avatarView.loadImage(from: model.senderAvatar)avatarView.frame = CGRect(x: 8, y: 6, width: 24, height: 24)container.addSubview(avatarView)// 发送者名称 let nameLabel = UILabel()nameLabel.text = model.senderName nameLabel.font = UIFont.systemFont(ofSize: 12)nameLabel.textColor = .white nameLabel.frame = CGRect(x: 40, y: 6, width: 80, height: 15)container.addSubview(nameLabel)// 礼物图标 let giftIcon = UIImageView()giftIcon.contentMode = .scaleAspectFit giftIcon.loadImage(from: model.giftIconURL)giftIcon.frame = CGRect(x: 40, y: 24, width: 20, height: 20)container.addSubview(giftIcon)// 礼物描述 let giftDescLabel = UILabel()giftDescLabel.text = "送出 \(model.giftName)"giftDescLabel.font = UIFont.systemFont(ofSize: 12)giftDescLabel.textColor = .white giftDescLabel.frame = CGRect(x: 65, y: 24, width: 100, height: 15)container.addSubview(giftDescLabel)// 礼物数量 let countLabel = UILabel()countLabel.text = "x\(model.count)"countLabel.font = UIFont.boldSystemFont(ofSize: 16)countLabel.textColor = UIColor(hex: "#FFD700")countLabel.frame = CGRect(x: 170, y: 12, width: 40, height: 20)container.addSubview(countLabel)// 容器大小 container.frame.size = CGSize(width: 220, height: 36)return container }
}
  1. 礼物连击效果处理
class GiftComboManager {private var comboDict: [String: (model: GiftBarrageModel, count: Int, timer: Timer?)] = [:]func addGift(_ model: GiftBarrageModel) {if let existing = comboDict[model.comboId] {// 已有连击,更新数量 comboDict[model.comboId]?.count += model.count comboDict[model.comboId]?.timer?.invalidate()// 重置计时器 startComboTimer(for: model.comboId)// 更新显示 updateDisplay(for: model.comboId)} else {// 新连击 comboDict[model.comboId] = (model: model, count: model.count, timer: nil)startComboTimer(for: model.comboId)// 首次显示 displayNewCombo(model)}}private func startComboTimer(for comboId: String) {let timer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { [weak self] _ in self?.endCombo(for: comboId)}comboDict[comboId]?.timer = timer }private func updateDisplay(for comboId: String) {guard let combo = comboDict[comboId] else { return }// 更新UI显示最新的连击数 NotificationCenter.default.post(name: .giftComboUpdated,object: nil,userInfo: ["comboId": comboId,"count": combo.count,"model": combo.model ])}private func displayNewCombo(_ model: GiftBarrageModel) {// 通知显示新连击 NotificationCenter.default.post(name: .newGiftCombo,object: nil,userInfo: ["model": model])}private func endCombo(for comboId: String) {guard let combo = comboDict[comboId] else { return }// 通知连击结束 NotificationCenter.default.post(name: .giftComboEnded,object: nil,userInfo: ["comboId": comboId,"finalCount": combo.count,"model": combo.model ])comboDict.removeValue(forKey: comboId)}
}extension Notification.Name {static let newGiftCombo = Notification.Name("newGiftCombo")static let giftComboUpdated = Notification.Name("giftComboUpdated")static let giftComboEnded = Notification.Name("giftComboEnded")
}
  1. 礼物选择面板
class GiftSelectionView: UIView {private let gifts: [GiftModel]private var collectionView: UICollectionView!private var sendButton: UIButton!private var countLabel: UILabel!private var selectedGift: GiftModel?private var selectedCount = 1 init(gifts: [GiftModel]) {self.gifts = gifts super.init(frame: .zero)setupUI()}private func setupUI() {backgroundColor = UIColor(hex: "#1A1A1A")layer.cornerRadius = 12 clipsToBounds = true // 标题 let titleLabel = UILabel()titleLabel.text = "选择礼物"titleLabel.textColor = .white titleLabel.font = UIFont.boldSystemFont(ofSize: 16)titleLabel.frame = CGRect(x: 15, y: 15, width: 100, height: 20)addSubview(titleLabel)// 关闭按钮 let closeButton = UIButton(type: .system)closeButton.setImage(UIImage(named: "close_icon"), for: .normal)closeButton.tintColor = .white closeButton.frame = CGRect(x: bounds.width - 35, y: 15, width: 20, height: 20)closeButton.addTarget(self, action: #selector(close), for: .touchUpInside)addSubview(closeButton)// 礼物列表 let layout = UICollectionViewFlowLayout()layout.itemSize = CGSize(width: 60, height: 80)layout.minimumInteritemSpacing = 10 layout.minimumLineSpacing = 10 layout.sectionInset = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15)collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)collectionView.backgroundColor = .clear collectionView.register(GiftCell.self, forCellWithReuseIdentifier: "GiftCell")collectionView.dataSource = self collectionView.delegate = self collectionView.frame = CGRect(x: 0, y: 50, width: bounds.width, height: 180)addSubview(collectionView)// 数量选择 let minusButton = UIButton(type: .system)minusButton.setTitle("-", for: .normal)minusButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)minusButton.tintColor = .white minusButton.backgroundColor = UIColor(hex: "#333333")minusButton.layer.cornerRadius = 15 minusButton.frame = CGRect(x: 15, y: 240, width: 30, height: 30)minusButton.addTarget(self, action: #selector(decreaseCount), for: .touchUpInside)addSubview(minusButton)countLabel = UILabel()countLabel.text = "1"countLabel.textColor = .white countLabel.textAlignment = .center countLabel.font = UIFont.systemFont(ofSize: 16)countLabel.frame = CGRect(x: 55, y: 240, width: 40, height: 30)addSubview(countLabel)let plusButton = UIButton(type: .system)plusButton.setTitle("+", for: .normal)plusButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)plusButton.tintColor = .white plusButton.backgroundColor = UIColor(hex: "#333333")plusButton.layer.cornerRadius = 15 plusButton.frame = CGRect(x: 105, y: 240, width: 30, height: 30)plusButton.addTarget(self, action: #selector(increaseCount), for: .touchUpInside)addSubview(plusButton)// 发送按钮 sendButton = UIButton(type: .system)sendButton.setTitle("发送", for: .normal)sendButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)sendButton.tintColor = .white sendButton.backgroundColor = UIColor(hex: "#FF2D55")sendButton.layer.cornerRadius = 18 sendButton.frame = CGRect(x: bounds.width - 115, y: 240, width: 100, height: 36)sendButton.addTarget(self, action: #selector(sendGift), for: .touchUpInside)addSubview(sendButton)}@objc private func close() {removeFromSuperview()}@objc private func decreaseCount() {if selectedCount > 1 {selectedCount -= 1 countLabel.text = "\(selectedCount)"updateSendButton()}}@objc private func increaseCount() {selectedCount += 1 countLabel.text = "\(selectedCount)"updateSendButton()}@objc private func sendGift() {guard let gift = selectedGift else { return }let barrageModel = GiftBarrageModel(giftId: gift.id,giftName: gift.name,giftIconURL: gift.iconURL,giftPrice: gift.price,senderName: User.current.nickname,senderAvatar: User.current.avatarURL,count: selectedCount,comboId: "\(User.current.id)_\(gift.id)",timestamp: Date().timeIntervalSince1970,effectType: gift.effectType )// 发送到服务器 APIManager.sendGift(giftId: gift.id, count: selectedCount) { success in if success {// 通知显示礼物弹幕 NotificationCenter.default.post(name: .showGiftBarrage,object: nil,userInfo: ["model": barrageModel])self.close()}}}private func updateSendButton() {guard let gift = selectedGift else {sendButton.setTitle("发送", for: .normal)sendButton.backgroundColor = UIColor(hex: "#666666")sendButton.isEnabled = false return }let totalPrice = gift.price * Double(selectedCount)sendButton.setTitle(\(totalPrice)", for: .normal)sendButton.backgroundColor = UIColor(hex: "#FF2D55")sendButton.isEnabled = true }
}extension GiftSelectionView: UICollectionViewDataSource, UICollectionViewDelegate {func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {return gifts.count }func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GiftCell", for: indexPath) as! GiftCell cell.configure(with: gifts[indexPath.item])cell.isSelected = selectedGift?.id == gifts[indexPath.item].id return cell }func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {selectedGift = gifts[indexPath.item]selectedCount = 1 countLabel.text = "1"updateSendButton()collectionView.reloadData()}
}class GiftCell: UICollectionViewCell {private let iconView = UIImageView()private let nameLabel = UILabel()private let priceLabel = UILabel()override init(frame: CGRect) {super.init(frame: frame)iconView.contentMode = .scaleAspectFit iconView.frame = CGRect(x: 10, y: 0, width: 40, height: 40)contentView.addSubview(iconView)nameLabel.textAlignment = .center nameLabel.font = UIFont.systemFont(ofSize: 12)nameLabel.textColor = .white nameLabel.frame = CGRect(x: 0, y: 45, width: 60, height: 15)contentView.addSubview(nameLabel)priceLabel.textAlignment = .center priceLabel.font = UIFont.systemFont(ofSize: 12)priceLabel.textColor = UIColor(hex: "#FFD700")priceLabel.frame = CGRect(x: 0, y: 60, width: 60, height: 15)contentView.addSubview(priceLabel)}func configure(with gift: GiftModel) {iconView.loadImage(from: gift.iconURL)nameLabel.text = gift.name priceLabel.text = \(gift.price)"if isSelected {backgroundColor = UIColor(hex: "#FF2D55").withAlphaComponent(0.3)layer.borderWidth = 1 layer.borderColor = UIColor(hex: "#FF2D55").cgColor layer.cornerRadius = 4 } else {backgroundColor = .clear layer.borderWidth = 0 }}
}

高级特效实现

  1. 全屏礼物特效
class FullScreenGiftEffectView: UIView {static func show(for model: GiftBarrageModel, in view: UIView) {let effectView = FullScreenGiftEffectView(model: model)effectView.frame = view.bounds view.addSubview(effectView)effectView.animate()}private let model: GiftBarrageModel init(model: GiftBarrageModel) {self.model = model super.init(frame: .zero)setupUI()}private func setupUI() {backgroundColor = UIColor.black.withAlphaComponent(0.7)// 礼物图标 let giftImageView = UIImageView()giftImageView.contentMode = .scaleAspectFit giftImageView.loadImage(from: model.giftIconURL)giftImageView.frame = CGRect(x: 0, y: 0, width: 150, height: 150)giftImageView.center = center addSubview(giftImageView)// 发送者信息 let senderLabel = UILabel()senderLabel.text = "\(model.senderName) 送出了"senderLabel.textColor = .white senderLabel.font = UIFont.boldSystemFont(ofSize: 20)senderLabel.textAlignment = .center senderLabel.frame = CGRect(x: 0, y: center.y - 100, width: bounds.width, height: 30)addSubview(senderLabel)// 礼物名称 let giftNameLabel = UILabel()giftNameLabel.text = model.giftName giftNameLabel.textColor = UIColor(hex: "#FFD700")giftNameLabel.font = UIFont.boldSystemFont(ofSize: 24)giftNameLabel.textAlignment = .center giftNameLabel.frame = CGRect(x: 0, y: center.y + 100, width: bounds.width, height: 30)addSubview(giftNameLabel)// 数量 if model.count > 1 {let countLabel = UILabel()countLabel.text = "x\(model.count)"countLabel.textColor = UIColor(hex: "#FF2D55")countLabel.font = UIFont.boldSystemFont(ofSize: 30)countLabel.textAlignment = .center countLabel.frame = CGRect(x: 0, y: center.y + 150, width: bounds.width, height: 40)addSubview(countLabel)}}func animate() {alpha = 0 transform = CGAffineTransform(scaleX: 0.5, y: 0.5)UIView.animate(withDuration: 0.3, animations: {self.alpha = 1 self.transform = .identity }) { _ in DispatchQueue.main.asyncAfter(deadline: .now() + 2) {UIView.animate(withDuration: 0.3, animations: {self.alpha = 0 }) { _ in self.removeFromSuperview()}}}}
}
  1. 粒子特效
class ParticleGiftEffectView: UIView {static func show(for model: GiftBarrageModel, in view: UIView) {let effectView = ParticleGiftEffectView(model: model)effectView.frame = view.bounds view.addSubview(effectView)effectView.startAnimation()}private let model: GiftBarrageModel private var emitterLayer: CAEmitterLayer!init(model: GiftBarrageModel) {self.model = model super.init(frame: .zero)setupEmitterLayer()}private func setupEmitterLayer() {emitterLayer = CAEmitterLayer()emitterLayer.emitterPosition = CGPoint(x: bounds.midX, y: bounds.maxY)emitterLayer.emitterSize = CGSize(width: bounds.width, height: 0)emitterLayer.emitterShape = .line emitterLayer.renderMode = .additive emitterLayer.beginTime = CACurrentMediaTime()let cell = CAEmitterCell()cell.contents = UIImage(named: "gift_particle")?.cgImage cell.birthRate = 20 cell.lifetime = 10 cell.velocity = 100 cell.velocityRange = 50 cell.emissionLongitude = .pi cell.emissionRange = .pi / 4 cell.scale = 0.2 cell.scaleRange = 0.1 cell.spin = 2 cell.spinRange = 3 cell.yAcceleration = -50 emitterLayer.emitterCells = [cell]layer.addSublayer(emitterLayer)}func startAnimation() {let duration: TimeInterval = 3 // 停止发射 DispatchQueue.main.asyncAfter(deadline: .now() + duration) {self.emitterLayer.birthRate = 0 }// 移除视图 DispatchQueue.main.asyncAfter(deadline: .now() + duration + 5) {self.removeFromSuperview()}}
}

完整集成示例

class LiveViewController: UIViewController {private var giftBarrageView: GiftBarrageView!private var giftComboManager: GiftComboManager!override func viewDidLoad() {super.viewDidLoad()setupGiftViews()setupNotifications()}private func setupGiftViews() {giftBarrageView = GiftBarrageView()giftBarrageView.frame = CGRect(x: 0, y: 100, width: view.bounds.width, height: 200)view.addSubview(giftBarrageView)giftComboManager = GiftComboManager()}private func setupNotifications() {NotificationCenter.default.addObserver(self,selector: #selector(handleNewGiftBarrage(_:)),name: .showGiftBarrage,object: nil )NotificationCenter.default.addObserver(self,selector: #selector(handleNewGiftCombo(_:)),name: .newGiftCombo,object: nil )NotificationCenter.default.addObserver(self,selector: #selector(handleGiftComboUpdated(_:)),name: .giftComboUpdated,object: nil )}@objc private func handleNewGiftBarrage(_ notification: Notification) {guard let model = notification.userInfo?["model"] as? GiftBarrageModel else { return }// 根据特效类型显示不同效果 switch model.effectType {case .fullScreen:FullScreenGiftEffectView.show(for: model, in: view)case .special:ParticleGiftEffectView.show(for: model, in: view)default:break }// 添加到连击管理器 giftComboManager.addGift(model)// 显示弹幕 giftBarrageView.addGiftBarrage(model)}@objc private func handleNewGiftCombo(_ notification: Notification) {guard let model = notification.userInfo?["model"] as? GiftBarrageModel else { return }// 显示连击开始UI showComboStartView(for: model)}@objc private func handleGiftComboUpdated(_ notification: Notification) {guard let comboId = notification.userInfo?["comboId"] as? String,let count = notification.userInfo?["count"] as? Int,let model = notification.userInfo?["model"] as? GiftBarrageModel else { return }// 更新连击计数显示 updateComboCount(comboId, count: count, model: model)}@IBAction func showGiftPanel(_ sender: UIButton) {APIManager.fetchGiftList { [weak self] gifts in guard let self = self else { return }let giftView = GiftSelectionView(gifts: gifts)giftView.frame = CGRect(x: 0, y: self.view.bounds.height - 300, width: self.view.bounds.width, height: 300)self.view.addSubview(giftView)}}// 连击开始视图 private func showComboStartView(for model: GiftBarrageModel) {// 实现连击开始动画 }// 更新连击计数 private func updateComboCount(_ comboId: String, count: Int, model: GiftBarrageModel) {// 更新连击计数显示 }
}

服务器端实现要点

  1. 礼物数据结构:
{"id": "gift_001","name": "火箭","icon_url": "https://example.com/gifts/rocket.png","price": 100.0,"effect_type": 2,"combo_support": true,"weight": 10,"category": "luxury"
}
  1. 礼物发送API:
POST /api/live/{live_id}/send_gift Request:
{"gift_id": "gift_001","count": 3,"combo_id": "user123_gift001" // 可选,用于连击 
}Response:
{"success": true,"combo_count": 5, // 当前连击数 "total_cost": 500.0,"user_balance": 1500.0 
}
  1. 礼物广播WebSocket消息:
{"type": "gift","data": {"gift_id": "gift_001","sender_id": "user123","sender_name": "张三","sender_avatar": "https://example.com/avatars/user123.jpg","count": 3,"combo_id": "user123_gift001","combo_count": 5,"timestamp": 1630000000,"effect_type": 2 }
}

注意事项

  1. 性能优化:

    • 使用对象池复用礼物弹幕视图
    • 限制同时显示的特效数量
    • 对复杂特效使用Metal或SpriteKit提高性能
  2. 内存管理:

    • 及时移除不可见的特效视图
    • 使用weak引用避免循环引用
    • 对大量礼物数据使用分页加载
  3. 用户体验:

    • 提供礼物预览功能
    • 实现礼物连击动画
    • 支持礼物屏蔽功能
    • 添加礼物发送确认弹窗
  4. 支付安全:

    • 客户端验证用户余额
    • 服务端二次验证
    • 记录详细的礼物交易日志

通过以上方案,你可以实现一个功能丰富、性能良好的iOS直播弹幕礼物系统。根据实际需求,你可以进一步扩展功能,如礼物排行榜、特殊礼物特效等。

相关文章:

  • 多模态AI终极形态?GPT-5与Stable Diffusion 3的融合实验报告
  • 【49. 字母异位词分组】
  • iOS 上线前的性能与稳定性检查流程实录:开发者的“最后一公里”(含 KeyMob 应用经验)
  • C# 高效读取大文件
  • Spark on Yarn 高可用模式部署流程
  • 如何提高服务器的QPS来应对618活动的并发流量
  • 如何将带有LFS对象的git仓库推送到gitlab
  • 前端(小程序)学习笔记(CLASS 2):WXML模板语法与WXSS模板样式
  • C语言实现顺序存储结构
  • PostgreSQL中的权限管理简介
  • Python爬虫(35)Python爬虫高阶:基于Docker集群的动态页面自动化采集系统实战
  • Terraform创建阿里云基础组件资源
  • Java SpringBoot 扣子CozeAI SseEmitter流式对话完整实战 打字机效果
  • Android 网络全栈攻略(五)—— 从 OkHttp 拦截器来看 HTTP 协议二
  • 使用OpenSSL生成根证书并自签署证书
  • 数据结构(6)线性表-队列
  • 【leetcode】3356. 零数组变换②
  • 字节跳动旗下火山引擎都覆盖哪些领域
  • 四、GPU是如何成为当前电脑中不可或缺的一部分的,opengl在其中起到了什么效果
  • 基于SpringMVC的动态时钟设计
  • 做网站有哪些软件/网络营销企业是什么
  • 网站换空间要重新备案吗/网站快速被百度收录
  • 天元建设集团有限公司股份/沈阳seo排名公司
  • 网站制作现在赚钱么/徐州百度推广公司
  • 网站 app 共同架构/广告公司网上接单平台
  • 微网站建设教程/seo运营人士揭秘