Slide 1

Slide 1 text

MVP←MVC ↳MVVM→Flux ͷ࣮૷ͷҧ͍Λൺֱͯ͠ΈΔ iOSDC 2017: September 17th Taiki Suzuki / @marty_suzuki

Slide 2

Slide 2 text

ΞδΣϯμ - ࣗݾ঺հ - MVC - MVC→MVP - MVP→MVVM - MVVM→Flux

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

Github Sample App

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

MVC

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

final class FavoriteViewController: UIViewController, UITableViewDelegate { @IBOutlet weak var tableView: UITableView! ... override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self ... } ... private func showRepository(with repository: Repository) { let vc = RepositoryViewController(repository: repository, favoriteModel: favoriteModel) navigationController?.pushViewController(vc, animated: true) } ... func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let repository = favoriteModel.favorites[indexPath.row] showRepository(with: repository) } }

Slide 10

Slide 10 text

final class RepositoryViewController: SFSafariViewController { ... private let repository: Repository private let favoriteModel: FavoriteModel init(repository: Repository, favoriteModel: FavoriteModel) { self.repository = repository self.favoriteModel = favoriteModel super.init(url: repository.url, entersReaderIfAvailable: true) } ... }

Slide 11

Slide 11 text

final class FavoriteModel { private(set) var favorites: [Repository] = [] { didSet { delegate?.favoriteDidChange?() } } weak var delegate: FavoriteModelDelegate? func addFavorite(_ repository: Repository) { favorites.append(repository) } func removeFavorite(_ repository: Repository) { favorites.remove(at: index) } }

Slide 12

Slide 12 text

@objc protocol FavoriteModelDelegate: class { @objc optional func favoriteDidChange() } final class FavoriteViewController: UIViewController, FavoriteModelDelegate { @IBOutlet weak var tableView: UITableView! let favoriteModel = FavoriteModel() ... override func viewDidLoad() { super.viewDidLoad() ... favoriteModel.delegate = self ... } ... func favoriteDidChange() { tableView.reloadData() } }

Slide 13

Slide 13 text

final class RepositoryViewController: SFSafariViewController { private let favoriteModel: FavoriteModel private let repository: Repository private(set) lazy var favoriteButtonItem: UIBarButtonItem ... @objc private func favoriteButtonTap(_ sender: UIBarButtonItem) { if favoriteModel.favorites.index(where: { $0.url == repository.url }) == nil { favoriteModel.addFavorite(repository) ... } else { favoriteModel.removeFavorite(repository) ... } } }

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

protocol FavoriteView: class { ... func showRepository(with repository: Repository) } final class FavoriteViewController: UIViewController, FavoriteView { @IBOutlet weak var tableView: UITableView! private(set) lazy var presenter: FavoritePresenter = FavoriteViewPresenter(view: self) ... func showRepository(with repository: Repository) { let vc = RepositoryViewController(repository: repository, favoritePresenter: presenter) navigationController?.pushViewController(vc, animated: true) } }

Slide 18

Slide 18 text

final class RepositoryViewController: SFSafariViewController { ... private let presenter: RepositoryPresenter init(repository: Repository, favoritePresenter: FavoritePresenter) { self.presenter = .init(repository: repository, favoritePresenter: favoritePresenter) super.init(url: repository.url, entersReaderIfAvailable: true) } ... }

Slide 19

Slide 19 text

protocol FavoritePresenter: class { ... func addFavorite(_ repository: Repository) func removeFavorite(_ repository: Repository) func contains(_ repository: Repository) -> Bool }

Slide 20

Slide 20 text

final class FavoriteViewPresenter: FavoritePresenter { private weak var view: FavoriteView? private var favorites: [Repository] = [] { didSet { view?.reloadData() } } ... func addFavorite(_ repository: Repository) { if contains(repository) { return } favorites.append(repository) } func removeFavorite(_ repository: Repository) { guard let index = index(of: repository) else { return } favorites.remove(at: index) } private func index(of repository: Repository) -> Int? { return favorites.lazy.index { $0.url == repository.url } } func contains(_ repository: Repository) -> Bool { return index(of: repository) != nil } }

Slide 21

Slide 21 text

protocol FavoriteView: class { func reloadData() ... } final class FavoriteViewController: UIViewController, FavoriteView { @IBOutlet weak var tableView: UITableView! private(set) lazy var presenter: FavoritePresenter = .init(view: self) ... func reloadData() { tableView?.reloadData() } }

Slide 22

Slide 22 text

final class RepositoryViewController: SFSafariViewController { private(set) lazy var favoriteButtonItem: UIBarButtonItem ... private let presenter: RepositoryPresenter init(repository: Repository, favoritePresenter: FavoritePresenter, entersReaderIfAvailable: Bool = true) { self.presenter = .init(repository: repository, favoritePresenter: favoritePresenter) super.init(url: repository.url, entersReaderIfAvailable: entersReaderIfAvailable) self.presenter.view = self } ... @objc private func favoriteButtonTap(_ sender: UIBarButtonItem) { presenter.favoriteButtonTap() } }

Slide 23

