Swift代理赋值self遇nil解包错误:跨类控制UIImageView与AVAudioPlayer
解决AVAudioPlayer代理设置时的nil崩溃问题
看起来你在跨类通信时遇到了代理设置的致命错误,我帮你拆解下问题所在,一步步解决:
首先看Card类里的几个关键问题
- 错误手动调用代理方法:你在
playSound()里直接调用了audioPlayerDidFinishPlaying(player),这完全没必要——AVAudioPlayer的代理方法是当播放结束时由系统自动触发的,手动调用不仅逻辑错误,还可能在代理还没设置好的时候执行,导致打印"the delegate is not set"。 - 未设置AVAudioPlayer的代理:你注释掉了
player?.delegate = self,这会导致播放结束时系统根本不会通知Card类,自然也不会触发代理回调给ViewController。 - 代理未使用weak引用:当前
delegate是强引用,如果ViewController持有Card实例,会形成循环引用,导致内存泄漏,而且协议没有类限制,可能引发不必要的问题。
修复Card类的代码
先把Card类调整正确:
import Foundation import UIKit import AVFoundation // 加上class限制,确保只有类能遵守,同时代理用weak避免循环引用 protocol isOnProtocol: class { func isOn() } class Card: NSObject { weak var delegate: isOnProtocol? // 使用weak var player: AVAudioPlayer? var image: UIImage var soundUrl: String init(image: UIImage, soundUrl: String, isActive: Bool = true) { self.image = image self.soundUrl = soundUrl super.init() // 显式调用NSObject的初始化方法,更安全 } func playSound() { guard let url = Bundle.main.url(forResource: self.soundUrl, withExtension: "m4a") else { print("找不到音频文件:\(soundUrl).m4a") // 播放失败时恢复imageView状态,避免用户无法操作 delegate?.isOn() return } do { try AVAudioSession.sharedInstance().setCategory(.playback) try AVAudioSession.sharedInstance().setActive(true) player = try AVAudioPlayer(contentsOf: url) player?.delegate = self // 打开代理设置 guard let player = player else { return } player.prepareToPlay() player.play() print("开始播放音频") } catch let error { print("播放失败:\(error.localizedDescription)") delegate?.isOn() } } } extension Card: AVAudioPlayerDelegate { func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { // 系统触发的代理方法,带成功标志更严谨 if flag { print("播放完成") } else { print("播放未成功完成") } delegate?.isOn() } }
ViewController里的正确实现
接下来确保ViewController里的Card实例初始化和代理设置正确,避免nil崩溃:
class ViewController: UIViewController, isOnProtocol { // 确保imageView和card都正确初始化 var card: Card! var imageView: UIImageView! override func viewDidLoad() { super.viewDidLoad() // 初始化imageView(示例代码,根据你的实际布局调整) imageView = UIImageView(frame: CGRect(x: 50, y: 50, width: 200, height: 200)) imageView.image = UIImage(named: "your_card_image") imageView.isUserInteractionEnabled = true let tapGesture = UITapGestureRecognizer(target: self, action: #selector(imageViewTapped)) imageView.addGestureRecognizer(tapGesture) view.addSubview(imageView) // 初始化Card实例,确保图片和音频文件名正确 guard let cardImage = UIImage(named: "your_card_image") else { print("找不到卡片图片") return } card = Card(image: cardImage, soundUrl: "your_audio_file_name") card.delegate = self // 此时card已初始化,不会出现nil崩溃 } // 点击imageView时禁用它并播放声音 @objc func imageViewTapped() { // 禁用imageView的交互和视觉反馈 imageView.isUserInteractionEnabled = false imageView.alpha = 0.5 // 播放音频 card.playSound() } // 实现代理方法,播放完成后恢复imageView func isOn() { DispatchQueue.main.async { // 确保在主线程更新UI self.imageView.isUserInteractionEnabled = true self.imageView.alpha = 1.0 print("恢复imageView可操作状态") } } }
排查nil崩溃的核心要点
如果还是出现崩溃,检查这几点:
- 确保
card在设置card.delegate = self之前已经被完全初始化,不要用未初始化的隐式可选类型(Card!)直接设置代理。 - 检查音频文件是否真的存在于项目Bundle中,并且文件名和
soundUrl完全一致(iOS区分文件名大小写)。 - 确保ViewController遵守了
isOnProtocol协议,并且实现了isOn()方法,否则编译器会报错,但运行时的nil崩溃大概率是Card实例未初始化导致的。
内容的提问来源于stack exchange,提问作者Anthony Rubin
相关产品推荐
相关产品推荐

