Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RxSwiftからCombineへの移行準備とSwiftUIへの道標

 RxSwiftからCombineへの移行準備とSwiftUIへの道標

0e6b2b5b4861d0e4b136527c0d7d428e?s=128

Yuichi Kobayashi

March 26, 2021
Tweet

Transcript

  1. 3Y4XJGU͔Β$PNCJOF ΁ͷҠߦ४උͱ 4XJGU6*΁ͷಓඪ QPUBUPUJQT

  2. ࣗݾ঺հ w ίόϠγϢ΢Πν w גࣜձࣾ.FEJQMBU -1'ࣄۀ෦։ൃ( ϝυϐΞάϧʔϓ !JNLP

  3. 3Y4XJGU ˣ $PNCJOF

  4. $PNCJOF΁Ͳ͏Ҡߦ͢Δʁ w Ұؾʹॻ͖׵͑Δͷ͸େมͦ͏ w ճؼςετ͢Δ༨༟͕ͳ͍

  5. ແཧͤͣ Ͱ͖Δͱ͜Ζ͔Β ΍͍ͬͯ͘💪

  6. $PNCJOF࣌୅ͷ7JFX.PEFM 👉7JFX.PEFMͰ͋Ε͹ը໘୯ҐͰҠߦͰ͖ͦ͏ʁ final class CombineProductsViewModel { // Output @Published private(set)

    var isLoading = false @Published private(set) var products: [Product] = [] // Input / Action func load() { // PassthroughSubject#sendΛτϦΨʔͨ͠Γɺ // ௚઀ϩδοΫΛ࣮ߦͯ͠OutputΛಘΔ } }
  7. 3Y4XJGUͰ΋ !1VCMJTIFE͍ͨ͠🔥

  8. !1VCMJTIFE͸ 1SPQFSUZ8SBQQFS @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0,

    *) @propertyWrapper public struct Published<Value> { /// Creates the published instance with an initial wrapped value. /// /// Don't use this initializer directly. Instead, create a property with the `@Published` attribute, as shown here: /// /// @Published var lastUpdated: Date = Date() /// /// - Parameter wrappedValue: The publisher's initial value. public init(wrappedValue: Value) … }
  9. !3Y1VCMJTIFEͭͬͯ͘Έͨ @propertyWrapper struct RxPublished<Value> { var projectedValue: BehaviorRelay<Value> { relay

    } var wrappedValue: Value { get { relay.value } set { relay.accept(newValue) } } private let relay: BehaviorRelay<Value> init(wrappedValue: Value) { self.relay = .init(value: wrappedValue) } }
  10. 6TBHF!3Y1VCMJTIFE final class RxProductsViewModel { private let useCase = ProductUseCase()

    private let bag = DisposeBag() @RxPublished private(set) var isLoading = false @RxPublished private(set) var products: [Product] = [] func load() { useCase .products() .do( onSuccess: { [unowned self] _ in self.isLoading = false }, onError: { [unowned self] error in self.isLoading = false // Handle error }, onSubscribe: { [unowned self] in self.isLoading = true } ) // subscribe .assign(to: \.products, on: self) .disposed(by: bag) } } extension PrimitiveSequence where Trait == SingleTrait { func assign<Root>(to keyPath: ReferenceWritableKeyPath<Root, Element>, on object: Root) -> Disposable { return subscribe(onSuccess: { object[keyPath: keyPath] = $0 }) } } ˞BTTJHO ͸FYUFOTJPO͍ͯ͠·͢
  11. CJOE ͯ͠ΈΔ

  12. !3Y1VCMJTIFE final class RxProductsViewController: UIViewController { private let viewModel =

    RxProductsViewModel() private let bag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() bind() viewModel.load() } private func bind() { viewModel.$isLoading .observeOn(MainScheduler.instance) .subscribe(onNext: { isLoading in // Toggle loading indicator }) .disposed(by: bag) viewModel.$products .observeOn(MainScheduler.instance) .subscribe(onNext: { products in // Update view }) .disposed(by: bag) } }
  13. !3Y1VCMJTIFEWT!1VCMJTIFE final class RxProductsViewController: UIViewController { private let viewModel =

    RxProductsViewModel() private let bag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() bind() viewModel.load() } private func bind() { viewModel.$isLoading .observeOn(MainScheduler.instance) .subscribe(onNext: { isLoading in // Toggle loading indicator }) .disposed(by: bag) viewModel.$products .observeOn(MainScheduler.instance) .subscribe(onNext: { products in // Update view }) .disposed(by: bag) } } final class CombineProductsViewController: UIViewController { private let viewModel = CombineProductsViewModel() private var cancellables = Set<AnyCancellable>() override func viewDidLoad() { super.viewDidLoad() bind() viewModel.load() } private func bind() { viewModel.$isLoading .receive(on: DispatchQueue.main) .sink(receiveValue: { isLoading in // Toggle loading indicator }) .store(in: &cancellables) viewModel.$products .receive(on: DispatchQueue.main) .sink(receiveValue: { products in // Update view }) .store(in: &cancellables) } } PCTFSWF0O ˠSFDFJWF TVCTDSJCF ˠTJOL EJTQPTFE ˠTUPSF
  14. ͓࡞๏͕ಉ͡😍

  15. !1VCMJTIFEͳ7JFX.PEFM ✅4XJGU6*΁Ҡߦ͠΍͍͢ ❌7JFXͷςετ͕ॻ͖ͮΒ͍ 7JFX.PEFMͷQSPUPDPM४ڌΛͲ͏͢Δ͔ʁ

  16. υϝΠϯϩδοΫ͸ Ͳ͏͠Α͏🤔

  17. ޙճ͠ʹͰ͖ͳ͘΋ͳ͍ w 7JFX.PEFMͰҰ୴TVCTDSJCFͪ͠Ό͏ w 3Y$PNCJOFͰ0CTFSWBCMF㲗1VCMJTIFS

  18. 3Y$PNCJOF final class CombineProductsViewModel { private let useCase = ProductUseCase()

    private var cancellables = Set<AnyCancellable>() @Published private(set) var isLoading = false @Published private(set) var products: [Product] = [] func load() { useCase .products() // Single -> AnyPublisher .asPublisher() // লུ: isLoading ͸ handleEvents() Ͱ੍ޚ .catch { error in // Handle error return Empty() } .assign(to: \.products, on: self) .store(in: &cancellables) } } final class ProductUseCase { func products() -> Single<[Product]> { … } } ˞6TF$BTF͸͜Μͳײ͡
  19. Ϗϡʔ΋͍͔ͭ 4XJGU6*Ͱ࡞Γ͍ͨ😎

  20. 4XJGU6*ରԠ7.ʹ͢Δ final class CombineProductsHostingController: UIHostingController<ProductsScene> { private let viewModel =

    CombineProductsViewModel() required init?(coder aDecoder: NSCoder) { super.init( coder: aDecoder, rootView: ProductsScene(viewModel: self.viewModel) ) } } struct ProductsScene: View { @ObservedObject private(set) var viewModel: CombineProductsViewModel var body: some View { List { ForEach(viewModel.products) { product in Text(product.name) } } .onAppear(perform: { viewModel.load() }) } } 4XJGU6*ͰϏϡʔΛ࡞੒ final class CombineProductsViewModel: ObservableObject { … } 7JFX.PEFMΛमਖ਼
  21. %POF🙌

  22. ྑ͍ͱ͜Ζ w 6*,JU࣮૷ͷը໘Λ4XJGU6*ʹ͢Δ w 4XJGU6*Ͱ࡞Εͳ͍มߋ͕ੜ͡ɺ΍Ήͳ͘6*,JUʹม͑Δ 👉ͲͪΒͷ৔߹Ͱ͋ͬͯ΋ɺ7.ͷେ෯ͳઃܭมߋ͕ੜ͡ͳ͍

  23. ·ͱΊ w !3Y1VCMJTIFEͰ$PNCJOFҠߦ΁උ͑Δ w 3Y4XJGUͱͷ૬ޓ৐׵ͰɺঃʑʹҠߦ΋Մೳ w !1VCMJTIFEͰઃܭ͞Εͨ7.͸6*,JU4XJGU6*͍ͣΕʹ΋ ༥௨͕͖͘

  24. ͝ਗ਼ௌ ͋Γ͕ͱ͏͍͟͝·ͨ͠🙇