Slide 23 text

protocol RepositoryPresenter: class { init(repository: Repository, favoritePresenter: FavoritePresenter) weak var view: RepositoryView? { get set } ... func favoriteButtonTap() }

Slide 24

Slide 24 text

final class RepositoryViewPresenter: RepositoryPresenter { weak var view: RepositoryView? private let favoritePresenter: FavoritePresenter private let repository: Repository ... init(repository: Repository, favoritePresenter: FavoritePresenter) { self.repository = repository self.favoritePresenter = favoritePresenter } ... func favoriteButtonTap() { if favoritePresenter.contains(repository) { favoritePresenter.removeFavorite(repository) ... } else { favoritePresenter.addFavorite(repository) ... } } }

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

- ViewͱPresenterͰ੹຿Λ੾Γ෼͚ Δ͜ͱ͕Ͱ͖Δ! - ViewΛMockԽ͢Δ͜ͱͰPresenter ͷςετ͕༰қʹͰ͖Δ! - ந৅Խ͍ͯ͠Δ͕ɺPresenterͱ View͸ޓ͍ʹґଘ͍ͯ͠Δ!

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

Data binding

Slide 30

Slide 30 text

final class FavoriteViewModel { let relaodData: Observable ... private let _favorites = Variable<[Repository]>([]) private let disposeBag = DisposeBag() ... init(favoritesObservable: Observable<[Repository]>, ...) { ... self.relaodData = _favorites.asObservable().map { _ in } favoritesObservable.bind(to: _favorites).disposed(by: disposeBag) } ... }

Slide 31

Slide 31 text

final class FavoriteViewController: UIViewController { ... var favoritesInput: AnyObserver<[Repository]> { return favorites.asObserver() } var favoritesOutput: Observable<[Repository]> { return viewModel.favorites } private let favorites = PublishSubject<[Repository]>() private let disposeBag = DisposeBag() private private(set) lazy var viewModel: FavoriteViewModel = { .init(favoritesObservable: self.favorites, ...) }() ... private var showRepository: AnyObserver { return UIBindingObserver(UIElement: self) { me, repository in let vc = RepositoryViewController(repository: repository, favoritesOutput: me.favoritesOutput, favoritesInput: me.favoritesInput) me.navigationController?.pushViewController(vc, animated: true) }.asObserver() } ... }

Slide 32

Slide 32 text

final class RepositoryViewController: SFSafariViewController { private let favoriteButtonItem: UIBarButtonItem private let viewModel: RepositoryViewModel ... init(repository: Repository, favoritesOutput: Observable<[Repository]>, favoritesInput: AnyObserver<[Repository]>) { let favoriteButtonItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil) self.favoriteButtonItem = favoriteButtonItem self.viewModel = RepositoryViewModel(repository: repository, favoritesOutput: favoritesOutput, favoritesInput: favoritesInput, favoriteButtonTap: favoriteButtonItem.rx.tap) super.init(url: repository.url, entersReaderIfAvailable: true) } ... }

Slide 33

Slide 33 text

final class RepositoryViewModel { private let disposeBag = DisposeBag() ... init(repository: Repository, favoritesOutput: Observable<[Repository]>, favoritesInput: AnyObserver<[Repository]>, favoriteButtonTap: ControlEvent) { let favoritesAndIndex: Observable<([Favorites], Int?)> = favoritesOutput .map { [repository] favorites in (favorites, favorites.index(where: { $0.url == repository.url })) } ... favoriteButtonTap .withLatestFrom(favoritesAndIndex) .map { [repository] favorites, index in var favorites = favorites if let index = index { favorites.remove(at: index) return favorites } favorites.append(repository) return favorites } .subscribe(onNext: { favoritesInput.onNext($0) }).disposed(by: disposeBag) } }

Slide 34

Slide 34 text

final class FavoriteViewController: UIViewController { @IBOutlet weak var tableView: UITableView! ... private let disposeBag = DisposeBag() private private(set) lazy var viewModel: FavoriteViewModel ... override func viewDidLoad() { super.viewDidLoad() ... viewModel.relaodData.bind(to: reloadData).disposed(by: disposeBag) } ... private var reloadData: AnyObserver { return UIBindingObserver(UIElement: self) { me, _ in me.tableView.reloadData() }.asObserver() } }

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

- ObservableΛ֎෦͔Β஫ೖ͢Δ͜ͱ ʹΑͬͯλοϓͷςετͳͲ΋Մೳ! - ViewController͸ViewModelʹґଘ ͍ͯ͠Δ͕ɺViewModel͸͠ͳ͍! - ඪ४ϑϨʔϜϫʔΫ͚ͩͰ͸ɺ࣮૷ ͕͠ʹ͍͘!

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

ActionɾDispatcherɾStore ͷఆٛ

Slide 40

Slide 40 text

final class RepositoryAction: Actionable { typealias DispatchValueType = Dispatcher.Repository ... func selectRepository(_ repository: Repository) { invoke(.selectedRepository(repository)) } ... } extension Dispatcher { enum Repository: DispatchValue { case selectedRepository(GithubKit.Repository?) ... } }

