使用FLAnimatedImage实现鱼类往返动画失效问题排查
解决鱼类往返游动动画的问题
看起来你在给鱼类添加往返翻转动画时遇到了麻烦,我先梳理下你代码里的几个核心问题,然后给你一个修复后的版本:
你的代码里的主要问题
- 嵌套函数与作用域混乱:在
for numFish in 1...numberOfFish循环内部定义goLeft、goRight这些函数,每次循环都会重新定义它们,而且它们引用的fish3是循环内的局部变量,多次循环后会导致引用错乱,动画行为不可控。 - 重复加载GIF资源:每次循环都重新读取本地GIF文件创建
FLAnimatedImage实例,这不仅浪费性能,还会导致不必要的内存占用。 - 动画选项冲突:你同时使用了
.repeat和在completion里触发下一个动画,.repeat的动画不会触发completion回调(除非动画被手动停止),这会导致你的往返逻辑根本无法正常触发。 - FLAnimatedImage切换方式不当:直接替换
animatedImage属性虽然可行,但没有考虑动画的连续性,而且你只操作了fish3,循环里创建的fish2完全没被使用,还会造成内存泄漏。
修复后的代码示例
首先,提前在类的属性里加载GIF资源,避免重复读取:
class YourViewController: UIViewController { // 提前加载GIF资源,复用实例 private let fishGif1: FLAnimatedImage! private let fishGif2: FLAnimatedImage! // 存储所有鱼的实例,避免引用丢失 private var fishViews = [FLAnimatedImageView]() override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { // 初始化时加载GIF let path1 = Bundle.main.path(forResource: "fishgif", ofType: "gif")! let data1 = try! Data(contentsOf: URL(fileURLWithPath: path1)) fishGif1 = FLAnimatedImage(animatedGIFData: data1) let path2 = Bundle.main.path(forResource: "fishFriendtoo", ofType: "gif")! let data2 = try! Data(contentsOf: URL(fileURLWithPath: path2)) fishGif2 = FLAnimatedImage(animatedGIFData: data2) super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } required init?(coder: NSCoder) { // 初始化时加载GIF let path1 = Bundle.main.path(forResource: "fishgif", ofType: "gif")! let data1 = try! Data(contentsOf: URL(fileURLWithPath: path1)) fishGif1 = FLAnimatedImage(animatedGIFData: data1) let path2 = Bundle.main.path(forResource: "fishFriendtoo", ofType: "gif")! let data2 = try! Data(contentsOf: URL(fileURLWithPath: path2)) fishGif2 = FLAnimatedImage(animatedGIFData: data2) super.init(coder: coder) } @IBAction func animateFish(_ sender: Any) { guard let numFishSlider = numFish else { return } let numberOfFish = Int(roundf(numFishSlider.value)) for _ in 1...numberOfFish { // 随机生成鱼的尺寸和初始位置 let wFish = (Int(arc4random() % 10) + 1) * 20 // 20-200 let hFish = (wFish * 3) / 4 let xStart = -wFish let xEnd = view.bounds.width + wFish let yPos = Int(arc4random() % UInt32(view.bounds.height - hFish)) + hFish/2 // 确保鱼在屏幕内 // 创建鱼的ImageView let fishView = FLAnimatedImageView() fishView.animatedImage = fishGif1 fishView.frame = CGRect(x: xStart, y: yPos, width: wFish, height: hFish) view.addSubview(fishView) fishViews.append(fishView) // 启动该鱼的往返动画 startSwimCycle(for: fishView, startX: xStart, endX: xEnd, yPos: yPos, width: wFish, height: hFish) } } // 封装单个鱼的往返动画逻辑 private func startSwimCycle(for fishView: FLAnimatedImageView, startX: Int, endX: Int, yPos: Int, width: Int, height: Int) { // 先游向右 let swimRightDuration = Double(arc4random() % 10) + 5 // 5-15秒,避免过快 let swimRightDelay = Double(arc4random() % 2) UIView.animate(withDuration: swimRightDuration, delay: swimRightDelay, options: .curveEaseInOut, animations: { fishView.frame = CGRect(x: endX, y: yPos, width: width, height: height) }, completion: { [weak self] _ in guard let self = self else { return } // 切换到翻转后的GIF fishView.animatedImage = self.fishGif2 // 再游向左 self.swimLeft(for: fishView, startX: startX, endX: endX, yPos: yPos, width: width, height: height) }) } private func swimLeft(for fishView: FLAnimatedImageView, startX: Int, endX: Int, yPos: Int, width: Int, height: Int) { let swimLeftDuration = Double(arc4random() % 10) + 5 let swimLeftDelay = Double(arc4random() % 2) UIView.animate(withDuration: swimLeftDuration, delay: swimLeftDelay, options: .curveEaseInOut, animations: { fishView.frame = CGRect(x: startX, y: yPos, width: width, height: height) }, completion: { [weak self] _ in guard let self = self else { return } // 切换回原GIF fishView.animatedImage = self.fishGif1 // 循环往复 self.startSwimCycle(for: fishView, startX: startX, endX: endX, yPos: yPos, width: width, height: height) }) } }
关键修复点说明
- 提前加载资源:在视图控制器初始化时就加载好两个GIF的
FLAnimatedImage实例,循环里直接复用,提升性能。 - 封装动画逻辑:把单个鱼的往返动画拆成
startSwimCycle和swimLeft方法,每个鱼实例独立管理自己的动画,避免作用域混乱。 - 移除.repeat选项:改用
completion回调手动触发反向动画,确保每次动画完成后都能切换GIF并启动下一段动画。 - 存储鱼实例:用
fishViews数组保存所有创建的FLAnimatedImageView,避免循环结束后实例被释放,同时方便后续清理(比如停止动画时)。 - 优化位置计算:使用
view.bounds.width来适配不同屏幕尺寸,而不是硬编码380或450,让动画在不同设备上都能正常显示。
这样调整后,你的鱼类往返游动加翻转的动画应该就能正常工作了,而且代码结构更清晰,也避免了内存泄漏的问题。
内容的提问来源于stack exchange,提问作者Vera Boukhonine
相关产品推荐
相关产品推荐

