iOS 直播弹幕礼物功能详解

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

核心实现方案

  1. 礼物弹幕数据模型
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          // 特殊特效 
    }
}
  1. 礼物弹幕视图
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 
    }
}
  1. 礼物连击效果处理
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")
}
  1. 礼物选择面板
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 
        }
    }
}

高级特效实现

  1. 全屏礼物特效
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()
                }
            }
        }
    }
}
  1. 粒子特效
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) {
        // 更新连击计数显示 
    }
}

服务器端实现要点

  1. 礼物数据结构:
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"
}
  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
    }

  2. 礼物广播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 
  }
}

注意事项

  1. 性能优化:

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

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

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

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

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

相关推荐
晓风伴月4 小时前
微信小程序:列表项上同样的css样式在IOS上字体大小不一样
css·ios·微信小程序
Li_Ning215 小时前
【uniapp】 iosApp开发xcode原生配置项(iOS平台Capabilities配置)
ios·uni-app·xcode
serve the people7 小时前
PyInstaller 如何在mac电脑上生成在window上可执行的exe文件
macos
小鹿撞出了脑震荡12 小时前
iOS工厂模式
学习·ios·objective-c
Dingyin HU18 小时前
使用RUST在Arduino上进行编程(MacOS,mega板)
macos·rust·arduino
Jackson@ML1 天前
2025最新版Visual Studio Code for Mac安装使用指南
ide·vscode·macos
gadiaola1 天前
【苍穹外卖】Day01—Mac前端环境搭建
前端·nginx·macos·homebrew
猫头虎1 天前
MacBookPro上macOS安装第三方应用报错解决方案:遇到:“无法打开“XXX”,因为Apple无法检查其是否包含恶意软件 问题如何解决
macos·开源软件·mac·策略模式·远程工作·软件需求·安全架构
I烟雨云渊T1 天前
iOS 直播弹幕功能的实现
ios