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

Combineを利用したSwiftUI・UIKitのどちらにも対応するUnidirectionalな設計を実現するには

Ae276805027a01983503c3edafbdb6b2?s=47 Taiki Suzuki
September 26, 2019

 Combineを利用したSwiftUI・UIKitのどちらにも対応するUnidirectionalな設計を実現するには

Ae276805027a01983503c3edafbdb6b2?s=128

Taiki Suzuki

September 26, 2019
Tweet

More Decks by Taiki Suzuki

Other Decks in Programming

Transcript

  1. // // CA.swift #10 // // Talked by @marty_suzuki on

    2019/09/26 // CombineΛར༻ͨ͠ SwiftUIɾUIKitͷͲͪΒʹ΋ରԠ͢Δ UnidirectionalͳઃܭΛ࣮ݱ͢Δʹ͸
  2. marty_suzuki marty-suzuki Taiki Suzuki ΠϯλʔωοτςϨϏہ ʮAbemaTVʯΛ୲౰͢ΔiOSΤϯδ χΞɻ2014೥αΠόʔΤʔδΣϯτ ৽ଔೖࣾɻ ίϛϡχςΟαʔϏεͰαʔόʔα ΠυΛ୲౰ͨ͠ޙɺiOSΤϯδχΞʹ

    స޲͠ϑΝογϣϯ௨ൢαΠτ ʮVILECTʯͷ্ཱͪ͛ɾӡӦʹै ࣄɻͦͷޙ৽ײ֮SNSʮ755ʯͰͷ։ ൃΛܦͯɺ2017೥3݄ΑΓݱ৬ɻ Popular repositories Overview SAHistoryNavigationViewController Swift 1,559 SAHistoryNavigationViewController realizes iOS task manager like UI in UINavigationController. SABlurImageView Swift 523 You can use blur effect and it's animation easily to call only two methods. URLEmbeddedView Swift 549 URLEmbeddedView automatically caches the object that is confirmed the Open Graph Protocol. ReverseExtension Swift 1,446 A UITableView extension that enables cell insertion from the bottom of a table view.
  3. 1. ·ͣ͸͡Ίʹ 2. import UIKit 3. import UIKit + Ricemill

    4. import SwiftUI 5. import SwiftUI + Ricemill ΞδΣϯμ
  4. ·ͣ͸͡Ίʹ

  5. IUUQTCJUMZNRL4 ʮ.77.ͷ࣮૷ΛറΔ'SBNFXPSLΛ։ൃɾಋೖ͠ νʔϜͰ͹Β͖͕࣮ͭ͋ͬͨ૷Λ౷Ұ͢Δʯ ϖʔδ

  6. RxSwiftͰUnidirectionalͳઃܭΛ࣮ݱ͢Δ IUUQTHJUIVCDPNDBUTPTT6OJP

  7. IUUQTHJUIVCDPNNBSUZTV[VLJ3JDFNJMM import Ricemill CombineΛར༻ͨ͠SwiftUIɾUIKitͷͲͪΒʹ΋ରԠ͢Δ UnidirectionalͳઃܭΛ࣮ݱ͢Δ

  8. import UIKit

  9. final class CounterViewController: UIViewController { let counterToggle: UISwitch let incrementButton:

    UIButton let decrementButton: UIButton let countLabel: UILabel let counterStateLabel: UILabel private var cancellables: [AnyCancellable] = [] private let viewModel = ViewModel() ... } ViewControllerͷఆٛ
  10. final class CounterViewController: UIViewController { let counterToggle: UISwitch let incrementButton:

    UIButton let decrementButton: UIButton let countLabel: UILabel let counterStateLabel: UILabel private var cancellables: [AnyCancellable] = [] private let viewModel = ViewModel() ... } ViewControllerͷఆٛ
  11. final class CounterViewController: UIViewController { ... override func viewDidLoad() {

    ... incrementButton.tap .map { _ in () } .subscribe(viewModel.increment) decrementButton.tap .map { _ in () } .subscribe(viewModel.decrement) counterToggle.valueChanged .subscribe(viewModel.isOn) ... } } ViewController͔Βͷೖྗ
  12. final class CounterViewController: UIViewController { ... override func viewDidLoad() {

    ... incrementButton.tap .map { _ in () } .subscribe(viewModel.increment) decrementButton.tap .map { _ in () } .subscribe(viewModel.decrement) counterToggle.valueChanged .subscribe(viewModel.isOn) ... } } ͷλοϓΠϕϯτΛ7JFX.PEFMʹ఻͑Δ ViewController͔Βͷೖྗ
  13. final class CounterViewController: UIViewController { ... override func viewDidLoad() {

    ... incrementButton.tap .map { _ in () } .subscribe(viewModel.increment) decrementButton.tap .map { _ in () } .subscribe(viewModel.decrement) counterToggle.valueChanged .subscribe(viewModel.isOn) ... } } ͷλοϓΠϕϯτΛ7JFX.PEFMʹ఻͑Δ ViewController͔Βͷೖྗ
  14. final class CounterViewController: UIViewController { ... override func viewDidLoad() {

    ... incrementButton.tap .map { _ in () } .subscribe(viewModel.increment) decrementButton.tap .map { _ in () } .subscribe(viewModel.decrement) counterToggle.valueChanged .subscribe(viewModel.isOn) ... } } ViewController͔Βͷೖྗ 6*4XJUDIͷ஋ͷมԽΛ7JFX.PEFMʹ఻͑Δ
  15. final class CounterViewController: UIViewController { ... override func viewDidLoad() {

    ... viewModel.count .assign(to: \.text, on: countLabel) .store(in: &cancellables) viewModel.isIncrementEnabled .assign(to: \.isEnabled, on: incrementButton) .store(in: &cancellables) viewModel.isDecrementEnabled .assign(to: \.isEnabled, on: decrementButton) .store(in: &cancellables) } } ViewController΁ͷ൓ө
  16. final class CounterViewController: UIViewController { ... override func viewDidLoad() {

    ... viewModel.count .assign(to: \.text, on: countLabel) .store(in: &cancellables) viewModel.isIncrementEnabled .assign(to: \.isEnabled, on: incrementButton) .store(in: &cancellables) viewModel.isDecrementEnabled .assign(to: \.isEnabled, on: decrementButton) .store(in: &cancellables) } } Χ΢ϯτΛ6*-BCFMʹ൓ө ViewController΁ͷ൓ө
  17. final class CounterViewController: UIViewController { ... override func viewDidLoad() {

    ... viewModel.count .assign(to: \.text, on: countLabel) .store(in: &cancellables) viewModel.isIncrementEnabled .assign(to: \.isEnabled, on: incrementButton) .store(in: &cancellables) viewModel.isDecrementEnabled .assign(to: \.isEnabled, on: decrementButton) .store(in: &cancellables) } } ͕༗ޮ͔Ͳ͏͔Λ൓ө ViewController΁ͷ൓ө
  18. final class CounterViewController: UIViewController { ... override func viewDidLoad() {

    ... viewModel.count .assign(to: \.text, on: countLabel) .store(in: &cancellables) viewModel.isIncrementEnabled .assign(to: \.isEnabled, on: incrementButton) .store(in: &cancellables) viewModel.isDecrementEnabled .assign(to: \.isEnabled, on: decrementButton) .store(in: &cancellables) } } ͕༗ޮ͔Ͳ͏͔Λ൓ө ViewController΁ͷ൓ө
  19. final class ViewModel { let increment: Subscribers.Sink<Void, Never> let decrement:

    Subscribers.Sink<Void, Never> let isOn: Subscribers.Sink<Bool, Never> var count: AnyPublisher<String?, Never> { $_count.map { Optional.some(String($0)) } .eraseToAnyPublisher() } var isIncrementEnabled: AnyPublisher<Bool, Never> { $_isToggleEnabled.eraseToAnyPublisher() } var isDecrementEnabled: AnyPublisher<Bool, Never> { $_isToggleEnabled.combineLatest($_count) { $0 && $1 > 0 } .eraseToAnyPublisher() } @Published private var _count: Int = 0 @Published private var _isToggleEnabled = false private var cancellables: [AnyCancellable] = [] init() { ... } } ViewModelͷఆٛ
  20. final class ViewModel { let increment: Subscribers.Sink<Void, Never> let decrement:

    Subscribers.Sink<Void, Never> let isOn: Subscribers.Sink<Bool, Never> var count: AnyPublisher<String?, Never> { $_count.map { Optional.some(String($0)) } .eraseToAnyPublisher() } var isIncrementEnabled: AnyPublisher<Bool, Never> { $_isToggleEnabled.eraseToAnyPublisher() } var isDecrementEnabled: AnyPublisher<Bool, Never> { $_isToggleEnabled.combineLatest($_count) { $0 && $1 > 0 } .eraseToAnyPublisher() } @Published private var _count: Int = 0 @Published private var _isToggleEnabled = false private var cancellables: [AnyCancellable] = [] init() { ... } } ViewModelͷఆٛ ֎෦͔Βͷೖྗ ΠϕϯτΛड͚෇͚Δ͜ͱ͚ͩͰ͖Ε͹ྑ͍ ͷͰɺࠓճͷ৔߹͸4VCTDSJCFST4JOLͰఆٛ
  21. final class ViewModel { let increment: Subscribers.Sink<Void, Never> let decrement:

    Subscribers.Sink<Void, Never> let isOn: Subscribers.Sink<Bool, Never> var count: AnyPublisher<String?, Never> { $_count.map { Optional.some(String($0)) } .eraseToAnyPublisher() } var isIncrementEnabled: AnyPublisher<Bool, Never> { $_isToggleEnabled.eraseToAnyPublisher() } var isDecrementEnabled: AnyPublisher<Bool, Never> { $_isToggleEnabled.combineLatest($_count) { $0 && $1 > 0 } .eraseToAnyPublisher() } @Published private var _count: Int = 0 @Published private var _isToggleEnabled = false private var cancellables: [AnyCancellable] = [] init() { ... } } ViewModelͷఆٛ ֎෦΁ͷग़ྗ ΠϕϯτΛग़ྗ͢Δ͜ͱ͚ͩͰ͖Ε͹ྑ͍ ͷͰɺࠓճͷ৔߹͸"OZ1VCMJTIFSͰఆٛ
  22. final class ViewModel { let increment: Subscribers.Sink<Void, Never> let decrement:

    Subscribers.Sink<Void, Never> let isOn: Subscribers.Sink<Bool, Never> var count: AnyPublisher<String?, Never> { $_count.map { Optional.some(String($0)) } .eraseToAnyPublisher() } var isIncrementEnabled: AnyPublisher<Bool, Never> { $_isToggleEnabled.eraseToAnyPublisher() } var isDecrementEnabled: AnyPublisher<Bool, Never> { $_isToggleEnabled.combineLatest($_count) { $0 && $1 > 0 } .eraseToAnyPublisher() } @Published private var _count: Int = 0 @Published private var _isToggleEnabled = false private var cancellables: [AnyCancellable] = [] init() { ... } } ViewModelͷఆٛ ಺෦ঢ়ଶ อ࣋ͨ͠ঢ়ଶ͕มԽͨ͜͠ͱ΋ݕ஌͍ͨ͠ ͷͰɺࠓճͷ৔߹͸!1VCMJTIFEͰఆٛ
  23. final class ViewModel { ... init() { let _increment =

    PassthroughSubject<Void, Never>() let _decrement = PassthroughSubject<Void, Never>() let _isOn = PassthroughSubject<Bool, Never>() self.increment = .init(receiveCompletion: { _increment.send(completion: $0) }, receiveValue: { _increment.send($0) }) self.decrement = .init(receiveCompletion: { _decrement.send(completion: $0) }, receiveValue: { _decrement.send($0) }) self.isOn = .init(receiveCompletion: { _isOn.send(completion: $0) }, receiveValue: { _isOn.send($0) }) _isOn.assign(to: \._isToggleEnabled, on: self).store(in: &cancellables) let increment = _increment.flatMap { [weak self] _ in self.map { Just($0._count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 + 1 } let decrement = _decrement.flatMap { [weak self] _ in self.map { Just($0._count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 > 0 ? $0 - 1 : $0 } increment.merge(with: decrement).assign(to: \._count, on: self).store(in: &cancellables) } } ViewModelͷInitializerͷ࣮૷
  24. final class ViewModel { ... init() { let _increment =

    PassthroughSubject<Void, Never>() let _decrement = PassthroughSubject<Void, Never>() let _isOn = PassthroughSubject<Bool, Never>() self.increment = .init(receiveCompletion: { _increment.send(completion: $0) }, receiveValue: { _increment.send($0) }) self.decrement = .init(receiveCompletion: { _decrement.send(completion: $0) }, receiveValue: { _decrement.send($0) }) self.isOn = .init(receiveCompletion: { _isOn.send(completion: $0) }, receiveValue: { _isOn.send($0) }) _isOn.assign(to: \._isToggleEnabled, on: self).store(in: &cancellables) let increment = _increment.flatMap { [weak self] _ in self.map { Just($0._count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 + 1 } let decrement = _decrement.flatMap { [weak self] _ in self.map { Just($0._count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 > 0 ? $0 - 1 : $0 } increment.merge(with: decrement).assign(to: \._count, on: self).store(in: &cancellables) } } ViewModelͷInitializerͷ࣮૷ ೖྗΛड͚औͬͯ*OJUBMJ[FS಺ͰΠϕϯτΛ ϦϨʔ͢ΔͨΊͷ1BTTUISPVHI4VCKFDU
  25. final class ViewModel { ... init() { let _increment =

    PassthroughSubject<Void, Never>() let _decrement = PassthroughSubject<Void, Never>() let _isOn = PassthroughSubject<Bool, Never>() self.increment = .init(receiveCompletion: { _increment.send(completion: $0) }, receiveValue: { _increment.send($0) }) self.decrement = .init(receiveCompletion: { _decrement.send(completion: $0) }, receiveValue: { _decrement.send($0) }) self.isOn = .init(receiveCompletion: { _isOn.send(completion: $0) }, receiveValue: { _isOn.send($0) }) _isOn.assign(to: \._isToggleEnabled, on: self).store(in: &cancellables) let increment = _increment.flatMap { [weak self] _ in self.map { Just($0._count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 + 1 } let decrement = _decrement.flatMap { [weak self] _ in self.map { Just($0._count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 > 0 ? $0 - 1 : $0 } increment.merge(with: decrement).assign(to: \._count, on: self).store(in: &cancellables) } } ViewModelͷInitializerͷ࣮૷ ೖྗͷΠϕϯτΛ1BTTUISPVHI4VCKFDU ʹܨ͛Δ
  26. final class ViewModel { ... init() { let _increment =

    PassthroughSubject<Void, Never>() let _decrement = PassthroughSubject<Void, Never>() let _isOn = PassthroughSubject<Bool, Never>() self.increment = .init(receiveCompletion: { _increment.send(completion: $0) }, receiveValue: { _increment.send($0) }) self.decrement = .init(receiveCompletion: { _decrement.send(completion: $0) }, receiveValue: { _decrement.send($0) }) self.isOn = .init(receiveCompletion: { _isOn.send(completion: $0) }, receiveValue: { _isOn.send($0) }) _isOn.assign(to: \._isToggleEnabled, on: self).store(in: &cancellables) let increment = _increment.flatMap { [weak self] _ in self.map { Just($0._count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 + 1 } let decrement = _decrement.flatMap { [weak self] _ in self.map { Just($0._count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 > 0 ? $0 - 1 : $0 } increment.merge(with: decrement).assign(to: \._count, on: self).store(in: &cancellables) } } ViewModelͷInitializerͷ࣮૷ @JT0O͔ΒͷΠϕϯτΛ΋ͱʹ಺෦ঢ়ଶΛߋ৽
  27. final class ViewModel { ... init() { let _increment =

    PassthroughSubject<Void, Never>() let _decrement = PassthroughSubject<Void, Never>() let _isOn = PassthroughSubject<Bool, Never>() self.increment = .init(receiveCompletion: { _increment.send(completion: $0) }, receiveValue: { _increment.send($0) }) self.decrement = .init(receiveCompletion: { _decrement.send(completion: $0) }, receiveValue: { _decrement.send($0) }) self.isOn = .init(receiveCompletion: { _isOn.send(completion: $0) }, receiveValue: { _isOn.send($0) }) _isOn.assign(to: \._isToggleEnabled, on: self).store(in: &cancellables) let increment = _increment.flatMap { [weak self] _ in self.map { Just($0._count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 + 1 } let decrement = _decrement.flatMap { [weak self] _ in self.map { Just($0._count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 > 0 ? $0 - 1 : $0 } increment.merge(with: decrement).assign(to: \._count, on: self).store(in: &cancellables) } } ViewModelͷInitializerͷ࣮૷ @JODSFNFOU͔ΒͷΠϕϯτΛτϦΨʔʹ ಺෦ঢ়ଶͷ@DPVOUʹରͯ͠ ͨ͠஋Λྲྀ͢
  28. final class ViewModel { ... init() { let _increment =

    PassthroughSubject<Void, Never>() let _decrement = PassthroughSubject<Void, Never>() let _isOn = PassthroughSubject<Bool, Never>() self.increment = .init(receiveCompletion: { _increment.send(completion: $0) }, receiveValue: { _increment.send($0) }) self.decrement = .init(receiveCompletion: { _decrement.send(completion: $0) }, receiveValue: { _decrement.send($0) }) self.isOn = .init(receiveCompletion: { _isOn.send(completion: $0) }, receiveValue: { _isOn.send($0) }) _isOn.assign(to: \._isToggleEnabled, on: self).store(in: &cancellables) let increment = _increment.flatMap { [weak self] _ in self.map { Just($0._count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 + 1 } let decrement = _decrement.flatMap { [weak self] _ in self.map { Just($0._count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 > 0 ? $0 - 1 : $0 } increment.merge(with: decrement).assign(to: \._count, on: self).store(in: &cancellables) } } ViewModelͷInitializerͷ࣮૷ @EFDSFNFOU͔ΒͷΠϕϯτΛτϦΨʔʹ ಺෦ঢ়ଶͷ@DPVOUʹରͯͨ͠͠஋ΛΑΓ େ͖͍஋ʹͯ͠ྲྀ͢
  29. final class ViewModel { ... init() { let _increment =

    PassthroughSubject<Void, Never>() let _decrement = PassthroughSubject<Void, Never>() let _isOn = PassthroughSubject<Bool, Never>() self.increment = .init(receiveCompletion: { _increment.send(completion: $0) }, receiveValue: { _increment.send($0) }) self.decrement = .init(receiveCompletion: { _decrement.send(completion: $0) }, receiveValue: { _decrement.send($0) }) self.isOn = .init(receiveCompletion: { _isOn.send(completion: $0) }, receiveValue: { _isOn.send($0) }) _isOn.assign(to: \._isToggleEnabled, on: self).store(in: &cancellables) let increment = _increment.flatMap { [weak self] _ in self.map { Just($0._count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 + 1 } let decrement = _decrement.flatMap { [weak self] _ in self.map { Just($0._count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 > 0 ? $0 - 1 : $0 } increment.merge(with: decrement).assign(to: \._count, on: self).store(in: &cancellables) } } ViewModelͷInitializerͷ࣮૷ JODSFNFOUͱEFDSFNFOUͷΠϕϯτΛ ΋ͱʹ಺෦ঢ়ଶΛߋ৽
  30. final class ViewModel { let increment: Subscribers.Sink<Void, Never> let decrement:

    Subscribers.Sink<Void, Never> let isOn: Subscribers.Sink<Bool, Never> var count: AnyPublisher<String?, Never> { $_count.map { Optional.some(String($0)) } .eraseToAnyPublisher() } var isIncrementEnabled: AnyPublisher<Bool, Never> { $_isToggleEnabled.eraseToAnyPublisher() } var isDecrementEnabled: AnyPublisher<Bool, Never> { $_isToggleEnabled.combineLatest($_count) { $0 && $1 > 0 } .eraseToAnyPublisher() } @Published private var _count: Int = 0 @Published private var _isToggleEnabled = false private var cancellables: [AnyCancellable] = [] init() { ... } } 4UPSFE1SPQFSUZͰఆ͔͕ٛͨͬͨ͠ 1SPQFSUZ8SBQQFSΛར༻͍ͯ͠Δ1SPQFSUZʹ ΞΫηε͢ΔͨΊʹ͸ɺ͢΂ͯͷ1SPQFSUZͷॳظԽ ͕׬͍ྃͯ͠Δඞཁ͕͋ΔͨΊɺ$PNQVUFEͰఆٛ ViewModelͷInitializerʹऩ·Βͳ͔࣮ͬͨ૷
  31. final class ViewModel { let increment: Subscribers.Sink<Void, Never> let decrement:

    Subscribers.Sink<Void, Never> let isOn: Subscribers.Sink<Bool, Never> var count: AnyPublisher<String?, Never> { $_count.map { Optional.some(String($0)) } .eraseToAnyPublisher() } var isIncrementEnabled: AnyPublisher<Bool, Never> { $_isToggleEnabled.eraseToAnyPublisher() } var isDecrementEnabled: AnyPublisher<Bool, Never> { $_isToggleEnabled.combineLatest($_count) { $0 && $1 > 0 } .eraseToAnyPublisher() } @Published private var _count: Int = 0 @Published private var _isToggleEnabled = false private var cancellables: [AnyCancellable] = [] init() { ... } } ViewModelͷInitializerʹऩ·Βͳ͔࣮ͬͨ૷ ಺෦ঢ়ଶͷ@DPVOU͕มߋ͞ΕͨΒ 4USJOH ʹม׵ͯ͠ग़ྗ
  32. final class ViewModel { let increment: Subscribers.Sink<Void, Never> let decrement:

    Subscribers.Sink<Void, Never> let isOn: Subscribers.Sink<Bool, Never> var count: AnyPublisher<String?, Never> { $_count.map { Optional.some(String($0)) } .eraseToAnyPublisher() } var isIncrementEnabled: AnyPublisher<Bool, Never> { $_isToggleEnabled.eraseToAnyPublisher() } var isDecrementEnabled: AnyPublisher<Bool, Never> { $_isToggleEnabled.combineLatest($_count) { $0 && $1 > 0 } .eraseToAnyPublisher() } @Published private var _count: Int = 0 @Published private var _isToggleEnabled = false private var cancellables: [AnyCancellable] = [] init() { ... } } ViewModelͷInitializerʹऩ·Βͳ͔࣮ͬͨ૷ ಺෦ঢ়ଶͷ@JT5PHHMF&OBCMFE͕มߋ͞ΕͨΒ ͦͷΠϕϯτΛ֎෦ʹग़ྗ
  33. final class ViewModel { let increment: Subscribers.Sink<Void, Never> let decrement:

    Subscribers.Sink<Void, Never> let isOn: Subscribers.Sink<Bool, Never> var count: AnyPublisher<String?, Never> { $_count.map { Optional.some(String($0)) } .eraseToAnyPublisher() } var isIncrementEnabled: AnyPublisher<Bool, Never> { $_isToggleEnabled.eraseToAnyPublisher() } var isDecrementEnabled: AnyPublisher<Bool, Never> { $_isToggleEnabled.combineLatest($_count) { $0 && $1 > 0 } .eraseToAnyPublisher() } @Published private var _count: Int = 0 @Published private var _isToggleEnabled = false private var cancellables: [AnyCancellable] = [] init() { ... } } ViewModelͷInitializerʹऩ·Βͳ͔࣮ͬͨ૷ ಺෦ঢ়ଶͷ@JT5PHHMF&OBCMFE·ͨ͸@DPVOU͕ มߋ͞ΕͨΒɺ@JT5PHHMF&OBCMFE͕USVF͔ͭ @DPVOU͕ΑΓେ͖͍ͱ͍͏஋ʹม׵ͯ͠ग़ྗ
  34. final class CounterViewController: UIViewController { let counterToggle: UISwitch let incrementButton:

    UIButton let decrementButton: UIButton let countLabel: UILabel let counterStateLabel: UILabel private var cancellables: [AnyCancellable] = [] private let viewModel = ViewModel() override func viewDidLoad() { ... incrementButton.tap .map { _ in () } .subscribe(viewModel.increment) decrementButton.tap .map { _ in () } .subscribe(viewModel.decrement) counterToggle.valueChanged .subscribe(viewModel.isOn) viewModel.count .assign(to: \.text, on: countLabel) .store(in: &cancellables) viewModel.isIncrementEnabled .assign(to: \.isEnabled, on: incrementButton) .store(in: &cancellables) viewModel.isDecrementEnabled .assign(to: \.isEnabled, on: decrementButton) .store(in: &cancellables) } } ViewControllerͷ࣮૷ͷશମ૾
  35. import UIKit import Ricemill

  36. final class CounterViewController: UIViewController { let counterToggle: UISwitch let incrementButton:

    UIButton let decrementButton: UIButton let countLabel: UILabel let counterStateLabel: UILabel private var cancellables: [AnyCancellable] = [] private let viewModel = ViewModel(input: .init(), store: .init(), extra: .init()) ... } ViewControllerͷఆٛ
  37. final class CounterViewController: UIViewController { ... override func viewDidLoad() {

    ... let input = viewModel.input incrementButton.tap .map { _ in () } .subscribe(input.increment) .store(in: &cancellables) decrementButton.tap .map { _ in () } .subscribe(input.decrement) .store(in: &cancellables) counterToggle.valueChanged .subscribe(input.isOn) .store(in: &cancellables) ... } } ViewController͔Βͷೖྗ
  38. final class CounterViewController: UIViewController { ... override func viewDidLoad() {

    ... let input = viewModel.input incrementButton.tap .map { _ in () } .subscribe(input.increment) .store(in: &cancellables) decrementButton.tap .map { _ in () } .subscribe(input.decrement) .store(in: &cancellables) counterToggle.valueChanged .subscribe(input.isOn) .store(in: &cancellables) ... } } ViewController͔Βͷೖྗ 7JFX.PEFMͷೖྗ͕໌ࣔతʹͳ͍ͬͯΔ
  39. final class CounterViewController: UIViewController { ... override func viewDidLoad() {

    ... let output = viewModel.output output.count .assign(to: \.text, on: countLabel) .store(in: &cancellables) output.isIncrementEnabled .assign(to: \.isEnabled, on: incrementButton) .store(in: &cancellables) output.isDecrementEnabled .assign(to: \.isEnabled, on: decrementButton) .store(in: &cancellables) } } ViewController΁ͷ൓ө
  40. final class CounterViewController: UIViewController { ... override func viewDidLoad() {

    ... let output = viewModel.output output.count .assign(to: \.text, on: countLabel) .store(in: &cancellables) output.isIncrementEnabled .assign(to: \.isEnabled, on: incrementButton) .store(in: &cancellables) output.isDecrementEnabled .assign(to: \.isEnabled, on: decrementButton) .store(in: &cancellables) } } ViewController΁ͷ൓ө 7JFX.PEFM͔Βͷग़ྗ͕໌ࣔతʹͳ͍ͬͯΔ
  41. final class CounterViewModel: Machine<CounterViewModel.Resolver> { struct Input: InputType { let

    increment = PassthroughSubject<Void, Never>() let decrement = PassthroughSubject<Void, Never>() let isOn = PassthroughSubject<Bool, Never>() } struct Output: OutputType { let count: AnyPublisher<String?, Never> let isIncrementEnabled: AnyPublisher<Bool, Never> let isDecrementEnabled: AnyPublisher<Bool, Never> } final class Store: StoreType { @Published var count: Int = 0 @Published var isToggleEnabled = false } struct Extra: ExtraType {} enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store, extra: Extra) -> Polished<Output> { ... return Polished(output: Output(count: count, isIncrementEnabled: incrementEnabled, isDecrementEnabled: isDecrementEnabled), cancellables: cancellables) } } } ViewModelͷఆٛ
  42. final class CounterViewModel: Machine<CounterViewModel.Resolver> { struct Input: InputType { let

    increment = PassthroughSubject<Void, Never>() let decrement = PassthroughSubject<Void, Never>() let isOn = PassthroughSubject<Bool, Never>() } struct Output: OutputType { let count: AnyPublisher<String?, Never> let isIncrementEnabled: AnyPublisher<Bool, Never> let isDecrementEnabled: AnyPublisher<Bool, Never> } final class Store: StoreType { @Published var count: Int = 0 @Published var isToggleEnabled = false } struct Extra: ExtraType {} enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store, extra: Extra) -> Polished<Output> { ... return Polished(output: Output(count: count, isIncrementEnabled: incrementEnabled, isDecrementEnabled: isDecrementEnabled), cancellables: cancellables) } } } ViewModelͷఆٛ
  43. struct Input: InputType { let increment = PassthroughSubject<Void, Never>() let

    decrement = PassthroughSubject<Void, Never>() let isOn = PassthroughSubject<Bool, Never>() } let input: InputProxy<Input> let isOn: SubjectProxy<PassthroughSubject<Bool, Never>> = input.isOn isOn.send(true) @dynamicMemberLookup final class InputProxy<Input: InputType> { private let input: Input init(_ input: Input) { self.input = input } subscript<S: Subject>(dynamicMember keyPath: KeyPath<Input, S>) -> SubjectProxy<S> { SubjectProxy(input[keyPath: keyPath]) } } RicemillͷInput
  44. struct Input: InputType { let increment = PassthroughSubject<Void, Never>() let

    decrement = PassthroughSubject<Void, Never>() let isOn = PassthroughSubject<Bool, Never>() } let input: InputProxy<Input> let isOn: SubjectProxy<PassthroughSubject<Bool, Never>> = input.isOn isOn.send(true) @dynamicMemberLookup final class InputProxy<Input: InputType> { private let input: Input init(_ input: Input) { self.input = input } subscript<S: Subject>(dynamicMember keyPath: KeyPath<Input, S>) -> SubjectProxy<S> { SubjectProxy(input[keyPath: keyPath]) } } RicemillͷInput 3JDFNJMMͰ͸*OQVU͕*OQVU1SPYZʹ ϥοϓ͞Εͨঢ়ଶͰެ։͞ΕΔ
  45. struct Input: InputType { let increment = PassthroughSubject<Void, Never>() let

    decrement = PassthroughSubject<Void, Never>() let isOn = PassthroughSubject<Bool, Never>() } let input: InputProxy<Input> let isOn: SubjectProxy<PassthroughSubject<Bool, Never>> = input.isOn isOn.send(true) @dynamicMemberLookup final class InputProxy<Input: InputType> { private let input: Input init(_ input: Input) { self.input = input } subscript<S: Subject>(dynamicMember keyPath: KeyPath<Input, S>) -> SubjectProxy<S> { SubjectProxy(input[keyPath: keyPath]) } } RicemillͷInput 4VCKFDU1SPYZʹϥοϓ͞Εͨঢ়ଶͷ4VCKFDUऔಘ͢Δ
  46. struct Input: InputType { let increment = PassthroughSubject<Void, Never>() let

    decrement = PassthroughSubject<Void, Never>() let isOn = PassthroughSubject<Bool, Never>() } let input: InputProxy<Input> let isOn: SubjectProxy<PassthroughSubject<Bool, Never>> = input.isOn isOn.send(true) @dynamicMemberLookup final class InputProxy<Input: InputType> { private let input: Input init(_ input: Input) { self.input = input } subscript<S: Subject>(dynamicMember keyPath: KeyPath<Input, S>) -> SubjectProxy<S> { SubjectProxy(input[keyPath: keyPath]) } } RicemillͷInput 4XJGU͔Βར༻ՄೳʹͳͬͨUZQFTBGFͳ,FZ1BUI ϕʔεͷEZOBNJD.FNCFS-PPLVQΛར༻ͯ͠ QSPQFSUZʹΞΫηε͍ͯ͠Δ͔ͷΑ͏ͳJOUFSGBDFͰ ܕม׵ͨ͠ΠϯελϯεΛऔಘ͢Δ ࣮ࡍͷJOQVU͸QSJWBUFʹͳ͍ͬͯΔͷͰɺ֎෦͔Β͸ ௚઀ΞΫηε͢Δ͜ͱ͕Ͱ͖ͳ͍
  47. struct Input: InputType { let increment = PassthroughSubject<Void, Never>() let

    decrement = PassthroughSubject<Void, Never>() let isOn = PassthroughSubject<Bool, Never>() } let input: InputProxy<Input> let isOn: SubjectProxy<PassthroughSubject<Bool, Never>> = input.isOn isOn.send(true) @dynamicMemberLookup final class InputProxy<Input: InputType> { private let input: Input init(_ input: Input) { self.input = input } subscript<S: Subject>(dynamicMember keyPath: KeyPath<Input, S>) -> SubjectProxy<S> { SubjectProxy(input[keyPath: keyPath]) } } RicemillͷInput 4VCKFDU1SPYZͰ͸ ɾGVODTFOE @  ɾGVODTFOE DPNQMFUJPO  ɾGVODTFOE TVCTDSJQUJPO  ͷΈ͕ެ։͞Ε͍ͯΔͷͰɺೖྗʹಛԽͨ͠ܕʹͳ͍ͬͯΔ
  48. final class CounterViewModel: Machine<CounterViewModel.Resolver> { struct Input: InputType { let

    increment = PassthroughSubject<Void, Never>() let decrement = PassthroughSubject<Void, Never>() let isOn = PassthroughSubject<Bool, Never>() } struct Output: OutputType { let count: AnyPublisher<String?, Never> let isIncrementEnabled: AnyPublisher<Bool, Never> let isDecrementEnabled: AnyPublisher<Bool, Never> } final class Store: StoreType { @Published var count: Int = 0 @Published var isToggleEnabled = false } struct Extra: ExtraType {} enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store, extra: Extra) -> Polished<Output> { ... return Polished(output: Output(count: count, isIncrementEnabled: incrementEnabled, isDecrementEnabled: isDecrementEnabled), cancellables: cancellables) } } } ViewModelͷఆٛ
  49. struct Output: OutputType { let count: AnyPublisher<String?, Never> let isIncrementEnabled:

    AnyPublisher<Bool, Never> let isDecrementEnabled: AnyPublisher<Bool, Never> } let output: OutputProxy<Output> let count: AnyPublisher<String?, Never> = output.count let cancellable = count.sink(receiveValue: { print(String(describing: $0)) }) @dynamicMemberLookup final class OutputProxy<Output: OutputType> { private let output: Output init(_ output: Output) { self.output = output } subscript<P: Publisher>(dynamicMember keyPath: KeyPath<Output, P>) -> AnyPublisher<P.Output, P.Failure> { output[keyPath: keyPath].eraseToAnyPublisher() } } RicemillͷOutput
  50. struct Output: OutputType { let count: AnyPublisher<String?, Never> let isIncrementEnabled:

    AnyPublisher<Bool, Never> let isDecrementEnabled: AnyPublisher<Bool, Never> } let output: OutputProxy<Output> let count: AnyPublisher<String?, Never> = output.count let cancellable = count.sink(receiveValue: { print(String(describing: $0)) }) @dynamicMemberLookup final class OutputProxy<Output: OutputType> { private let output: Output init(_ output: Output) { self.output = output } subscript<P: Publisher>(dynamicMember keyPath: KeyPath<Output, P>) -> AnyPublisher<P.Output, P.Failure> { output[keyPath: keyPath].eraseToAnyPublisher() } } RicemillͷOutput 3JDFNJMMͰ͸0VUQVU͕0VUQVU1SPYZʹ ϥοϓ͞Εͨঢ়ଶͰެ։͞ΕΔ
  51. struct Output: OutputType { let count: AnyPublisher<String?, Never> let isIncrementEnabled:

    AnyPublisher<Bool, Never> let isDecrementEnabled: AnyPublisher<Bool, Never> } let output: OutputProxy<Output> let count: AnyPublisher<String?, Never> = output.count let cancellable = count.sink(receiveValue: { print(String(describing: $0)) }) @dynamicMemberLookup final class OutputProxy<Output: OutputType> { private let output: Output init(_ output: Output) { self.output = output } subscript<P: Publisher>(dynamicMember keyPath: KeyPath<Output, P>) -> AnyPublisher<P.Output, P.Failure> { output[keyPath: keyPath].eraseToAnyPublisher() } } RicemillͷOutput "OZ1VCMJTIFSʹUZQFFSBTFͨ͠ΠϯελϯεΛऔಘ͢Δ ˞ࠓճͷ৔߹ɺ΋ͱ΋ͱ"OZ1VCMJTIFSͳͷͰܕม׵ͳ͠
  52. struct Output: OutputType { let count: AnyPublisher<String?, Never> let isIncrementEnabled:

    AnyPublisher<Bool, Never> let isDecrementEnabled: AnyPublisher<Bool, Never> } let output: OutputProxy<Output> let count: AnyPublisher<String?, Never> = output.count let cancellable = count.sink(receiveValue: { print(String(describing: $0)) }) @dynamicMemberLookup final class OutputProxy<Output: OutputType> { private let output: Output init(_ output: Output) { self.output = output } subscript<P: Publisher>(dynamicMember keyPath: KeyPath<Output, P>) -> AnyPublisher<P.Output, P.Failure> { output[keyPath: keyPath].eraseToAnyPublisher() } } RicemillͷOutput EZOBNJD.FNCFS-PPLVQΛར༻ͯ͠ɺ1VCMJTIFSʹ ४ڌ͍ͯ͠ΔΦϒδΣΫτʢ1BTTUISPVHI4VCKFDUɺ 1VCMJTIFST˓˓ͳͲʣΛ"OZ1VCMJTIFSʹܕม׵ͨ͠ ΠϯελϯεͰऔಘ͢Δ ࣮ࡍͷPVUQVU͸QSJWBUFʹͳ͍ͬͯΔͷͰɺ֎෦͔Β͸ ௚઀ΞΫηε͢Δ͜ͱ͕Ͱ͖ͳ͍
  53. struct Output: OutputType { let count: AnyPublisher<String?, Never> let isIncrementEnabled:

    AnyPublisher<Bool, Never> let isDecrementEnabled: AnyPublisher<Bool, Never> } let output: OutputProxy<Output> let count: AnyPublisher<String?, Never> = output.count let cancellable = count.sink(receiveValue: { print(String(describing: $0)) }) @dynamicMemberLookup final class OutputProxy<Output: OutputType> { private let output: Output init(_ output: Output) { self.output = output } subscript<P: Publisher>(dynamicMember keyPath: KeyPath<Output, P>) -> AnyPublisher<P.Output, P.Failure> { output[keyPath: keyPath].eraseToAnyPublisher() } } RicemillͷOutput "OZ1VCMJTIFSͳͷͰɺग़ྗʹಛԽͨ͠ܕʹͳ͍ͬͯΔ
  54. final class CounterViewModel: Machine<CounterViewModel.Resolver> { struct Input: InputType { let

    increment = PassthroughSubject<Void, Never>() let decrement = PassthroughSubject<Void, Never>() let isOn = PassthroughSubject<Bool, Never>() } struct Output: OutputType { let count: AnyPublisher<String?, Never> let isIncrementEnabled: AnyPublisher<Bool, Never> let isDecrementEnabled: AnyPublisher<Bool, Never> } final class Store: StoreType { @Published var count: Int = 0 @Published var isToggleEnabled = false } struct Extra: ExtraType {} enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store, extra: Extra) -> Polished<Output> { ... return Polished(output: Output(count: count, isIncrementEnabled: incrementEnabled, isDecrementEnabled: isDecrementEnabled), cancellables: cancellables) } } } ViewModelͷఆٛ - Store ஋ͷมߋΛ؂ࢹͰ͖ΔΑ͏ʹ͢ΔͨΊɺ!1VCMJTIFEͰఆٛΛ͢Δ 3JDFNJMͰ͸4UPSF͸3FTPMWFSͷGVODQPMJTI JOQVUTUPSFFYUSB  ͔ΒͷΈࢀরՄೳͳͷͰɺJOUFSOBMͰఆٛͯ͠΋BDDFTTMFWFM͸໰୊ͳ͍
  55. final class CounterViewModel: Machine<CounterViewModel.Resolver> { struct Input: InputType { let

    increment = PassthroughSubject<Void, Never>() let decrement = PassthroughSubject<Void, Never>() let isOn = PassthroughSubject<Bool, Never>() } struct Output: OutputType { let count: AnyPublisher<String?, Never> let isIncrementEnabled: AnyPublisher<Bool, Never> let isDecrementEnabled: AnyPublisher<Bool, Never> } final class Store: StoreType { @Published var count: Int = 0 @Published var isToggleEnabled = false } struct Extra: ExtraType {} enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store, extra: Extra) -> Polished<Output> { ... return Polished(output: Output(count: count, isIncrementEnabled: incrementEnabled, isDecrementEnabled: isDecrementEnabled), cancellables: cancellables) } } } ViewModelͷఆٛ - Extra ֎෦ґଘΛఆٛ͢Δ ˞ࠓճͷ৔߹͸֎෦ґଘͳ͠
  56. final class CounterViewModel: Machine<CounterViewModel.Resolver> { struct Input: InputType { let

    increment = PassthroughSubject<Void, Never>() let decrement = PassthroughSubject<Void, Never>() let isOn = PassthroughSubject<Bool, Never>() } struct Output: OutputType { let count: AnyPublisher<String?, Never> let isIncrementEnabled: AnyPublisher<Bool, Never> let isDecrementEnabled: AnyPublisher<Bool, Never> } final class Store: StoreType { @Published var count: Int = 0 @Published var isToggleEnabled = false } struct Extra: ExtraType {} enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store, extra: Extra) -> Polished<Output> { ... return Polished(output: Output(count: count, isIncrementEnabled: incrementEnabled, isDecrementEnabled: isDecrementEnabled), cancellables: cancellables) } } } ViewModelͷఆٛ
  57. public protocol ResolverType { associatedtype Input: InputType associatedtype Output: OutputType

    associatedtype Store: StoreType associatedtype Extra: ExtraType static func polish(input: Publishing<Input>, store: Store, extra: Extra) -> Polished<Output> } RicemillͷResolverͷఆٛ *OQVUɾ0VUQVUɾ4UPSFɾ&YUSBΛඥ෇͚ͯ *OQVUɾ4UPSFɾ&YUSB͔Β0VUQVUΛੜ੒͢Δ
  58. enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store,

    extra: Extra) -> Polished<Output> { var cancellables: [AnyCancellable] = [] let increment = input.increment .flatMap { _ in Just(store.count) } .map { $0 + 1 } let decrement = input.decrement .flatMap { _ in Just(store.count) } .map { $0 > 0 ? $0 - 1 : $0 } increment.merge(with: decrement) .assign(to: \.count, on: store) .store(in: &cancellables) input.isOn .assign(to: \.isToggleEnabled, on: store) .store(in: &cancellables) ... } } RicemillͷResolverͷ࣮૷
  59. enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store,

    extra: Extra) -> Polished<Output> { var cancellables: [AnyCancellable] = [] let increment = input.increment .flatMap { _ in Just(store.count) } .map { $0 + 1 } let decrement = input.decrement .flatMap { _ in Just(store.count) } .map { $0 > 0 ? $0 - 1 : $0 } increment.merge(with: decrement) .assign(to: \.count, on: store) .store(in: &cancellables) input.isOn .assign(to: \.isToggleEnabled, on: store) .store(in: &cancellables) ... } } RicemillͷResolverͷ࣮૷ 1VCMJTIJOHܦ༝Ͱ֎෦͔ΒͷೖྗΛɺ಺෦޲͚ͷ ग़ྗͱͯͯ͠͠ड͚औΔ
  60. struct Input: InputType { let increment = PassthroughSubject<Void, Never>() let

    decrement = PassthroughSubject<Void, Never>() let isOn = PassthroughSubject<Bool, Never>() } let input: Publishing<Input> let increment: AnyPublisher<Void, Never> = input.increment @dynamicMemberLookup final class Publishing<Input: InputType> { private let input: Input init(_ input: Input) { self.input = input } subscript<P: Publisher>(dynamicMember keyPath: KeyPath<Input, P>) -> AnyPublisher<P.Output, P.Failure> { input[keyPath: keyPath].eraseToAnyPublisher() } } RicemillͷResolver - Publishing
  61. struct Input: InputType { let increment = PassthroughSubject<Void, Never>() let

    decrement = PassthroughSubject<Void, Never>() let isOn = PassthroughSubject<Bool, Never>() } let input: Publishing<Input> let increment: AnyPublisher<Void, Never> = input.increment @dynamicMemberLookup final class Publishing<Input: InputType> { private let input: Input init(_ input: Input) { self.input = input } subscript<P: Publisher>(dynamicMember keyPath: KeyPath<Input, P>) -> AnyPublisher<P.Output, P.Failure> { input[keyPath: keyPath].eraseToAnyPublisher() } } RicemillͷResolver - Publishing "OZ1VCMJTIFSʹUZQFFSBTFͨ͠ΠϯελϯεΛऔಘ͢Δ
  62. struct Input: InputType { let increment = PassthroughSubject<Void, Never>() let

    decrement = PassthroughSubject<Void, Never>() let isOn = PassthroughSubject<Bool, Never>() } let input: Publishing<Input> let increment: AnyPublisher<Void, Never> = input.increment @dynamicMemberLookup final class Publishing<Input: InputType> { private let input: Input init(_ input: Input) { self.input = input } subscript<P: Publisher>(dynamicMember keyPath: KeyPath<Input, P>) -> AnyPublisher<P.Output, P.Failure> { input[keyPath: keyPath].eraseToAnyPublisher() } } RicemillͷResolver - Publishing EZOBNJD.FNCFS-PPLVQΛར༻ͯ͠ɺ1VCMJTIFSʹ ४ڌ͍ͯ͠ΔΦϒδΣΫτʢ1BTTUISPVHI4VCKFDUɺ 1VCMJTIFST˓˓ͳͲʣΛ"OZ1VCMJTIFSʹܕม׵ͨ͠ ΠϯελϯεͰऔಘ͢Δ ࣮ࡍͷJOQVU͸QSJWBUFʹͳ͍ͬͯΔͷͰɺ֎෦͔Β͸ ௚઀ΞΫηε͢Δ͜ͱ͕Ͱ͖ͳ͍
  63. enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store,

    extra: Extra) -> Polished<Output> { ... let count = store.$count .map(String.init) .map(Optional.some) .eraseToAnyPublisher() let incrementEnabled = store.$isToggleEnabled .eraseToAnyPublisher() let isDecrementEnabled = store.$isToggleEnabled .combineLatest(store.$count) .map { $0 && $1 > 0 } .eraseToAnyPublisher() return Polished(output: Output(count: count, isIncrementEnabled: incrementEnabled, isDecrementEnabled: isDecrementEnabled), cancellables: cancellables) } } RicemillͷResolverͷ࣮૷
  64. enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store,

    extra: Extra) -> Polished<Output> { ... let count = store.$count .map(String.init) .map(Optional.some) .eraseToAnyPublisher() let incrementEnabled = store.$isToggleEnabled .eraseToAnyPublisher() let isDecrementEnabled = store.$isToggleEnabled .combineLatest(store.$count) .map { $0 && $1 > 0 } .eraseToAnyPublisher() return Polished(output: Output(count: count, isIncrementEnabled: incrementEnabled, isDecrementEnabled: isDecrementEnabled), cancellables: cancellables) } } RicemillͷResolverͷ࣮૷ ಺෦ঢ়ଶͷมߋΛ΋ͱʹ0VUQVUΛੜ੒
  65. enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store,

    extra: Extra) -> Polished<Output> { ... let count = store.$count .map(String.init) .map(Optional.some) .eraseToAnyPublisher() let incrementEnabled = store.$isToggleEnabled .eraseToAnyPublisher() let isDecrementEnabled = store.$isToggleEnabled .combineLatest(store.$count) .map { $0 && $1 > 0 } .eraseToAnyPublisher() return Polished(output: Output(count: count, isIncrementEnabled: incrementEnabled, isDecrementEnabled: isDecrementEnabled), cancellables: cancellables) } } RicemillͷResolverͷ࣮૷
  66. open class Machine<Resolver: ResolverType> { public let input: InputProxy<Resolver.Input> public

    let output: OutputProxy<Resolver.Output> private let _extra: Resolver.Extra private let _store: Resolver.Store private let _cancellables: [AnyCancellable] private init(input: Resolver.Input, output: Resolver.Output, store: Resolver.Store, extra: Resolver.Extra, cancellables: [AnyCancellable]) { self.input = InputProxy(input) self.output = OutputProxy(output) self._store = store self._extra = extra self._cancellables = cancellables } public convenience init(input: Resolver.Input, store: Resolver.Store, extra: Resolver.Extra) { let receivableInput = Publishing(input) let polished = Resolver.polish(input: receivableInput, store: store, extra: extra) self.init(input: input, output: polished.output ?? { fatalError() }(), store: store, extra: extra, cancellables: polished.cancellables) } } RicemillͷResolver - Machine
  67. open class Machine<Resolver: ResolverType> { public let input: InputProxy<Resolver.Input> public

    let output: OutputProxy<Resolver.Output> private let _extra: Resolver.Extra private let _store: Resolver.Store private let _cancellables: [AnyCancellable] private init(input: Resolver.Input, output: Resolver.Output, store: Resolver.Store, extra: Resolver.Extra, cancellables: [AnyCancellable]) { self.input = InputProxy(input) self.output = OutputProxy(output) self._store = store self._extra = extra self._cancellables = cancellables } public convenience init(input: Resolver.Input, store: Resolver.Store, extra: Resolver.Extra) { let receivableInput = Publishing(input) let polished = Resolver.polish(input: receivableInput, store: store, extra: extra) self.init(input: input, output: polished.output ?? { fatalError() }(), store: store, extra: extra, cancellables: polished.cancellables) } } RicemillͷResolver - Machine .BDIJOFͷॳظԽ࣌ʹඥ෇͍͍ͯΔ3FTPMWFSͷ GVODQPMJTI JOQVUTUPSFFYUSB ͕Ұ౓͚ͩݺͼग़͠ ฦΓ஋ͷ1PMJTIFE಺ͷPVUQVUΛར༻͢Δ
  68. open class Machine<Resolver: ResolverType> { public let input: InputProxy<Resolver.Input> public

    let output: OutputProxy<Resolver.Output> private let _extra: Resolver.Extra private let _store: Resolver.Store private let _cancellables: [AnyCancellable] private init(input: Resolver.Input, output: Resolver.Output, store: Resolver.Store, extra: Resolver.Extra, cancellables: [AnyCancellable]) { self.input = InputProxy(input) self.output = OutputProxy(output) self._store = store self._extra = extra self._cancellables = cancellables } public convenience init(input: Resolver.Input, store: Resolver.Store, extra: Resolver.Extra) { let receivableInput = Publishing(input) let polished = Resolver.polish(input: receivableInput, store: store, extra: extra) self.init(input: input, output: polished.output ?? { fatalError() }(), store: store, extra: extra, cancellables: polished.cancellables) } } RicemillͷResolver - Machine *OQVU1SPYZ΍0VUQVU1SPYZͰ֘౰ͷΠϯελϯεΛ ϥοϓͯ͠ɺॳظԽΛ׬ྃ͢Δ
  69. final class CounterViewController: UIViewController { let counterToggle: UISwitch let incrementButton:

    UIButton let decrementButton: UIButton let countLabel: UILabel let counterStateLabel: UILabel private var cancellables: [AnyCancellable] = [] private let viewModel = ViewModel(input: .init(),ɹstore: .init(),ɹextra: .init()) override func viewDidLoad() { ... let input = viewModel.input incrementButton.tap.map { _ in () }.subscribe(input.increment) .store(in: &cancellables) decrementButton.tap.map { _ in () }.subscribe(input.decrement) .store(in: &cancellables) counterToggle.valueChanged.subscribe(input.isOn) .store(in: &cancellables) let output = viewModel.output output.count.assign(to: \.text, on: countLabel) .store(in: &cancellables) output.isIncrementEnabled.assign(to: \.isEnabled, on: incrementButton) .store(in: &cancellables) output.isDecrementEnabled.assign(to: \.isEnabled, on: decrementButton) .store(in: &cancellables) } } ViewControllerͷ࣮૷ͷશମ૾
  70. import SwiftUI

  71. struct CounterView: View { @ObservedObject var viewModel = ViewModel() var

    body: some View { VStack { Button(" ") { self.viewModel.increment() } .font(.system(size: 50)) .disabled(!viewModel.isIncrementEnabled) .opacity(viewModel.isIncrementEnabled ? 1 : 0.5) Text("\(viewModel.count)") .font(.system(size: 50)) Button(" ") { self.viewModel.decrement() } .font(.system(size: 50)) .disabled(!viewModel.isDecrementEnabled) .opacity(viewModel.isDecrementEnabled ? 1 : 0.5) Toggle(viewModel.toggleText, isOn: $viewModel.isOn) .frame(width: CGFloat(150), alignment: .center) } } } Viewͷ࣮૷
  72. struct CounterView: View { @ObservedObject var viewModel = ViewModel() var

    body: some View { VStack { Button(" ") { self.viewModel.increment() } .font(.system(size: 50)) .disabled(!viewModel.isIncrementEnabled) .opacity(viewModel.isIncrementEnabled ? 1 : 0.5) Text("\(viewModel.count)") .font(.system(size: 50)) Button(" ") { self.viewModel.decrement() } .font(.system(size: 50)) .disabled(!viewModel.isDecrementEnabled) .opacity(viewModel.isDecrementEnabled ? 1 : 0.5) Toggle(viewModel.toggleText, isOn: $viewModel.isOn) .frame(width: CGFloat(150), alignment: .center) } } } Viewͷ࣮૷ JT*ODSFNFOU&OBCMFEΛ΋ͱʹϘλϯͷ༗ޮɾແޮΛ ൓ө͠ɺϘλϯ͕λοϓ͞ΕΔͱJODSFNFOU ΛݺͿ
  73. struct CounterView: View { @ObservedObject var viewModel = ViewModel() var

    body: some View { VStack { Button(" ") { self.viewModel.increment() } .font(.system(size: 50)) .disabled(!viewModel.isIncrementEnabled) .opacity(viewModel.isIncrementEnabled ? 1 : 0.5) Text("\(viewModel.count)") .font(.system(size: 50)) Button(" ") { self.viewModel.decrement() } .font(.system(size: 50)) .disabled(!viewModel.isDecrementEnabled) .opacity(viewModel.isDecrementEnabled ? 1 : 0.5) Toggle(viewModel.toggleText, isOn: $viewModel.isOn) .frame(width: CGFloat(150), alignment: .center) } } } Viewͷ࣮૷ DPVOUΛ΋ͱʹ5FYUΛߋ৽
  74. struct CounterView: View { @ObservedObject var viewModel = ViewModel() var

    body: some View { VStack { Button(" ") { self.viewModel.increment() } .font(.system(size: 50)) .disabled(!viewModel.isIncrementEnabled) .opacity(viewModel.isIncrementEnabled ? 1 : 0.5) Text("\(viewModel.count)") .font(.system(size: 50)) Button(" ") { self.viewModel.decrement() } .font(.system(size: 50)) .disabled(!viewModel.isDecrementEnabled) .opacity(viewModel.isDecrementEnabled ? 1 : 0.5) Toggle(viewModel.toggleText, isOn: $viewModel.isOn) .frame(width: CGFloat(150), alignment: .center) } } } Viewͷ࣮૷ JT%FDSFNFOU&OBCMFEΛ΋ͱʹϘλϯͷ༗ޮɾແޮΛ ൓ө͠ɺϘλϯ͕λοϓ͞ΕΔͱEFDSFNFOU ΛݺͿ
  75. struct CounterView: View { @ObservedObject var viewModel = ViewModel() var

    body: some View { VStack { Button(" ") { self.viewModel.increment() } .font(.system(size: 50)) .disabled(!viewModel.isIncrementEnabled) .opacity(viewModel.isIncrementEnabled ? 1 : 0.5) Text("\(viewModel.count)") .font(.system(size: 50)) Button(" ") { self.viewModel.decrement() } .font(.system(size: 50)) .disabled(!viewModel.isDecrementEnabled) .opacity(viewModel.isDecrementEnabled ? 1 : 0.5) Toggle(viewModel.toggleText, isOn: $viewModel.isOn) .frame(width: CGFloat(150), alignment: .center) } } } Viewͷ࣮૷ Ϙλϯͷঢ়ଶ͕JT0OʹΑͬͯWJFX.PEFM΁ྲྀ͞ΕΔ
  76. final class ViewModel: ObservableObject { let increment: () -> Void

    let decrement: () -> Void @Published var isOn = false @Published private(set) var count: Int = 0 @Published private(set) var isIncrementEnabled = false @Published private(set) var isDecrementEnabled = false private var cancellables: [AnyCancellable] = [] init() { ... } } ViewModelͷఆٛ
  77. final class ViewModel: ObservableObject { let increment: () -> Void

    let decrement: () -> Void @Published var isOn = false @Published private(set) var count: Int = 0 @Published private(set) var isIncrementEnabled = false @Published private(set) var isDecrementEnabled = false private var cancellables: [AnyCancellable] = [] init() { ... } } ViewModelͷఆٛ !0CTFWFE0CKFDUͱͯ͠7JFXଆͰఆٛ Ͱ͖ΔΑ͏ʹ͢Δ
  78. final class ViewModel: ObservableObject { let increment: () -> Void

    let decrement: () -> Void @Published var isOn = false @Published private(set) var count: Int = 0 @Published private(set) var isIncrementEnabled = false @Published private(set) var isDecrementEnabled = false private var cancellables: [AnyCancellable] = [] init() { ... } } ViewModelͷఆٛ ֎෦͔Βͷೖྗᶃ ΠϕϯτΛड͚෇͚Δ͜ͱ͚ͩͰ͖Ε͹ྑ͍ ͷͰɺࠓճͷ৔߹͸ 7PJEͰఆٛ
  79. final class ViewModel: ObservableObject { let increment: () -> Void

    let decrement: () -> Void @Published var isOn = false @Published private(set) var count: Int = 0 @Published private(set) var isIncrementEnabled = false @Published private(set) var isDecrementEnabled = false private var cancellables: [AnyCancellable] = [] init() { ... } } ViewModelͷఆٛ ֎෦͔Βͷೖྗᶄ ΠϕϯτΛड͚෇͚ΔͨΊʹ#JOEJOH#PPM͕ ඞཁʹͳΔͷͰ!1VCMJTIFEJOUFSOBMWBSͰఆٛ
  80. final class ViewModel: ObservableObject { let increment: () -> Void

    let decrement: () -> Void @Published var isOn = false @Published private(set) var count: Int = 0 @Published private(set) var isIncrementEnabled = false @Published private(set) var isDecrementEnabled = false private var cancellables: [AnyCancellable] = [] init() { ... } } ViewModelͷఆٛ ֎෦΁ͷग़ྗͱ಺෦ঢ়ଶ !1VCMJTIFEͰఆٛ͞Ε͍ͯΔQSPQFSUZ͕ߋ৽͞ΕΔͱ 0CTFSWBCMF0CKFDUͷPCKFDU8JMM$IBOHF͕ൃՐͯ͠ 7JFXͷCPEZ͕ߋ৽͞ΕΔ ͦͯ͠ɺ7JFXଆͰ͸஋ʹΞΫηεͰ͖Δ͚ͩͰྑ͍ͷͰ QSJWBUF TFU Ͱఆٛͯ͠ɺ಺෦ͰͷΈߋ৽Մೳͱ͢Δ
  81. ViewModelͷ࣮૷ final class ViewModel: ObservableObject { ... init(){ let _increment

    = PassthroughSubject<Void, Never>() let _decrement = PassthroughSubject<Void, Never>() self.increment = { _increment.send(()) } self.decrement = { _decrement.send(()) } let increment = _increment.flatMap { [weak self]_ in self.map { Just($0.count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 + 1 } let decrement = _decrement.flatMap { [weak self]_ in self.map { Just($0.count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 > 0 ? $0 - 1 : $0 } increment.merge(with: decrement).assign(to: \.count, on: self).store(in: &cancellables) $isOn.assign(to: \.isIncrementEnabled, on: self).store(in: &cancellables) $isOn.combineLatest($count).map { $0 && $1 > 0 } .assign(to: \.isDecrementEnabled, on: self) .store(in: &cancellables) } }
  82. final class ViewModel: ObservableObject { ... init(){ let _increment =

    PassthroughSubject<Void, Never>() let _decrement = PassthroughSubject<Void, Never>() self.increment = { _increment.send(()) } self.decrement = { _decrement.send(()) } let increment = _increment.flatMap { [weak self]_ in self.map { Just($0.count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 + 1 } let decrement = _decrement.flatMap { [weak self]_ in self.map { Just($0.count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 > 0 ? $0 - 1 : $0 } increment.merge(with: decrement).assign(to: \.count, on: self).store(in: &cancellables) $isOn.assign(to: \.isIncrementEnabled, on: self).store(in: &cancellables) $isOn.combineLatest($count).map { $0 && $1 > 0 } .assign(to: \.isDecrementEnabled, on: self) .store(in: &cancellables) } } ViewModelͷ࣮૷ ೖྗΛड͚औͬͯ*OJUBMJ[FS಺ͰΠϕϯτΛ ϦϨʔ͢ΔͨΊͷ1BTTUISPVHI4VCKFDU
  83. final class ViewModel: ObservableObject { ... init(){ let _increment =

    PassthroughSubject<Void, Never>() let _decrement = PassthroughSubject<Void, Never>() self.increment = { _increment.send(()) } self.decrement = { _decrement.send(()) } let increment = _increment.flatMap { [weak self]_ in self.map { Just($0.count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 + 1 } let decrement = _decrement.flatMap { [weak self]_ in self.map { Just($0.count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 > 0 ? $0 - 1 : $0 } increment.merge(with: decrement).assign(to: \.count, on: self).store(in: &cancellables) $isOn.assign(to: \.isIncrementEnabled, on: self).store(in: &cancellables) $isOn.combineLatest($count).map { $0 && $1 > 0 } .assign(to: \.isDecrementEnabled, on: self) .store(in: &cancellables) } } ViewModelͷ࣮૷ ೖྗͷΠϕϯτΛ1BTTUISPVHI4VCKFDU ʹܨ͛Δ
  84. final class ViewModel: ObservableObject { ... init(){ let _increment =

    PassthroughSubject<Void, Never>() let _decrement = PassthroughSubject<Void, Never>() self.increment = { _increment.send(()) } self.decrement = { _decrement.send(()) } let increment = _increment.flatMap { [weak self]_ in self.map { Just($0.count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 + 1 } let decrement = _decrement.flatMap { [weak self]_ in self.map { Just($0.count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 > 0 ? $0 - 1 : $0 } increment.merge(with: decrement).assign(to: \.count, on: self).store(in: &cancellables) $isOn.assign(to: \.isIncrementEnabled, on: self).store(in: &cancellables) $isOn.combineLatest($count).map { $0 && $1 > 0 } .assign(to: \.isDecrementEnabled, on: self) .store(in: &cancellables) } } ViewModelͷ࣮૷ @JODSFNFOU͔ΒͷΠϕϯτΛτϦΨʔʹ ಺෦ঢ়ଶͷ@DPVOUʹରͯ͠ ͨ͠஋Λྲྀ͢
  85. final class ViewModel: ObservableObject { ... init(){ let _increment =

    PassthroughSubject<Void, Never>() let _decrement = PassthroughSubject<Void, Never>() self.increment = { _increment.send(()) } self.decrement = { _decrement.send(()) } let increment = _increment.flatMap { [weak self]_ in self.map { Just($0.count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 + 1 } let decrement = _decrement.flatMap { [weak self]_ in self.map { Just($0.count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 > 0 ? $0 - 1 : $0 } increment.merge(with: decrement).assign(to: \.count, on: self).store(in: &cancellables) $isOn.assign(to: \.isIncrementEnabled, on: self).store(in: &cancellables) $isOn.combineLatest($count).map { $0 && $1 > 0 } .assign(to: \.isDecrementEnabled, on: self) .store(in: &cancellables) } } ViewModelͷ࣮૷ @EFDSFNFOU͔ΒͷΠϕϯτΛτϦΨʔʹ ಺෦ঢ়ଶͷ@DPVOUʹରͯͨ͠͠஋ΛΑΓ େ͖͍஋ʹͯ͠ྲྀ͢
  86. final class ViewModel: ObservableObject { ... init(){ let _increment =

    PassthroughSubject<Void, Never>() let _decrement = PassthroughSubject<Void, Never>() self.increment = { _increment.send(()) } self.decrement = { _decrement.send(()) } let increment = _increment.flatMap { [weak self]_ in self.map { Just($0.count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 + 1 } let decrement = _decrement.flatMap { [weak self]_ in self.map { Just($0.count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 > 0 ? $0 - 1 : $0 } increment.merge(with: decrement).assign(to: \.count, on: self).store(in: &cancellables) $isOn.assign(to: \.isIncrementEnabled, on: self).store(in: &cancellables) $isOn.combineLatest($count).map { $0 && $1 > 0 } .assign(to: \.isDecrementEnabled, on: self) .store(in: &cancellables) } } ViewModelͷ࣮૷ JODSFNFOUͱEFDSFNFOUͷΠϕϯτΛ ΋ͱʹ಺෦ঢ়ଶΛߋ৽
  87. final class ViewModel: ObservableObject { ... init(){ let _increment =

    PassthroughSubject<Void, Never>() let _decrement = PassthroughSubject<Void, Never>() self.increment = { _increment.send(()) } self.decrement = { _decrement.send(()) } let increment = _increment.flatMap { [weak self]_ in self.map { Just($0.count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 + 1 } let decrement = _decrement.flatMap { [weak self]_ in self.map { Just($0.count).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher() } .map { $0 > 0 ? $0 - 1 : $0 } increment.merge(with: decrement).assign(to: \.count, on: self).store(in: &cancellables) $isOn.assign(to: \.isIncrementEnabled, on: self).store(in: &cancellables) $isOn.combineLatest($count).map { $0 && $1 > 0 } .assign(to: \.isDecrementEnabled, on: self) .store(in: &cancellables) } } ViewModelͷ࣮૷ @JT0O͔ΒͷΠϕϯτΛ΋ͱʹ಺෦ঢ়ଶΛߋ৽
  88. struct CounterView: View { @ObservedObject var viewModel = ViewModel() var

    body: some View { VStack { Button(" ") { self.viewModel.increment() } .font(.system(size: 50)) .disabled(!viewModel.isIncrementEnabled) .opacity(viewModel.isIncrementEnabled ? 1 : 0.5) Text("\(viewModel.count)") .font(.system(size: 50)) Button(" ") { self.viewModel.decrement() } .font(.system(size: 50)) .disabled(!viewModel.isDecrementEnabled) .opacity(viewModel.isDecrementEnabled ? 1 : 0.5) Toggle(viewModel.toggleText, isOn: $viewModel.isOn) .frame(width: CGFloat(150), alignment: .center) } } } Viewͷ࣮૷ͷશମ૾
  89. import SwiftUI import Ricemill

  90. struct CounterView: View { @ObservedObject var viewModel = ViewModel(input: .init(),

    store: .init(), extra: .init()) var body: some View { let input = viewModel.input let output = viewModel.output return VStack { Button(" ") { input.increment.send() } .font(.system(size: 50)) .disabled(!output.isIncrementEnabled) .opacity(output.isIncrementEnabled ? 1 : 0.5) Text("\(output.count)") .font(.system(size: 50)) Button(" ") { input.decrement.send() } .font(.system(size: 50)) .disabled(!output.isDecrementEnabled) .opacity(output.isDecrementEnabled ? 1 : 0.5) Toggle(output.toggleText, isOn: input.isOn) .frame(width: 150, alignment: .center) } } } Viewͷ࣮૷
  91. struct CounterView: View { @ObservedObject var viewModel = ViewModel(input: .init(),

    store: .init(), extra: .init()) var body: some View { let input = viewModel.input let output = viewModel.output return VStack { Button(" ") { input.increment.send() } .font(.system(size: 50)) .disabled(!output.isIncrementEnabled) .opacity(output.isIncrementEnabled ? 1 : 0.5) Text("\(output.count)") .font(.system(size: 50)) Button(" ") { input.decrement.send() } .font(.system(size: 50)) .disabled(!output.isDecrementEnabled) .opacity(output.isDecrementEnabled ? 1 : 0.5) Toggle(output.toggleText, isOn: input.isOn) .frame(width: 150, alignment: .center) } } } Viewͷ࣮૷ 7JFX.PEFMͷೖग़ྗ͕໌ࣔతʹͳ͍ͬͯΔ
  92. final class ViewModel: Machine<ViewModel.Resolver> { typealias Output = Store final

    class Input: BindableInputType { let increment = PassthroughSubject<Void, Never>() let decrement = PassthroughSubject<Void, Never>() @Published var isOn = false } final class Store: StoredOutputType { @Published var count: Int = 0 @Published var isIncrementEnabled = false @Published var isDecrementEnabled = false } struct Extra: ExtraType {} enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store, extra: Extra) -> Polished<Output> { ... return Polished(cancellables: cancellables) } } } ViewModelͷఆٛ
  93. final class ViewModel: Machine<ViewModel.Resolver> { typealias Output = Store final

    class Input: BindableInputType { let increment = PassthroughSubject<Void, Never>() let decrement = PassthroughSubject<Void, Never>() @Published var isOn = false } final class Store: StoredOutputType { @Published var count: Int = 0 @Published var isIncrementEnabled = false @Published var isDecrementEnabled = false } struct Extra: ExtraType {} enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store, extra: Extra) -> Polished<Output> { ... return Polished(cancellables: cancellables) } } } ViewModelͷఆٛ
  94. final class Input: BindableInputType { let increment = PassthroughSubject<Void, Never>()

    let decrement = PassthroughSubject<Void, Never>() @Published var isOn = false } protocol BindableInputType: InputType, ObservableObject {} extension InputProxy where Input: BindableInputType { subscript<Subject>( dynamicMember keyPath: ReferenceWritableKeyPath<Input, Subject> ) -> Binding<Subject> { ObservedObject(initialValue: input).projectedValue[dynamicMember: keyPath] } } SwiftUIͰͷInputͷৼΔ෣͍
  95. final class Input: BindableInputType { let increment = PassthroughSubject<Void, Never>()

    let decrement = PassthroughSubject<Void, Never>() @Published var isOn = false } protocol BindableInputType: InputType, ObservableObject {} extension InputProxy where Input: BindableInputType { subscript<Subject>( dynamicMember keyPath: ReferenceWritableKeyPath<Input, Subject> ) -> Binding<Subject> { ObservedObject(initialValue: input).projectedValue[dynamicMember: keyPath] } } 0CTFSWBCMF0CKFDUʹ४ڌͨ͠*OQVU SwiftUIͰͷInputͷৼΔ෣͍
  96. final class Input: BindableInputType { let increment = PassthroughSubject<Void, Never>()

    let decrement = PassthroughSubject<Void, Never>() @Published var isOn = false } protocol BindableInputType: InputType, ObservableObject {} extension InputProxy where Input: BindableInputType { subscript<Subject>( dynamicMember keyPath: ReferenceWritableKeyPath<Input, Subject> ) -> Binding<Subject> { ObservedObject(initialValue: input).projectedValue[dynamicMember: keyPath] } } *OQVU͕#JOEBCMF*OQVU5ZQFͷ৔߹͸EZOBNJD.FNCFS-PPLVQ Ͱ!1VCMJTIFEͰఆٛ͞Ε͍ͯΔQSPQFSUZ͔Β#JOEJOHΛऔಘՄೳʹ͢Δ SwiftUIͰͷInputͷৼΔ෣͍
  97. final class ViewModel: Machine<ViewModel.Resolver> { typealias Output = Store final

    class Input: BindableInputType { let increment = PassthroughSubject<Void, Never>() let decrement = PassthroughSubject<Void, Never>() @Published var isOn = false } final class Store: StoredOutputType { @Published var count: Int = 0 @Published var isIncrementEnabled = false @Published var isDecrementEnabled = false } struct Extra: ExtraType {} enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store, extra: Extra) -> Polished<Output> { ... return Polished(cancellables: cancellables) } } } ViewModelͷఆٛ
  98. final class Store: StoredOutputType { @Published var count: Int =

    0 @Published var isIncrementEnabled = false @Published var isDecrementEnabled = false } protocol StoredOutputType: OutputType, StoreType {} extension Machine: ObservableObject where Resolver.Output == Resolver.Store { var objectWillChange: Resolver.Store.ObjectWillChangePublisher { return _store.objectWillChange } } SwiftUIͰͷStoreͱOutputͷৼΔ෣͍
  99. final class Store: StoredOutputType { @Published var count: Int =

    0 @Published var isIncrementEnabled = false @Published var isDecrementEnabled = false } protocol StoredOutputType: OutputType, StoreType {} extension Machine: ObservableObject where Resolver.Output == Resolver.Store { var objectWillChange: Resolver.Store.ObjectWillChangePublisher { return _store.objectWillChange } } 4UPSFͱ0VUQVUʹ४ڌͨ͠QSPUPDPM SwiftUIͰͷStoreͱOutputͷৼΔ෣͍
  100. final class Store: StoredOutputType { @Published var count: Int =

    0 @Published var isIncrementEnabled = false @Published var isDecrementEnabled = false } protocol StoredOutputType: OutputType, StoreType {} extension Machine: ObservableObject where Resolver.Output == Resolver.Store { var objectWillChange: Resolver.Store.ObjectWillChangePublisher { return _store.objectWillChange } } 4UPSFͱ0VUQVU͕ಉ͡ܕͩͬͨ৔߹ʢ4UPSFE0VUQVU5ZQFʣʹ 4UPSFͷPCKFDU8JMM$IBOHFΛ.BDIJOFͷPCKFDU8JMM$IBOHF ͔ΒΞΫηεͰ͖ΔΑ͏ʹ͠ɺ4UPSFͷมߋΛ7JFXʹ఻͑Δ SwiftUIͰͷStoreͱOutputͷৼΔ෣͍
  101. final class ViewModel: Machine<ViewModel.Resolver> { typealias Output = Store final

    class Input: BindableInputType { let increment = PassthroughSubject<Void, Never>() let decrement = PassthroughSubject<Void, Never>() @Published var isOn = false } final class Store: StoredOutputType { @Published var count: Int = 0 @Published var isIncrementEnabled = false @Published var isDecrementEnabled = false } struct Extra: ExtraType {} enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store, extra: Extra) -> Polished<Output> { ... return Polished(cancellables: cancellables) } } } ViewModelͷఆٛ
  102. enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store,

    extra: Extra) -> Polished<Output> { var cancellables: [AnyCancellable] = [] let increment = input.increment .flatMap { _ in Just(store.count) } .map { $0 + 1 } let decrement = input.decrement .flatMap { _ in Just(store.count) } .map { $0 > 0 ? $0 - 1 : $0 } increment.merge(with: decrement) .assign(to: \.count, on: store) .store(in: &cancellables) ... } } SwiftUIͰͷResolverͷৼΔ෣͍
  103. enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store,

    extra: Extra) -> Polished<Output> { var cancellables: [AnyCancellable] = [] let increment = input.increment .flatMap { _ in Just(store.count) } .map { $0 + 1 } let decrement = input.decrement .flatMap { _ in Just(store.count) } .map { $0 > 0 ? $0 - 1 : $0 } increment.merge(with: decrement) .assign(to: \.count, on: store) .store(in: &cancellables) ... } } SwiftUIͰͷResolverͷৼΔ෣͍ 1VCMJTIJOHܦ༝Ͱ֎෦͔ΒͷೖྗΛɺ಺෦޲͚ͷ ग़ྗͱͯͯ͠͠ड͚औΔ
  104. enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store,

    extra: Extra) -> Polished<Output> { ... input.$isOn .assign(to: \.isIncrementEnabled, on: store) .store(in: &cancellables) input.$isOn .combineLatest(store.$count) .map { $0 && $1 > 0 } .assign(to: \.isDecrementEnabled, on: store) .store(in: &cancellables) return Polished(cancellables: cancellables) } } SwiftUIͰͷResolverͷৼΔ෣͍
  105. enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store,

    extra: Extra) -> Polished<Output> { ... input.$isOn .assign(to: \.isIncrementEnabled, on: store) .store(in: &cancellables) input.$isOn .combineLatest(store.$count) .map { $0 && $1 > 0 } .assign(to: \.isDecrementEnabled, on: store) .store(in: &cancellables) return Polished(cancellables: cancellables) } } SwiftUIͰͷResolverͷৼΔ෣͍ 1VCMJTIJOHܦ༝Ͱ֎෦ͷ#JOEJOH͔ΒͷೖྗΛ ಺෦޲͚ͷग़ྗͱͯͯ͠͠ड͚औΔ
  106. final class Input: BindableInputType { let increment = PassthroughSubject<Void, Never>()

    let decrement = PassthroughSubject<Void, Never>() @Published var isOn = false } let input: Publishing<Input> let isOn: AnyPublisher<Bool, Never> = input.$isOn extension Publishing where Input: BindableInputType { subscript<Value>( dynamicMember keyPath: ReferenceWritableKeyPath<Input, Value> ) -> Value { input[keyPath: keyPath] } } SwiftUIͰͷPublishingͷৼΔ෣͍
  107. final class Input: BindableInputType { let increment = PassthroughSubject<Void, Never>()

    let decrement = PassthroughSubject<Void, Never>() @Published var isOn = false } let input: Publishing<Input> let isOn: AnyPublisher<Bool, Never> = input.$isOn extension Publishing where Input: BindableInputType { subscript<Value>( dynamicMember keyPath: ReferenceWritableKeyPath<Input, Value> ) -> Value { input[keyPath: keyPath] } } SwiftUIͰͷPublishingͷৼΔ෣͍ "OZ1VCMJTIFSʹUZQFFSBTFͨ͠ΠϯελϯεΛऔಘ͢Δ
  108. enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store,

    extra: Extra) -> Polished<Output> { ... input.$isOn .assign(to: \.isIncrementEnabled, on: store) .store(in: &cancellables) input.$isOn .combineLatest(store.$count) .map { $0 && $1 > 0 } .assign(to: \.isDecrementEnabled, on: store) .store(in: &cancellables) return Polished(cancellables: cancellables) } } SwiftUIͰͷResolverͷৼΔ෣͍
  109. enum Resolver: ResolverType { static func polish(input: Publishing<Input>, store: Store,

    extra: Extra) -> Polished<Output> { ... input.$isOn .assign(to: \.isIncrementEnabled, on: store) .store(in: &cancellables) input.$isOn .combineLatest(store.$count) .map { $0 && $1 > 0 } .assign(to: \.isDecrementEnabled, on: store) .store(in: &cancellables) return Polished(cancellables: cancellables) } } SwiftUIͰͷResolverͷৼΔ෣͍ 4UPSFͱ0VUQVU͕ಉҰͰ͋ΔͨΊɺ1PMJTIFE ͷJOJUJBMJ[FSͷҾ਺ʹ͸DBODFMMBCMFTͷΈ
  110. struct Polished<Output> { let output: Output? let cancellables: [AnyCancellable] }

    extension Polished where Output: StoredOutputType { init(cancellables: [AnyCancellable]) { self.output = nil self.cancellables = cancellables } } extension Polished where Output: OutputType { init(output: Output, cancellables: [AnyCancellable]) { self.output = output self.cancellables = cancellables } } SwiftUIͰͷPublishedͷৼΔ෣͍
  111. struct Polished<Output> { let output: Output? let cancellables: [AnyCancellable] }

    extension Polished where Output: StoredOutputType { init(cancellables: [AnyCancellable]) { self.output = nil self.cancellables = cancellables } } extension Polished where Output: OutputType { init(output: Output, cancellables: [AnyCancellable]) { self.output = output self.cancellables = cancellables } } SwiftUIͰͷPublishedͷৼΔ෣͍
  112. struct CounterView: View { @ObservedObject var viewModel = ViewModel(input: .init(),

    store: .init(), extra: .init()) var body: some View { let input = viewModel.input let output = viewModel.output return VStack { Button(" ") { input.increment.send() } .font(.system(size: 50)) .disabled(!output.isIncrementEnabled) .opacity(output.isIncrementEnabled ? 1 : 0.5) Text("\(output.count)") .font(.system(size: 50)) Button(" ") { input.decrement.send() } .font(.system(size: 50)) .disabled(!output.isDecrementEnabled) .opacity(output.isDecrementEnabled ? 1 : 0.5) Toggle(output.toggleText, isOn: input.isOn) .frame(width: 150, alignment: .center) } } } Viewͷ࣮૷ͷશମ૾
  113. Ricemill IUUQXXXXBOQVHDPNJMMVTUJMMVTUQOH IUUQTXXXJSBTVUPZBDPNCMPHQPTU@IUNM IUUQTXXXBDJMMVTUDPNNBJOQSPpMFQIQ JE*)TI$:N7BNQBSFB ը૾ͷग़యݩɿ IUUQTJMMVTUSBJODPN Q ⤵ ⤵

    ⤵ ⤵
  114. ͝੩ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