Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Combine・sink(RxSwift・subscribe)では Task を作らないよう...

Combine・sink(RxSwift・subscribe)では Task を作らないようにして async なメソッドを呼ぶ / Do not create a Task inside a Combine/sink (RxSwift/subscribe) closure when calling an async method there

Combine・sink(RxSwift・subscribe)では Task を作らないようにして async なメソッドを呼ぶ / Do not create a Task inside a Combine/sink (RxSwift/subscribe) closure when calling an async method there

公開 SwiftWednesday【iOSDC Japan 2023 直前】
2023/08/24 19:30〜
https://dena.connpass.com/event/291447/

登壇ノートはこちら: 準備中

treastrain / Tanaka Ryoga

August 24, 2023
Tweet

More Decks by treastrain / Tanaka Ryoga

Other Decks in Technology

Transcript

  1. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. USFBTUSBJO5BOBLB3ZPHB%F/"$P

    -UE $PNCJOFɾsinkʢ3Y4XJGUɾsubscribeʣͰ͸ 
 TaskΛ࡞Βͳ͍Α͏ʹͯ͠ 
 asyncͳϝιουΛݺͿ 4XJGU8FEOFTEBZ 1 ެ։4XJGU8FEOFTEBZʲJ04%$+BQBO௚લʳ"VHVTU 
  2. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. ඇಉظॲཧͷҠߦ

    $PNCJOFɾ3Y4XJGUˠ4XJGU$PODVSSFODZ w ͭͷΠϕϯτ w $PNCJOFFutureʢDeferred Futureʣ w 3Y4XJGUSingleɾMaybeɾCompletable w ෳ਺ͷΠϕϯτ w $PNCJOFPublisher w 3Y4XJGURxSwift.Observable 2
  3. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. ݺΜͰ͍ͨϝιου͕asyncʹͳͬͨ

    4XJGU$PODVSSFODZ΁ͷҠߦ 3 import Combine import RxSwift import UIKit protocol PresenterProtocol { func handleEventA() func handleEventB() } final class ViewController: UIViewController { let presenter: some PresenterProtocol = ... // Combine let publisher: some Publisher<Output, Never> = ... var cancellables: [AnyCancellable] = [] // RxSwift let observable: RxSwift.Observable<Output> = ... let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() connect() } } extension ViewController { func connect() { publisher .sink( receiveValue: { [weak self] output in self?.presenter.handleEventA() } ) .store(in: &cancellables) observable .subscribe( onNext: { [weak self] output in self?.presenter.handleEventB() } ) .disposed(by: disposeBag) } } async async
  4. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. ݺΜͰ͍ͨϝιου͕asyncʹͳͬͨ

    4XJGU$PODVSSFODZ΁ͷҠߦ 4 import Combine import RxSwift import UIKit protocol PresenterProtocol { func handleEventA() async func handleEventB() async } final class ViewController: UIViewController { let presenter: some PresenterProtocol = ... // Combine let publisher: some Publisher<Output, Never> = ... var cancellables: [AnyCancellable] = [] // RxSwift let observable: RxSwift.Observable<Output> = ... let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() connect() } } extension ViewController { func connect() { publisher .sink( receiveValue: { [weak self] output in self?.presenter.handleEventA() } ) .store(in: &cancellables) observable .subscribe( onNext: { [weak self] output in self?.presenter.handleEventB() } ) .disposed(by: disposeBag) } } 'async' call in a function that does not support concurrency
  5. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. ҟͳΔΞΫλʔͷϝιουΛݺͿ͜ͱʹͳͬͨ

    4XJGU$PODVSSFODZ΁ͷҠߦ 5 import Combine import RxSwift import UIKit protocol ViewProtocol: AnyObject { func handleEventA() func handleEventB() } final class ViewController: UIViewController { @ViewLoading var object: ViewObject<ViewController> override func viewDidLoad() { super.viewDidLoad() object = .init(view: self) } } extension ViewController: ViewProtocol { ... } final class ViewObject<View: ViewProtocol> { unowned let view: View ... func connect() { publisher .sink( receiveValue: { [weak self] output in self?.view.handleEventA() } ) .store(in: &cancellables) observable .subscribe( onNext: { [weak self] output in self?.view.handleEventB() } ) .disposed(by: disposeBag) } } @MainActor
  6. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. ҟͳΔΞΫλʔͷϝιουΛݺͿ͜ͱʹͳͬͨ

    4XJGU$PODVSSFODZ΁ͷҠߦ 6 import Combine import RxSwift import UIKit @MainActor protocol ViewProtocol: AnyObject { func handleEventA() func handleEventB() } final class ViewController: UIViewController { @ViewLoading var object: ViewObject<ViewController> override func viewDidLoad() { super.viewDidLoad() object = .init(view: self) } } extension ViewController: ViewProtocol { ... } final class ViewObject<View: ViewProtocol> { unowned let view: View ... func connect() { publisher .sink( receiveValue: { [weak self] output in self?.view.handleEventA() } ) .store(in: &cancellables) observable .subscribe( onNext: { [weak self] output in self?.view.handleEventB() } ) .disposed(by: disposeBag) } } Call to main actor-isolated instance method * in a synchronous nonisolated context
  7. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. 4XJGU$PODVSSFODZ΁ͷҠߦ

    ίϯύΠϧ࣌ͷΤϥʔΛղফ͢Δʹ͸ w Πϕϯτड৴࣌ʹasyncͳ 
 ϝιουΛ࣮ߦͨ͘͠ͳͬͨ 㾎TaskΛ࡞ͬͯݺͿ w Πϕϯτड৴࣌ʹ 
 ҟͳΔΞΫλʔʹ෼཭͞Ε͍ͯΔ 
 ϝιουΛ࣮ߦͨ͘͠ͳͬͨ 㾎TaskΛ࡞ͬͯݺͿ 㾎ಉ͡ΞΫλʔͰ࣮ߦ͢ΔΑ͏ʹ ݺͿ 7
  8. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. Πϕϯτड৴৔ॴͰTaskΛ࡞ͬͯawait

    asyncͳϝιουΛݺͿͱ͖ 8 extension ViewController { func connect() { publisher .sink( receiveValue: { [weak self] output in self?.presenter.handleEventA() } ) .store(in: &cancellables) observable .subscribe( onNext: { [weak self] output in self?.presenter.handleEventB() } ) .disposed(by: disposeBag) } } extension ViewController { func connect() { publisher .sink( receiveValue: { [weak self] output in Task { await self?.presenter.handleEventA() } } ) .store(in: &cancellables) observable .subscribe( onNext: { [weak self] output in Task { await self?.presenter.handleEventB() } } ) .disposed(by: disposeBag) } }
  9. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. Πϕϯτड৴৔ॴͰTaskΛ࡞ͬͯawait

    asyncͳϝιουΛݺͿͱ͖ w self͸͜ͷ··Ͱ͍͍ͷ͔ʜʜʁ w ࣮ߦ͞ΕͨTaskͷΩϟϯηϧΛ Ͳ͏͠Α͏ʜʜ w $PNCJOFɾ3Y4XJGUͷ 
 4DIFEVMFSͷࢦఆ͸ʜʜʁ w ωετ͕ਂ͍ʜʜ 9 extension ViewController { func connect() { publisher .sink( receiveValue: { [weak self] output in Task { await self?.presenter.handleEventA() } } ) .store(in: &cancellables) observable .subscribe( onNext: { [weak self] output in Task { await self?.presenter.handleEventB() } } ) .disposed(by: disposeBag) } }
  10. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. AsyncSequence

    ͓͞Β͍ w Sequenceͷඇಉظ൛ w for-inϧʔϓΛॻ͚Δ 11 let numbers: some Sequence = [0, 1, 2, 3] for number in numbers { print(number, terminator: " ") } // "0 1 2 3 " var iterator = [0, 1, 2, 3].makeIterator() let counter: some AsyncSequence = AsyncStream { try? await Task.sleep(for: .seconds(1)) return iterator.next() } for try await count in counter { print(count, terminator: " ") } // "0 1 2 3 "
  11. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. AsyncSequence

    ͓͞Β͍ w Sequenceͷඇಉظ൛ w for-inϧʔϓΛॻ͚Δ w for-await-inϧʔϓΛॻ͚Δ w ࣦഊ͢ΔՄೳੑ͕͋ΔͳΒ 
 for-try-await-inϧʔϓ 12 let numbers: some Sequence = [0, 1, 2, 3] for number in numbers { print(number, terminator: " ") } // "0 1 2 3 " var iterator = [0, 1, 2, 3].makeIterator() let counter: some AsyncSequence = AsyncStream { try? await Task.sleep(for: .seconds(1)) return iterator.next() } for try await count in counter { print(count, terminator: " ") } // "0 1 2 3 "
  12. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. ༻ҙ͞Ε͍ͯΔΠϯελϯεϓϩύςΟ

    ͍ͣΕ΋AsyncSequenceʹద߹͍ͯ͠Δ w $PNCJOF w 3Y4XJGUʢ3Y4XJGU ʣ 13 extension Publisher where Self.Failure == Never { @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) public var values: AsyncPublisher<Self> { get } } extension Publisher { @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) public var values: AsyncThrowingPublisher<Self> { get } } #if swift(>=5.5.2) && canImport(_Concurrency) import Foundation @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public extension ObservableConvertibleType { var values: AsyncThrowingStream<Element, Error> { get } } #endif #if swift(>=5.5.2) && canImport(_Concurrency) && !os(Linux) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public extension InfallibleType { var values: AsyncStream<Element> { get } } #endif
  13. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. valuesΛ࢖ͬͯfor-await-inϧʔϓͰड৴

    await͕࢖͑ͯίʔυ͕୹͘ͳΓωετ΋ઙ͘ͳͬͨ 14 extension ViewController { func connect() { publisher .sink( receiveValue: { [weak self] output in Task { await self?.presenter.handleEventA() } } ) .store(in: &cancellables) observable .subscribe( onNext: { [weak self] output in Task { await self?.presenter.handleEventB() } } ) .disposed(by: disposeBag) } } extension ViewController { func connect() { Task { for await output in publisher.values { await presenter.handleEventA() } } Task { for try await output in observable.values { await presenter.handleEventB() } } } }
  14. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. valuesΛ࢖ͬͯfor-await-inϧʔϓͰड৴

    await͕࢖͑ͯίʔυ͕୹͘ͳΓωετ΋ઙ͘ͳͬͨ 15 extension ViewController { func connect() { publisher .sink( receiveValue: { [weak self] output in Task { await self?.presenter.handleEventA() } } ) .store(in: &cancellables) observable .subscribe( onNext: { [weak self] output in Task { await self?.presenter.handleEventB() } } ) .disposed(by: disposeBag) } } extension ViewController { func connect() { Task { for await output in publisher.values { await presenter.handleEventA() } } Task { for try await output in observable.values { await presenter.handleEventB() } } } } 🎉
  15. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. ࣮ࡍʹಈ͔ͯ͠ΈΔ

    for-await-inϧʔϓͰड৴ 16 import Combine import UIKit final class ViewController: UIViewController { deinit { print(self, "is deinited 🎉") } override func viewDidLoad() { // ... connect() } let publisher: some Publisher<Output, Never> = ... func connect() { Task { for await output in publisher.values { // ... } } } } EFJOJU͕ݺ͹Εͳ͍ʜʜ GPSBXBJUJOϧʔϓΛ 
 ऴྃͤ͞Δज़͕ͳ͍
  16. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. handleEventsɾreceiveCancel͕ݺ͹Εͳ͍

    3Y4XJGUͳΒdoɾonDispose 17 import Combine import UIKit final class ViewController: UIViewController { // ... func connect() { Task { let values = publisher .handleEvents( receiveCancel: { print("receiveCancel") } ) .values for await output in values { // ... } } } // ... } ݺ͹Εͳ͍ʜʜ Ωϟϯηϧ͞Εͣʹ଴͍ͬͯΔ
  17. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. handleEventsɾreceiveCancel͕ݺ͹Εͳ͍

    3Y4XJGUͳΒdoɾonDispose 18 import Combine import UIKit final class ViewController: UIViewController { // ... func connect() { Task { let values = publisher .handleEvents( receiveCancel: { print("receiveCancel") } ) .values for await output in values { // ... } } } // ... } import Combine import UIKit final class ViewController: UIViewController { // ... private var cancellables: [AnyCancellable] = [] func connect() { publisher .handleEvents( receiveCancel: { print("receiveCancel") } ) .sink( receiveValue: { [weak self] output in // ... } ) .store(in: &cancellables) } // ... } ݺ͹Εͳ͍ʜʜ Ωϟϯηϧ͞Εͣʹ଴͍ͬͯΔ ݺ͹ΕΔʂ
  18. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. for-await-inͷ͋ΔTaskʹΩϟϯηϧΛ఻͑Δ

    ͞·͟·ͳํ๏͕͋Δ w TaskΛ$PNCJOFͷ Cancellableʹద߹ͤ͞ɺ $PNCJOFͷPublisherͱ 
 ಉ͡Α͏ʹ 
 store(in: &cancellables) w ΦϦδφϧͰCancelBagΛ࡞Δ 
 ʢDisposeBagͷΑ͏ͳʣ w TaskΛอ͓͖࣋ͯ͠ɺ 
 deinitͳͲͷλΠϛϯάͰ Task.cancel()ΛࣗΒݺͿ w ෳ਺͋Δ৔߹͸TaskGroupͰ ·ͱΊͯ΋Α͍ 
 
 
 ɹɹɹɹɹɹɹɹɹFUD 20
  19. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. TaskΛ$PNCJOFͷCancellableʹ͢Δ

    ݁ہ$PNCJOF͕ཁΔ͕͓खܰ 21 extension ViewController { func connect() { Task { for await output in publisher.values { await presenter.handleEventA() } } Task { for try await output in observable.values { await presenter.handleEventB() } } } } import Combine import Foundation extension Task: Cancellable {} import Combine import UIKit final class ViewController: UIViewController { private let publisher: some Publisher<Output, Never> = ... private var cancellables: [AnyCancellable] = [] // ... func connect() { Task { [publisher] in for await output in publisher.values { // ... } } .store(in: &cancellables) } // … }
  20. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. deinitͳͲͷλΠϛϯάͰTask.cancel()ΛݺͿ

    deinit͕ݺ͹ΕΔΑ͏ʹ஫ҙ͢ΔʢselfΛΩϟϓνϟ͠ͳ͍ʣ 22 extension ViewController { func connect() { Task { for await output in publisher.values { await presenter.handleEventA() } } Task { for try await output in observable.values { await presenter.handleEventB() } } } } final class ViewController: UIViewController { var task1: Task<(), Never>? var task2: Task<(), any Error>? deinit { task1?.cancel() task2?.cancel() } ... } extension ViewController { func connect() { task1 = Task { [publisher, presenter] in for await output in publisher.values { await presenter.handleEventA() } } task2 = Task { [publisher, presenter] in ... } } }
  21. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. ·ͱΊ

    $PNCJOFɾ3Y4XJGUˠ4XJGU$PODVSSFODZ w $PNCJOFɾ3Y4XJGUͷΠϕϯτΛ 
 4XJGU$PODVSSFODZʢAsyncSequenceʣͰ 
 ड͚औΕΔ࢓૊Έ͕༻ҙ͞Ε͍ͯΔ w Ҡߦظʹศརʹ࢖͑ͦ͏ w ΩϟϯηϧΛࣗ෼ͰߦΘͳ͍ͱղ์͞Εͣʹ࢒ͬͯ͠·͏ͷͰ஫ҙ͢Δ 23
  22. Copyright © 2023 treastrain / Tanaka RyogaɹAll rights reserved. ͋Γ͕ͱ͏͍͟͝·ͨ͠ʂ

    $PNCJOFɾsinkʢ3Y4XJGUɾsubscribeʣͰ͸ 
 TaskΛ࡞Βͳ͍Α͏ʹͯ͠ 
 asyncͳϝιουΛݺͿ 4XJGU8FEOFTEBZ 24 ެ։4XJGU8FEOFTEBZʲJ04%$+BQBO௚લʳ"VHVTU