

[![アニメーションの一部分の画像](https://i.stack.imgur.com/ARHqQ.png)](https://i.stack.imgur.com/ARHqQ.png)

画像のようにパーセンテージを表す部分のテキストをアニメーションさせるためにCADisplayLinkを利用して実装したのですが、RxSwiftと上手く併用できず、画面遷移時に当該Cellのインスタンスを破棄することができません。subscribe内で呼び出している  
let displayLink = CADisplayLink(target: self, selector:
#selector(handleUpdate))  
の部分のselfの影響で循環参照が起き、deinitができないのかと思い色々と調べたのですが解決することができませんでした。  
下記のコードのようにsubscribe内からアニメーションを呼び出すような実装は、Rxを使った実装ではあまりされないものでしょうか？  
cellをdeinitするために、アドバイスをいただけないでしょうか。  
よろしくお願いいたします。

[code]

    import RxSwift
    import RxCocoa
    
    class AchievementRateCell: UITableViewCell {
        
        static let cellId = "AchievementRateCellId"
        
        let percentageNumberLabel: UILabel = {
            let label = UILabel()
            label.text = "0%"
            label.textAlignment = .center
            label.font = .systemFont(ofSize: 20)
            label.lastLetterToSmall(value: label.text!)
            return label
        }()
        
        let achievementRateCircleView = CircleProgressView()
        
        deinit {
            print("AchievementRateCell deinit")
        }
        
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            print("AchievementRateCell init")
            
            let stackView = UIStackView(arrangedSubviews: [achievementRateCircleView, percentageNumberLabel])
            addSubview(stackView)
            stackView.topAnchor.constraint(equalTo: topAnchor).isActive = true
            stackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
            stackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
            stackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        }
        
        func bind(to viewModel: AchievementRateCellViewModel) {
            
            viewModel.achievementRate
                .subscribe(onNext: { [weak self] rate in
                    let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
                    basicAnimation.toValue = rate
                    basicAnimation.duration = rate * (self?.animationSpeed ?? 1.5)
                    basicAnimation.fillMode = CAMediaTimingFillMode.forwards
                    basicAnimation.isRemovedOnCompletion = false
                    self?.achievementRateCircleView.foregroundLayer.add(basicAnimation, forKey: "urSoBasic")
                }).disposed(by: disposeBag)
            
    
    　　　　// ↓↓↓↓↓↓　この部分でtableView.rx.willDisplayCellイベントを受け取っている。
            viewModel.achievementRate
                .subscribe(onNext: {[weak self] rate in
                    self?.rate = rate
                    self?.countupOnLabel()
                })
                .disposed(by: disposeBag)
        }
        
        
        var animationStartDate: Date = Date()
        var rate: Double = 0
        let animationSpeed = 1.5
        
        private func countupOnLabel() {
            animationStartDate = Date()
    
    
    　　　　　　　　　　　　　　　//↓↓↓↓↓この部分のselfの影響で循環参照が起きている？
            let displayLink = CADisplayLink(target: self, selector: #selector(handleUpdate))
            displayLink.add(to: .current, forMode: .common)
        }
        
        @objc func handleUpdate() {
            let now = Date()
            let elapsedTime = now.timeIntervalSince(animationStartDate)
            let duration = rate * animationSpeed
    
            if elapsedTime > duration {
                let stringEndValue = String(format: "%.0f%", rate * 100) + "%"
                percentageNumberLabel.text = stringEndValue
                percentageNumberLabel.lastLetterToSmall(value: stringEndValue)
            } else {
                let persentage = elapsedTime / duration
                let value = persentage * rate
                let stringValue = String(format: "%.0f%", value * 100) + "%"
                percentageNumberLabel.text = stringValue
                percentageNumberLabel.lastLetterToSmall(value: stringValue)
            }
    
        }
        
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }
    
    
    
    
[/code]


CC BY-SA 4.0
2021-11-16T18:50:02.933
83663
2021-11-18T11:46:30.807
37960
question
swift uitableview rx-swift 循環参照
TableViewCellのレイアウトイベントをRxSwiftの”tableView.rx.willDisplayCell”で受け取り、subscribe内でCADisplayLinkを使いテキストをアニメーションしたい


下記の回答からヒントを得て、自己解決致しました。  
<https://stackoverflow.com/questions/47368609/definitively-do-you-have-to-
invalidate-a-cadisplaylink-when-the-controller-di>

"self"と"Rxのsubscribe"の扱いに原因があると思っていたのですが、単純にCADisplayLinkが正しく使えていませんでした。CADisplayLinkを使用する時は使用後にinvalidate()をしないとselfへの強参照が残ってしまうようで、そのためCellをdeinitできていませんでした。  
下記のように修正いたしました。

[code]

    var displayLink: CADisplayLink?
    
     @objc func handleUpdate() {
            let now = Date()
            let elapsedTime = now.timeIntervalSince(animationStartDate)
            let duration = rate * animationSpeed
    
            if elapsedTime > duration {
                let stringEndValue = String(format: "%.0f%", rate * 100) + "%"
                percentageNumberLabel.text = stringEndValue
                percentageNumberLabel.lastLetterToSmall(value: stringEndValue)
    
    　　　　　　　↓↓↓この部分の記述で解決
    　　　　　　　self.displayLink?.invalidate()
    
            } else {
                let persentage = elapsedTime / duration
                let value = persentage * rate
                let stringValue = String(format: "%.0f%", value * 100) + "%"
                percentageNumberLabel.text = stringValue
                percentageNumberLabel.lastLetterToSmall(value: stringValue)
            }
    
        }
    
[/code]


CC BY-SA 4.0
2021-11-18T11:46:30.807
83698
2021-11-18T11:46:30.807
37960
83663
answer