13
votes

iOS Swift Combine: annuler un ensemble <AnyCancellable>

Si j'ai stocké un ensemble annulable dans un ViewController:

for sub in bag {
   sub.cancel()
}

Qui contient plusieurs abonnements.

1 - Dois-je annuler l'abonnement en deinit? ou il fait le travail automatiquement?

2 - Si oui, comment puis-je annuler tous les abonnements enregistrés?

bag.removeAll() is enough?

ou devrais-je parcourir l'ensemble et annuler tous les abonnements un par un?

private var bag = Set<AnyCancellable>()

Apple dit que l'abonnement est actif jusqu'à ce que AnyCancellable stocké soit en mémoire. Donc je suppose que désallouer les bag.removeAll() avec bag.removeAll() devrait suffire, n'est-ce pas?


0 commentaires

5 Réponses :


6
votes

Essayez de créer un pipeline et de ne pas stocker l'annulable dans une variable d'état. Vous constaterez que le pipeline s'arrête dès qu'il rencontre une opération asynchrone. C'est parce que l'annulable a été nettoyé par ARC et il a donc été automatiquement annulé. Vous n'avez donc pas besoin d'appeler cancel sur un pipeline si vous libérez toutes les références à celui-ci.

De la documentation :

Une instance AnyCancellable appelle automatiquement cancel () lorsqu'elle est désinitialisée.


2 commentaires

cela ne semble pas fonctionner de cette façon comme le dit la documentation. J'ai testé syncRequest (). Sink (). Store (in: & jetables) et l'ai défini sur viewmodel et j'ai ajouté deinit {} pour afficher le modèle. deinit imprime à chaque fois que vous changez d'écran mais que l'abonnement receiveCancel n'est pas un appel et receiveValue est appelé plusieurs fois


@ MichałZiobro sonne comme une bonne question pour stackoverflow: D



2
votes

Je teste ce code

import Combine

typealias CancelBag = Set<AnyCancellable>

extension CancelBag {
  mutating func cancelAll() {
    forEach { $0.cancel() }
    removeAll()
  }
}

donc j'utilise cette extension.

let cancellable = Set<AnyCancellable>()

Timer.publish(every: 1, on: .main, in: .common).autoconnect()
  .sink { print("===== timer: \($0)") }
  .store(in: &cancellable)
  
cancellable.removeAll() // just remove from Set. not cancellable.cancel()


2 commentaires

Je pense que vous voulez dire "supprimer simplement de l'ensemble" vs "tableau"


Il n'est pas nécessaire d'utiliser un forEach . C'est incroyablement moche. Utilisez simplement une boucle for.



2
votes

À la deinit votre ViewController sera supprimé de la mémoire. Toutes ses variables d'instance seront désallouées.

Les documents pour Combine > Publisher > assign(to:on:) disent:

Une instance AnyCancellable. Appelez cancel () sur cette instance lorsque vous ne souhaitez plus que l'éditeur attribue automatiquement la propriété. La désinitialisation de cette instance annulera également l'attribution automatique.

1 - Dois-je annuler l'abonnement en deinit? ou il fait le travail automatiquement?

Vous n'avez pas besoin de le faire, il fait le travail automatiquement. Lorsque votre ViewController est désalloué, le bag variables d'instance sera également désalloué. Comme il n'y a plus de référence à vos AnyCancellable , la mission prendra fin.

2 - Si oui, comment puis-je annuler tous les abonnements enregistrés?

Non. Mais souvent, vous pouvez avoir des abonnements que vous souhaitez démarrer et arrêter sur, par exemple, viewWillAppear / viewDidDissapear . Dans ce cas, votre ViewController est toujours en mémoire.

Ainsi, dans viewDidDissappear , vous pouvez faire bag.removeAll() comme vous le soupçonniez. Cela supprimera les références et arrêtera l'attribution.

Voici un code que vous pouvez exécuter pour voir .removeAll() en action:

var bag = Set<AnyCancellable>()

func testRemoveAll() {
  Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    .sink { print("===== timer: \($0)") }
    .store(in: &bag)

  Timer.publish(every: 10, on: .main, in: .common).autoconnect()
    .sink { _ in self.bag.removeAll() }
    .store(in: &bag)
}

La première minuterie se déclenchera toutes les secondes et imprimera une ligne. La deuxième minuterie se déclenchera après 10 secondes, puis bag.removeAll() . Ensuite, les deux éditeurs de minuterie seront arrêtés.

https://developer.apple.com/documentation/combine/publisher/3235801-assign


0 commentaires

4
votes

si vous est arrivé de souscrire à un éditeur de votre View Controller, probablement , vous allez capturer l' self dans l' sink , qui fera une référence à, et ne laissez ARC supprimer votre contrôleur de vue plus tard , si l'abonné n'a pas fini, il est donc , conseillé de se capturer chaque semaine


donc au lieu de:

   ["title"]
      .publisher
      .sink { [weak self] (publishedValue) in
        self.title.text = publishedValue
    }

.store(in: &cancellable)

tu devrais faire:

   ["title"]
      .publisher
      .sink { (publishedValue) in
        self.title.text = publishedValue
    }

.store(in: &cancellable)

ainsi, lorsque le contrôleur View est supprimé, vous n'aurez aucun cycle de rétention ni aucune fuite de mémoire.


0 commentaires

1
votes

Créer un fichier Cancellable + Extensions.swift

import Combine
import Foundation

final class CurrentWeatherViewModel: ObservableObject {
    @Published private(set) var dataSource: CurrentWeatherDTO?

    let city: String
    private let weatherFetcher: WeatherFetchable
    private var disposables = Set<AnyCancellable>()

    init(city: String, weatherFetcher: WeatherFetchable = WeatherNetworking()) {
        self.weatherFetcher = weatherFetcher
        self.city = city
    }

    func refresh() {
        disposables.dispose()

        weatherFetcher
            .currentWeatherForecast(forCity: city)
            .map(CurrentWeatherDTO.init)
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { [weak self] value in
                guard let self = self else { return }
                switch value {
                case .failure:
                    self.dataSource = nil
                case .finished:
                    break
                }
                }, receiveValue: { [weak self] weather in
                    guard let self = self else { return }
                    self.dataSource = weather
            })
            .store(in: &disposables)
    }
}

Dans votre classe d'implémentation, dans mon cas CurrentWeatherViewModel.swift ajoutez simplement disposables.dispose() pour supprimer Set of AnyCancellable

import Combine

typealias DisposeBag = Set<AnyCancellable>

extension DisposeBag {
    mutating func dispose() {
        forEach { $0.cancel() }
        removeAll()
    }
}


0 commentaires