Vejo o corpo do SwiftUI sendo repetidamente chamado em um loop infinito na presença de variáveis de ambiente como horizontalSizeClass
ou verticalSizeClass
. Isso acontece depois que o dispositivo é girado do modo retrato para paisagem e depois de volta para o modo retrato. O deinit
método de TestPlayerVM
é repetidamente chamado. O código de exemplo minimamente reproduzível é colado abaixo.
O loop infinito não é visto se eu remover referências de ambiente de classe de tamanho OU se eu pular addPlayerObservers
a chamada no TestPlayerVM
inicializador.
import AVKit
import Combine
struct InfiniteLoopView: View {
@Environment(\.verticalSizeClass) var verticalSizeClass
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@State private var openPlayer = false
@State var playerURL: URL = URL(fileURLWithPath: Bundle.main.path(forResource: "Test_Video", ofType: ".mov")!)
var body: some View {
PlayerView(playerURL: playerURL)
.ignoresSafeArea()
}
}
struct PlayerView: View {
@Environment(\.dismiss) var dismiss
var playerURL:URL
@State var playerVM = TestPlayerVM()
var body: some View {
VideoPlayer(player: playerVM.player)
.ignoresSafeArea()
.background {
Color.black
}
.task {
let playerItem = AVPlayerItem(url: playerURL)
playerVM.playerItem = playerItem
}
}
}
@Observable
class TestPlayerVM {
private(set) public var player: AVPlayer = AVPlayer()
var playerItem:AVPlayerItem? {
didSet {
player.replaceCurrentItem(with: playerItem)
}
}
private var cancellable = Set<AnyCancellable>()
init() {
addPlayerObservers()
}
deinit {
print("Deinit Video player manager")
removeAllObservers()
}
private func removeAllObservers() {
cancellable.removeAll()
}
private func addPlayerObservers() {
player.publisher(for: \.timeControlStatus, options: [.initial, .new])
.receive(on: DispatchQueue.main)
.sink { timeControlStatus in
print("Player time control status \(timeControlStatus)")
}
.store(in: &cancellable)
}
}
Isso é causado por uma combinação de várias coisas no seu código:
@State
with@Observable
chamaráTestPlayerVM.init
toda vezPlayerView.init
que for chamado. Normalmente, essa nova instância será desinicializada imediatamente, pois a visualização precisa apenas de uma instância, mas o SwiftUI vaza uma instância às vezes. Esse é um bug bem conhecido.cancellable
é rastreado por@Observable
cancellable
eminit
cancellable
emdeinit
Se nenhuma dessas quatro coisas acontecesse, você não entraria em um loop infinito.
No início,
PlayerView.init
é chamado e isso cria a primeira instância deTestPlayerVM
.Girar a tela faz com que o
@Environment
s mude, então o SwiftUI executa uma atualização de visualização, chamandoInfiniteLoopView.init
, onde você chamaPlayerView.init
. Isso cria uma segunda instância deTestPlayerVM
. Essa segunda instância é vazada, ou seja, retida erroneamente na memória pelo SwiftUI e não é desinicializada.É por isso que o loop infinito não começa até a segunda rotação da tela. Se o SwiftUI não vazar a segunda instância, ele
deinit
teria sido chamado após a primeira rotação, dando início ao loop infinito.Quando você gira a tela novamente,
InfiniteLoopView.body
é chamado novamente, entãoPlayerView.init
também é chamado novamente, criando a terceira instância deTestPlayerVM
. É aqui que a reação em cadeia começa.Em
TestPlayerVM.init
,cancellable
é modificado conforme você insere um novo elemento nele. Comocancellable
é rastreado por@Observable
, o SwiftUI agora pensa quecancellable
é uma dependência deInfiniteLoopView
.InfiniteLoopView
será atualizado sempre quecancellable
houver alterações. Devo enfatizar que este éInfiniteLoopView
, não . Afinal,PlayerView
ainda estamos no meio da avaliação , então o SwiftUI está encontrando as dependências para .InfiniteLoopView.body
InfiniteLoopView
Diferentemente da segunda instância, esta terceira instância é (corretamente) desinicializada imediatamente. Mas o que acontece em
deinit
? Você modificacancellable
removendo todos os seus elementos. O SwiftUI observa esta mudança e, como determinou quecancellable
é uma dependência deInfiniteLoopView
anteriormente, ele atualiza a visualização .InfiniteLoopView.body
é chamado, entãoPlayerView.init
é chamado e, portanto, outra instância deTestPlayerVM
é criada e o loop continua.Você pode fazer qualquer uma destas opções (ou todas elas) para quebrar o ciclo:
@State
opcional e inicialize-o em.task
.@ObservationIgnored
cancellable
addPlayerObservers
para fora deinit
- para dentro,onAppear
por exemplo.cancellable.removeAll()
. Deixe o ARC fazer seu trabalho. OAnyCancellable
será automaticamente desinicializado logo depoisTestPlayerVM.deinit
, de qualquer forma, assumindo que nada mais esteja mantendo uma referência a eles.