Slide 1

Slide 1 text

// // CA.swift #10 // // Talked by @marty_suzuki on 2019/09/26 // CombineΛར༻ͨ͠ SwiftUIɾUIKitͷͲͪΒʹ΋ରԠ͢Δ UnidirectionalͳઃܭΛ࣮ݱ͢Δʹ͸

Slide 2

Slide 2 text

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.

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

·ͣ͸͡Ίʹ

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

import UIKit

Slide 9

Slide 9 text

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ͷఆٛ

Slide 10

Slide 10 text

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ͷఆٛ

Slide 11

Slide 11 text

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͔Βͷೖྗ

Slide 12

Slide 12 text

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͔Βͷೖྗ

Slide 13

Slide 13 text

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͔Βͷೖྗ

Slide 14

Slide 14 text

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ʹ఻͑Δ

Slide 15

Slide 15 text

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΁ͷ൓ө

Slide 16

Slide 16 text

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΁ͷ൓ө

Slide 17

Slide 17 text

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΁ͷ൓ө

Slide 18

Slide 18 text

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΁ͷ൓ө

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

final class ViewModel { let increment: Subscribers.Sink let decrement: Subscribers.Sink let isOn: Subscribers.Sink var count: AnyPublisher { $_count.map { Optional.some(String($0)) } .eraseToAnyPublisher() } var isIncrementEnabled: AnyPublisher { $_isToggleEnabled.eraseToAnyPublisher() } var isDecrementEnabled: AnyPublisher { $_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Ͱఆٛ

Slide 21

Slide 21 text

final class ViewModel { let increment: Subscribers.Sink let decrement: Subscribers.Sink let isOn: Subscribers.Sink var count: AnyPublisher { $_count.map { Optional.some(String($0)) } .eraseToAnyPublisher() } var isIncrementEnabled: AnyPublisher { $_isToggleEnabled.eraseToAnyPublisher() } var isDecrementEnabled: AnyPublisher { $_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Ͱఆٛ

Slide 22

Slide 22 text

final class ViewModel { let increment: Subscribers.Sink let decrement: Subscribers.Sink let isOn: Subscribers.Sink var count: AnyPublisher { $_count.map { Optional.some(String($0)) } .eraseToAnyPublisher() } var isIncrementEnabled: AnyPublisher { $_isToggleEnabled.eraseToAnyPublisher() } var isDecrementEnabled: AnyPublisher { $_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Ͱఆٛ

Slide 23

Slide 23 text

final class ViewModel { ... init() { let _increment = PassthroughSubject() let _decrement = PassthroughSubject() let _isOn = PassthroughSubject() 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ͷ࣮૷

Slide 24

Slide 24 text

final class ViewModel { ... init() { let _increment = PassthroughSubject() let _decrement = PassthroughSubject() let _isOn = PassthroughSubject() 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

Slide 25

Slide 25 text

final class ViewModel { ... init() { let _increment = PassthroughSubject() let _decrement = PassthroughSubject() let _isOn = PassthroughSubject() 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 ʹܨ͛Δ

Slide 26

Slide 26 text

final class ViewModel { ... init() { let _increment = PassthroughSubject() let _decrement = PassthroughSubject() let _isOn = PassthroughSubject() 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͔ΒͷΠϕϯτΛ΋ͱʹ಺෦ঢ়ଶΛߋ৽

Slide 27

Slide 27 text

final class ViewModel { ... init() { let _increment = PassthroughSubject() let _decrement = PassthroughSubject() let _isOn = PassthroughSubject() 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ʹରͯͨ͠͠஋Λྲྀ͢

Slide 28

Slide 28 text

final class ViewModel { ... init() { let _increment = PassthroughSubject() let _decrement = PassthroughSubject() let _isOn = PassthroughSubject() 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ʹରͯͨ͠͠஋ΛΑΓ େ͖͍஋ʹͯ͠ྲྀ͢

Slide 29

Slide 29 text

final class ViewModel { ... init() { let _increment = PassthroughSubject() let _decrement = PassthroughSubject() let _isOn = PassthroughSubject() 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ͷΠϕϯτΛ ΋ͱʹ಺෦ঢ়ଶΛߋ৽

Slide 30

Slide 30 text

final class ViewModel { let increment: Subscribers.Sink let decrement: Subscribers.Sink let isOn: Subscribers.Sink var count: AnyPublisher { $_count.map { Optional.some(String($0)) } .eraseToAnyPublisher() } var isIncrementEnabled: AnyPublisher { $_isToggleEnabled.eraseToAnyPublisher() } var isDecrementEnabled: AnyPublisher { $_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ʹऩ·Βͳ͔࣮ͬͨ૷

Slide 31

Slide 31 text

final class ViewModel { let increment: Subscribers.Sink let decrement: Subscribers.Sink let isOn: Subscribers.Sink var count: AnyPublisher { $_count.map { Optional.some(String($0)) } .eraseToAnyPublisher() } var isIncrementEnabled: AnyPublisher { $_isToggleEnabled.eraseToAnyPublisher() } var isDecrementEnabled: AnyPublisher { $_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 ʹม׵ͯ͠ग़ྗ

Slide 32

Slide 32 text

final class ViewModel { let increment: Subscribers.Sink let decrement: Subscribers.Sink let isOn: Subscribers.Sink var count: AnyPublisher { $_count.map { Optional.some(String($0)) } .eraseToAnyPublisher() } var isIncrementEnabled: AnyPublisher { $_isToggleEnabled.eraseToAnyPublisher() } var isDecrementEnabled: AnyPublisher { $_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͕มߋ͞ΕͨΒ ͦͷΠϕϯτΛ֎෦ʹग़ྗ

Slide 33

Slide 33 text

final class ViewModel { let increment: Subscribers.Sink let decrement: Subscribers.Sink let isOn: Subscribers.Sink var count: AnyPublisher { $_count.map { Optional.some(String($0)) } .eraseToAnyPublisher() } var isIncrementEnabled: AnyPublisher { $_isToggleEnabled.eraseToAnyPublisher() } var isDecrementEnabled: AnyPublisher { $_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͕ΑΓେ͖͍ͱ͍͏஋ʹม׵ͯ͠ग़ྗ

Slide 34

Slide 34 text

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ͷ࣮૷ͷશମ૾

Slide 35

Slide 35 text

import UIKit import Ricemill

Slide 36

Slide 36 text

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ͷఆٛ

Slide 37

Slide 37 text

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͔Βͷೖྗ

Slide 38

Slide 38 text

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ͷೖྗ͕໌ࣔతʹͳ͍ͬͯΔ

Slide 39

Slide 39 text

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΁ͷ൓ө

Slide 40

Slide 40 text

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͔Βͷग़ྗ͕໌ࣔతʹͳ͍ͬͯΔ

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

enum Resolver: ResolverType { static func polish(input: Publishing, store: Store, extra: Extra) -> Polished { 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ͷ࣮૷

Slide 59

Slide 59 text

enum Resolver: ResolverType { static func polish(input: Publishing, store: Store, extra: Extra) -> Polished { 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ܦ༝Ͱ֎෦͔ΒͷೖྗΛɺ಺෦޲͚ͷ ग़ྗͱͯͯ͠͠ड͚औΔ

Slide 60

Slide 60 text

struct Input: InputType { let increment = PassthroughSubject() let decrement = PassthroughSubject() let isOn = PassthroughSubject() } let input: Publishing let increment: AnyPublisher = input.increment @dynamicMemberLookup final class Publishing { private let input: Input init(_ input: Input) { self.input = input } subscript(dynamicMember keyPath: KeyPath) -> AnyPublisher { input[keyPath: keyPath].eraseToAnyPublisher() } } RicemillͷResolver - Publishing

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

enum Resolver: ResolverType { static func polish(input: Publishing, store: Store, extra: Extra) -> Polished { ... 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ͷ࣮૷

Slide 64

Slide 64 text

enum Resolver: ResolverType { static func polish(input: Publishing, store: Store, extra: Extra) -> Polished { ... 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Λੜ੒

Slide 65

Slide 65 text

enum Resolver: ResolverType { static func polish(input: Publishing, store: Store, extra: Extra) -> Polished { ... 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ͷ࣮૷

Slide 66

Slide 66 text

open class Machine { public let input: InputProxy public let output: OutputProxy 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

Slide 67

Slide 67 text

open class Machine { public let input: InputProxy public let output: OutputProxy 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Λར༻͢Δ

Slide 68

Slide 68 text

open class Machine { public let input: InputProxy public let output: OutputProxy 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Ͱ֘౰ͷΠϯελϯεΛ ϥοϓͯ͠ɺॳظԽΛ׬ྃ͢Δ

Slide 69

Slide 69 text

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ͷ࣮૷ͷશମ૾

Slide 70

Slide 70 text

import SwiftUI

Slide 71

Slide 71 text

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ͷ࣮૷

Slide 72

Slide 72 text

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 ΛݺͿ

Slide 73

Slide 73 text

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Λߋ৽

Slide 74

Slide 74 text

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 ΛݺͿ

Slide 75

Slide 75 text

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΁ྲྀ͞ΕΔ

Slide 76

Slide 76 text

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ͷఆٛ

Slide 77

Slide 77 text

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ଆͰఆٛ Ͱ͖ΔΑ͏ʹ͢Δ

Slide 78

Slide 78 text

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Ͱఆٛ

Slide 79

Slide 79 text

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Ͱఆٛ

Slide 80

Slide 80 text

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 Ͱఆٛͯ͠ɺ಺෦ͰͷΈߋ৽Մೳͱ͢Δ

Slide 81

Slide 81 text

ViewModelͷ࣮૷ final class ViewModel: ObservableObject { ... init(){ let _increment = PassthroughSubject() let _decrement = PassthroughSubject() 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) } }

Slide 82

Slide 82 text

final class ViewModel: ObservableObject { ... init(){ let _increment = PassthroughSubject() let _decrement = PassthroughSubject() 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

Slide 83

Slide 83 text

final class ViewModel: ObservableObject { ... init(){ let _increment = PassthroughSubject() let _decrement = PassthroughSubject() 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 ʹܨ͛Δ

Slide 84

Slide 84 text

final class ViewModel: ObservableObject { ... init(){ let _increment = PassthroughSubject() let _decrement = PassthroughSubject() 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ʹରͯͨ͠͠஋Λྲྀ͢

Slide 85

Slide 85 text

final class ViewModel: ObservableObject { ... init(){ let _increment = PassthroughSubject() let _decrement = PassthroughSubject() 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ʹରͯͨ͠͠஋ΛΑΓ େ͖͍஋ʹͯ͠ྲྀ͢

Slide 86

Slide 86 text

final class ViewModel: ObservableObject { ... init(){ let _increment = PassthroughSubject() let _decrement = PassthroughSubject() 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ͷΠϕϯτΛ ΋ͱʹ಺෦ঢ়ଶΛߋ৽

Slide 87

Slide 87 text

final class ViewModel: ObservableObject { ... init(){ let _increment = PassthroughSubject() let _decrement = PassthroughSubject() 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͔ΒͷΠϕϯτΛ΋ͱʹ಺෦ঢ়ଶΛߋ৽

Slide 88

Slide 88 text

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ͷ࣮૷ͷશମ૾

Slide 89

Slide 89 text

import SwiftUI import Ricemill

Slide 90

Slide 90 text

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ͷ࣮૷

Slide 91

Slide 91 text

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ͷೖग़ྗ͕໌ࣔతʹͳ͍ͬͯΔ

Slide 92

Slide 92 text

final class ViewModel: Machine { typealias Output = Store final class Input: BindableInputType { let increment = PassthroughSubject() let decrement = PassthroughSubject() @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, store: Store, extra: Extra) -> Polished { ... return Polished(cancellables: cancellables) } } } ViewModelͷఆٛ

Slide 93

Slide 93 text

final class ViewModel: Machine { typealias Output = Store final class Input: BindableInputType { let increment = PassthroughSubject() let decrement = PassthroughSubject() @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, store: Store, extra: Extra) -> Polished { ... return Polished(cancellables: cancellables) } } } ViewModelͷఆٛ

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

final class ViewModel: Machine { typealias Output = Store final class Input: BindableInputType { let increment = PassthroughSubject() let decrement = PassthroughSubject() @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, store: Store, extra: Extra) -> Polished { ... return Polished(cancellables: cancellables) } } } ViewModelͷఆٛ

Slide 98

Slide 98 text

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ͷৼΔ෣͍

Slide 99

Slide 99 text

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ͷৼΔ෣͍

Slide 100

Slide 100 text

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ͷৼΔ෣͍

Slide 101

Slide 101 text

final class ViewModel: Machine { typealias Output = Store final class Input: BindableInputType { let increment = PassthroughSubject() let decrement = PassthroughSubject() @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, store: Store, extra: Extra) -> Polished { ... return Polished(cancellables: cancellables) } } } ViewModelͷఆٛ

Slide 102

Slide 102 text

enum Resolver: ResolverType { static func polish(input: Publishing, store: Store, extra: Extra) -> Polished { 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ͷৼΔ෣͍

Slide 103

Slide 103 text

enum Resolver: ResolverType { static func polish(input: Publishing, store: Store, extra: Extra) -> Polished { 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ܦ༝Ͱ֎෦͔ΒͷೖྗΛɺ಺෦޲͚ͷ ग़ྗͱͯͯ͠͠ड͚औΔ

Slide 104

Slide 104 text

enum Resolver: ResolverType { static func polish(input: Publishing, store: Store, extra: Extra) -> Polished { ... 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ͷৼΔ෣͍

Slide 105

Slide 105 text

enum Resolver: ResolverType { static func polish(input: Publishing, store: Store, extra: Extra) -> Polished { ... 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͔ΒͷೖྗΛ ಺෦޲͚ͷग़ྗͱͯͯ͠͠ड͚औΔ

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

final class Input: BindableInputType { let increment = PassthroughSubject() let decrement = PassthroughSubject() @Published var isOn = false } let input: Publishing let isOn: AnyPublisher = input.$isOn extension Publishing where Input: BindableInputType { subscript( dynamicMember keyPath: ReferenceWritableKeyPath ) -> Value { input[keyPath: keyPath] } } SwiftUIͰͷPublishingͷৼΔ෣͍ "OZ1VCMJTIFSʹUZQFFSBTFͨ͠ΠϯελϯεΛऔಘ͢Δ

Slide 108

Slide 108 text

enum Resolver: ResolverType { static func polish(input: Publishing, store: Store, extra: Extra) -> Polished { ... 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ͷৼΔ෣͍

Slide 109

Slide 109 text

enum Resolver: ResolverType { static func polish(input: Publishing, store: Store, extra: Extra) -> Polished { ... 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ͷΈ

Slide 110

Slide 110 text

struct Polished { 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ͷৼΔ෣͍

Slide 111

Slide 111 text

struct Polished { 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ͷৼΔ෣͍

Slide 112

Slide 112 text

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ͷ࣮૷ͷશମ૾

Slide 113

Slide 113 text

Ricemill IUUQXXXXBOQVHDPNJMMVTUJMMVTUQOH IUUQTXXXJSBTVUPZBDPNCMPHQPTU@IUNM IUUQTXXXBDJMMVTUDPNNBJOQSPpMFQIQ JE*)TI$:N7BNQBSFB ը૾ͷग़యݩɿ IUUQTJMMVTUSBJODPN Q ⤵ ⤵ ⤵ ⤵

Slide 114

Slide 114 text

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