Slide 41

Slide 41 text

final class RepositoryStore: Storable { typealias DispatchValueType = Dispatcher.Repository ... let selectedRepository: Observable var selectedRepositoryValue: Repository? { return _selectedRepository.value } private let _selectedRepository = Variable(nil) ... init(dispatcher: Dispatcher) { self.selectedRepository = _selectedRepository.asObservable() ... register { [weak self] in switch $0 { case .selectedRepository(let value): self?._selectedRepository.value = value ... } } } }

Slide 42

Slide 42 text

final class FavoriteViewController: UIViewController { ... private let store: RepositoryStore = .instantiate() private let action = RepositoryAction() ... private var showRepository: AnyObserver { return UIBindingObserver(UIElement: self) { me, _ in guard let vc = RepositoryViewController() else { return } me.navigationController?.pushViewController(vc, animated: true) }.asObserver() } ... }

Slide 43

Slide 43 text

final class RepositoryViewController: SFSafariViewController { private let action: RepositoryAction private let store: RepositoryStore ... init?() { self.store = .instantiate() self.action = .init() guard let repository = store.selectedRepositoryValue else { return nil } super.init(url: repository.url, entersReaderIfAvailable: true) } override func viewDidLoad() { super.viewDidLoad() let repository = store.selectedRepository.filter { $0 != nil }.map { $0! } ... } ... }

Slide 44

Slide 44 text

final class RepositoryAction: Actionable { typealias DispatchValueType = Dispatcher.Repository ... func clearSelectedRepository() { invoke(.selectedRepository(nil)) } func addFavorite(_ repository: Repository) { invoke(.addFavorite(repository)) } func removeFavorite(_ repository: Repository) { invoke(.removeFavorite(repository)) } } extension Dispatcher { enum Repository: DispatchValue { ... case addFavorite(GithubKit.Repository) case removeFavorite(GithubKit.Repository) case removeAllFavorites } }

Slide 45

Slide 45 text

final class RepositoryStore: Storable { typealias DispatchValueType = Dispatcher.Repository ... let favorites: Observable<[Repository]> var favoritesValue: [Repository] { return _favorites.value } private let _favorites = Variable<[Repository]>([]) ... init(dispatcher: Dispatcher) { self.favorites = _favorites.asObservable() ... register { [weak self] in switch $0 { ... case .addFavorite(let value): if self?._favorites.value.index(where: { $0.url == value.url }) == nil { self?._favorites.value.append(value) } case .removeFavorite(let value): if let index = self?._favorites.value.index(where: { $0.url == value.url }) { self?._favorites.value.remove(at: index) } case .removeAllFavorites: self?._favorites.value.removeAll() } } } }

Slide 46

Slide 46 text

final class RepositoryViewController: SFSafariViewController { private let favoriteButtonItem = .init(title: nil, style: .plain, target: nil, action: nil) private let disposeBag = DisposeBag() private let action: RepositoryAction private let store: RepositoryStore ... override func viewDidLoad() { super.viewDidLoad() let repository = store.selectedRepository.filter { $0 != nil }.map { $0! } let containsRepository = Observable.combineLatest(repository, store.favorites) { repo, favs in (favs.contains { $0.url == repo.url }, repo) } favoriteButtonItem.rx.tap .withLatestFrom(containsRepository) .subscribe(onNext: { [weak self] contains, repository in if contains { self?.action.removeFavorite(repository) } else { self?.action.addFavorite(repository) } }) .disposed(by: disposeBag) } ... }

Slide 47

Slide 47 text

final class FavoriteViewController: UIViewController { @IBOutlet weak var tableView: UITableView! private let dataSource = FavoriteViewDataSource() private let disposeBag = DisposeBag() private let store: RepositoryStore = .instantiate() private let action = RepositoryAction() override func viewDidLoad() { super.viewDidLoad() ... store.favorites.map { _ in } .bind(to: reloadData) .disposed(by: disposeBag) } ... private var reloadData: AnyObserver { return UIBindingObserver(UIElement: self) { me, _ in me.tableView.reloadData() }.asObserver() } }

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

- σʔλϑϩʔ͕୯Ұํ޲! - ભҠ࣌ʹΦϒδΣΫτͷࢀরΛ ViewControllerʹ౉͢ඞཁ͕ͳ͍! - Dispatcher͕γϯάϧτϯʢ࣮૷ʹ Αͬͯ͸Store΋ʣ!

Slide 50

Slide 50 text

https:/ /github.com/marty-suzuki/ iOSDesignPatternSamples

Slide 51

Slide 51 text

No content

Slide 52

Slide 52 text

https:/ /speakerdeck.com/martysuzuki/flux-and- mvvm-brooklyn-swift-developers

Slide 53

Slide 53 text

Flux + MVVM Sample with FluxCapacitor https:/ /github.com/marty-suzuki/FluxCapacitor/ tree/master/Examples/Flux%2BMVVM

Slide 54

Slide 54 text

͝੩ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠! Taiki Suzuki Twitter: @marty_suzuki Github: marty-suzuki