iOS Swift Combine: отмените установку ‹AnyCancellable›

Если я сохранил отменяемый набор в ViewController:

private var bag = Set<AnyCancellable>()

Которая содержит множественную подписку.

1 - Стоит ли отменять подписку в деините? или он делает работу автоматически?

2 - Если да, как я могу отменить все сохраненные подписки?

bag.removeAll() is enough?

или я должен перебирать набор и отменять все подписки одну за другой?

for sub in bag {
   sub.cancel()
}

Apple сообщает, что подписка действительна до тех пор, пока сохраненный AnyCancellable не окажется в памяти. Итак, я полагаю, что освобождения отменяемых с помощью bag.removeAll() должно быть достаточно, не так ли?


person Andrea Miotto    schedule 22.11.2019    source источник


Ответы (5)


deinit ваш ViewController будет удален из памяти. Все его переменные экземпляра будут освобождены.

Документы для Combine > Publisher > assign(to:on:) говорят:

Экземпляр AnyCancellable. Вызовите cancel () в этом экземпляре, если вы больше не хотите, чтобы издатель автоматически назначал свойство. Деинициализация этого экземпляра также отменит автоматическое назначение.

1 - Следует ли мне отменить подписку в деините? или он делает работу автоматически?

В этом нет необходимости, он выполняет свою работу автоматически. Когда ваш ViewController будет освобожден, переменная экземпляра bag также будет освобождена. Поскольку ссылки на ваши AnyCancellable больше не будут, назначение закончится.

2 - Если да, как я могу отменить все сохраненные подписки?

Не так. Но часто у вас могут быть какие-то подписки, которые вы хотите запускать и останавливать, например, _5 _ / _ 6_. В этом случае ваш ViewController все еще находится в памяти.

Итак, в viewDidDissappear вы можете делать bag.removeAll(), как вы и подозревали. Это удалит ссылки и остановит назначение.

Вот код, который вы можете запустить, чтобы увидеть .removeAll() в действии:

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)
}

Первый таймер срабатывает каждую секунду и распечатывает строку. Второй таймер сработает через 10 секунд, а затем вызовет bag.removeAll(). Тогда оба издателя таймера будут остановлены.

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

person Andy    schedule 08.06.2020

если вы подписались на издателя с вашего контроллера представления, вероятно, вы захватите self в sink, который сделает ссылку на него, и не позволит ARC удалить ваш контроллер представления позже, если подписчик не завершил работу, так что это , рекомендуется слабо захватить себя


так что вместо:

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

.store(in: &cancellable)

вы должны использовать [weak self]:

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

.store(in: &cancellable)

таким образом, когда контроллер View удален, у вас не будет цикла хранения или утечек памяти.

person Mostfa Essam    schedule 18.07.2020
comment
Но как это отвечает на его вопрос? - person giorashc; 22.02.2021
comment
Пользователь @giorashc спросил, следует ли ему отменять подписку вручную или он выполняет эту работу автоматически, мой ответ показывает, как можно не думать о подписках, если нет никаких сильных ссылок на себя. при использовании этого способа подписки будут автоматически удалены. - person Mostfa Essam; 23.02.2021

Попробуйте создать конвейер и не сохранять отменяемое значение в какой-либо переменной состояния. Вы обнаружите, что конвейер останавливается, как только он встречает асинхронную операцию. Это связано с тем, что «Отменяемое» было удалено ARC, и поэтому оно было автоматически отменено. Таким образом, вам не нужно вызывать отмену в конвейере, если вы освобождаете все ссылки на него.

Из документации:

Экземпляр AnyCancellable автоматически вызывает cancel () при деинициализации.

person Gil Birman    schedule 24.11.2019
comment
похоже, что это не работает, как говорится в документации. Я протестировал syncRequest () .ink (). Store (in: & одноразовые), определил его в viewmodel и добавил deinit {} для просмотра модели. deinit печатает каждый раз при переключении экранов, но подписка receiveCancel не является вызовом, а receiveValue вызывается несколько раз - person Michał Ziobro; 19.05.2020
comment
@ MichałZiobro звучит как хороший вопрос для stackoverflow: D - person Gil Birman; 19.05.2020
comment
subscriptions.removeAll() отлично работает в Swift 5.4 - person Mark; 01.06.2021

Я тестирую этот код

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()

поэтому я использую это расширение.

import Combine

typealias CancelBag = Set<AnyCancellable>

extension CancelBag {
  mutating func cancelAll() {
    forEach { $0.cancel() }
    removeAll()
  }
}
person Changhoon    schedule 29.05.2020
comment
Я думаю, вы имеете в виду просто удалить из набора vs array - person Paul D.; 19.06.2020

Создайте Cancellable + Extensions.swift

import Combine

typealias DisposeBag = Set<AnyCancellable>

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

В вашем классе реализации в моем случае CurrentWeatherViewModel.swift просто добавьте disposables.dispose(), чтобы удалить Set из AnyCancellable

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)
    }
}
person Nischal Hada    schedule 14.11.2020
comment
Почему вы явно вызываете cancel для каждого AnyCancelable в наборе? Достаточно просто позвонить removeAll(), чтобы обнулить их и отменить текущие задачи подписчика. - person Parth Tamane; 21.05.2021