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

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

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

Yuichi Kobayashi

March 26, 2021
Tweet

More Decks by Yuichi Kobayashi

Other Decks in Programming

Transcript

  1. $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ΛಘΔ } }
  2. !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) … }
  3. !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) } }
  4. 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͍ͯ͠·͢
  5. !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) } }
  6. !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
  7. 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͸͜Μͳײ͡
  8. 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Λमਖ਼