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直播弹幕礼物系统。根据实际需求,你可以进一步扩展功能,如礼物排行榜、特殊礼物特效等。

相关推荐
goodSleep3 小时前
更新Mac OS Tahoe26用命令恢复 Mac 启动台时不小心禁用了聚焦搜索
macos
叽哥9 小时前
Flutter Riverpod上手指南
android·flutter·ios
用户091 天前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan1 天前
iOS26适配指南之UIColor
ios·swift
权咚2 天前
阿权的开发经验小集
git·ios·xcode
用户092 天前
TipKit与CloudKit同步完全指南
ios·swift
小溪彼岸2 天前
macOS自带截图命令ScreenCapture
macos
法的空间2 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
2501_915918412 天前
iOS 上架全流程指南 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核实战经验分享
android·ios·小程序·uni-app·cocoa·iphone·webview
TESmart碲视2 天前
Mac 真正多显示器支持:TESmart USB-C KVM(搭载 DisplayLink 技术)如何实现
macos·计算机外设·电脑