弹幕礼物功能是直播平台的重要互动方式,它结合了弹幕和礼物打赏的特性。下面我将详细介绍如何在iOS应用中实现弹幕礼物功能。
核心实现方案
- 礼物弹幕数据模型
swift
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 // 特殊特效
}
}
- 礼物弹幕视图
swift
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
}
}
- 礼物连击效果处理
swift
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")
}
- 礼物选择面板
swift
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
}
}
}
高级特效实现
- 全屏礼物特效
swift
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()
}
}
}
}
}
- 粒子特效
swift
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()
}
}
}
完整集成示例
swift
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) {
// 更新连击计数显示
}
}
服务器端实现要点
- 礼物数据结构:
json
{
"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"
}
-
礼物发送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
} -
礼物广播WebSocket消息:
json
{
"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
}
}
注意事项
-
性能优化:
- 使用对象池复用礼物弹幕视图
- 限制同时显示的特效数量
- 对复杂特效使用Metal或SpriteKit提高性能
-
内存管理:
- 及时移除不可见的特效视图
- 使用weak引用避免循环引用
- 对大量礼物数据使用分页加载
-
用户体验:
- 提供礼物预览功能
- 实现礼物连击动画
- 支持礼物屏蔽功能
- 添加礼物发送确认弹窗
-
支付安全:
- 客户端验证用户余额
- 服务端二次验证
- 记录详细的礼物交易日志
通过以上方案,你可以实现一个功能丰富、性能良好的iOS直播弹幕礼物系统。根据实际需求,你可以进一步扩展功能,如礼物排行榜、特殊礼物特效等。