From 4fde1d9670e25721cca231317b6213bb859e8298 Mon Sep 17 00:00:00 2001 From: Alex Manzella Date: Wed, 11 Sep 2019 18:12:23 +0200 Subject: [PATCH] =?UTF-8?q?Testing=20MVV=C3=87=20concepts=20with=20SwiftUI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExampleApp.xcodeproj/project.pbxproj | 12 ++--- ExampleApp/ExampleApp/AppDelegate.swift | 0 .../ExampleApp/ToDoListCoordinator.swift | 10 ++-- .../ExampleApp/ToDoListInteractor.swift | 13 +++-- ExampleApp/ExampleApp/ToDoListView.swift | 25 +++++++++ .../ExampleApp/ToDoListViewController.swift | 53 ------------------- ExampleApp/ExampleApp/ToDoListViewModel.swift | 4 +- "MVVM\303\207.swift" | 39 +++++++++----- 8 files changed, 68 insertions(+), 88 deletions(-) mode change 100644 => 100755 ExampleApp/ExampleApp.xcodeproj/project.pbxproj mode change 100644 => 100755 ExampleApp/ExampleApp/AppDelegate.swift mode change 100644 => 100755 ExampleApp/ExampleApp/ToDoListCoordinator.swift mode change 100644 => 100755 ExampleApp/ExampleApp/ToDoListInteractor.swift create mode 100755 ExampleApp/ExampleApp/ToDoListView.swift delete mode 100644 ExampleApp/ExampleApp/ToDoListViewController.swift mode change 100644 => 100755 ExampleApp/ExampleApp/ToDoListViewModel.swift mode change 100644 => 100755 "MVVM\303\207.swift" diff --git a/ExampleApp/ExampleApp.xcodeproj/project.pbxproj b/ExampleApp/ExampleApp.xcodeproj/project.pbxproj old mode 100644 new mode 100755 index 458acd6..2289c8e --- a/ExampleApp/ExampleApp.xcodeproj/project.pbxproj +++ b/ExampleApp/ExampleApp.xcodeproj/project.pbxproj @@ -14,7 +14,7 @@ F8DF99162271C00700B68009 /* ToDo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF99152271C00700B68009 /* ToDo.swift */; }; F8DF99182271C04C00B68009 /* MVVMÇ.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF99172271C04C00B68009 /* MVVMÇ.swift */; }; F8DF991D2271C09A00B68009 /* ToDoListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF99192271C09A00B68009 /* ToDoListCoordinator.swift */; }; - F8DF991E2271C09A00B68009 /* ToDoListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF991A2271C09A00B68009 /* ToDoListViewController.swift */; }; + F8DF991E2271C09A00B68009 /* ToDoListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF991A2271C09A00B68009 /* ToDoListView.swift */; }; F8DF991F2271C09A00B68009 /* ToDoListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF991B2271C09A00B68009 /* ToDoListInteractor.swift */; }; F8DF99202271C09A00B68009 /* ToDoListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF991C2271C09A00B68009 /* ToDoListViewModel.swift */; }; F8DF99222271C0A900B68009 /* LoadableModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF99212271C0A900B68009 /* LoadableModel.swift */; }; @@ -42,7 +42,7 @@ F8DF99152271C00700B68009 /* ToDo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDo.swift; sourceTree = ""; }; F8DF99172271C04C00B68009 /* MVVMÇ.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "MVVMÇ.swift"; path = "../../MVVMÇ.swift"; sourceTree = ""; }; F8DF99192271C09A00B68009 /* ToDoListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListCoordinator.swift; sourceTree = ""; }; - F8DF991A2271C09A00B68009 /* ToDoListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListViewController.swift; sourceTree = ""; }; + F8DF991A2271C09A00B68009 /* ToDoListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListView.swift; sourceTree = ""; }; F8DF991B2271C09A00B68009 /* ToDoListInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListInteractor.swift; sourceTree = ""; }; F8DF991C2271C09A00B68009 /* ToDoListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListViewModel.swift; sourceTree = ""; }; F8DF99212271C0A900B68009 /* LoadableModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableModel.swift; sourceTree = ""; }; @@ -92,7 +92,7 @@ F8DF99212271C0A900B68009 /* LoadableModel.swift */, F8DF99152271C00700B68009 /* ToDo.swift */, F8DF99192271C09A00B68009 /* ToDoListCoordinator.swift */, - F8DF991A2271C09A00B68009 /* ToDoListViewController.swift */, + F8DF991A2271C09A00B68009 /* ToDoListView.swift */, F8DF991B2271C09A00B68009 /* ToDoListInteractor.swift */, F8DF991C2271C09A00B68009 /* ToDoListViewModel.swift */, F8DF98F42271BF8D00B68009 /* Assets.xcassets */, @@ -216,7 +216,7 @@ F8DF99182271C04C00B68009 /* MVVMÇ.swift in Sources */, F8DF99222271C0A900B68009 /* LoadableModel.swift in Sources */, F8DF991D2271C09A00B68009 /* ToDoListCoordinator.swift in Sources */, - F8DF991E2271C09A00B68009 /* ToDoListViewController.swift in Sources */, + F8DF991E2271C09A00B68009 /* ToDoListView.swift in Sources */, F8DF991F2271C09A00B68009 /* ToDoListInteractor.swift in Sources */, F8DF98EE2271BF8C00B68009 /* AppDelegate.swift in Sources */, ); @@ -303,7 +303,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.1; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -358,7 +358,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.1; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; diff --git a/ExampleApp/ExampleApp/AppDelegate.swift b/ExampleApp/ExampleApp/AppDelegate.swift old mode 100644 new mode 100755 diff --git a/ExampleApp/ExampleApp/ToDoListCoordinator.swift b/ExampleApp/ExampleApp/ToDoListCoordinator.swift old mode 100644 new mode 100755 index d6c862d..5e19595 --- a/ExampleApp/ExampleApp/ToDoListCoordinator.swift +++ b/ExampleApp/ExampleApp/ToDoListCoordinator.swift @@ -8,18 +8,14 @@ import Foundation import UIKit +import SwiftUI class ToDoListCoordinator: Coordinator { override func createRootViewController() -> UIViewController { let interactor = ToDoListInteractor() - let viewModel = ToDoListViewModel(model: interactor.model, interactor: interactor, coordinator: self) - let viewController = ToDoListViewController(viewModel: viewModel) - interactor.bind(with: viewController) { (interactor, viewController) in - let viewModel = ToDoListViewModel(model: interactor.model, interactor: interactor, coordinator: self) - viewController.configure(with: viewModel) - } - + let view = ToDoListView(viewModelPublisher: interactor.viewModelPublisher { ToDoListViewModel(model: $1, interactor: $0, coordinator: self)}) + let viewController = UIHostingController(rootView: view) return viewController } } diff --git a/ExampleApp/ExampleApp/ToDoListInteractor.swift b/ExampleApp/ExampleApp/ToDoListInteractor.swift old mode 100644 new mode 100755 index ef07382..b76a74d --- a/ExampleApp/ExampleApp/ToDoListInteractor.swift +++ b/ExampleApp/ExampleApp/ToDoListInteractor.swift @@ -7,17 +7,16 @@ // import Foundation +import Combine +import SwiftUI class ToDoListInteractor: Interactor { + @Published private(set) var model: LoadableModel<[ToDo]> = .none - private(set) var model: LoadableModel<[ToDo]> = .none { - didSet { - modelDidUpdate?() - } + var modelPublisher: AnyPublisher, Never> { + return $model.eraseToAnyPublisher() } - - var modelDidUpdate: (() -> Void)? - + func load() { model = model.byStartLoading() // Fake load! diff --git a/ExampleApp/ExampleApp/ToDoListView.swift b/ExampleApp/ExampleApp/ToDoListView.swift new file mode 100755 index 0000000..6cef35f --- /dev/null +++ b/ExampleApp/ExampleApp/ToDoListView.swift @@ -0,0 +1,25 @@ +// +// ToDoListView.swift +// ExampleApp +// +// Created by Alex Manzella on 25/04/2019. +// Copyright © 2019 mpow. All rights reserved. +// + +import Foundation +import SwiftUI + +struct ToDoListView: View, UpdateableView { + @ObservedObject var viewModelPublisher: ViewModelPublisher + + var body: some View { + List(viewModel.cellViewModels, rowContent: { Text($0) }) + .onAppear(perform: viewModel.load) + } +} + +extension String: Identifiable { + public var id: String { + return self + } +} diff --git a/ExampleApp/ExampleApp/ToDoListViewController.swift b/ExampleApp/ExampleApp/ToDoListViewController.swift deleted file mode 100644 index a12df1b..0000000 --- a/ExampleApp/ExampleApp/ToDoListViewController.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// ToDoListViewController.swift -// ExampleApp -// -// Created by Alex Manzella on 25/04/2019. -// Copyright © 2019 mpow. All rights reserved. -// - -import Foundation -import UIKit - -class ToDoListViewController: UITableViewController, UpdateableView { - - private var viewModel: ToDoListViewModel - - required init(viewModel: ToDoListViewModel) { - self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") - refreshControl = UIRefreshControl() - configure(with: viewModel) - viewModel.load() - } - - func configure(with viewModel: ToDoListViewModel) { - self.viewModel = viewModel - title = viewModel.title - tableView.reloadData() - if viewModel.isLoading { - refreshControl?.beginRefreshing() - } else { - refreshControl?.endRefreshing() - } - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return viewModel.cellViewModels?.count ?? 0 - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) - cell.textLabel?.text = viewModel.cellViewModels?[indexPath.row] - return cell - } -} diff --git a/ExampleApp/ExampleApp/ToDoListViewModel.swift b/ExampleApp/ExampleApp/ToDoListViewModel.swift old mode 100644 new mode 100755 index 9c2447a..e25b39b --- a/ExampleApp/ExampleApp/ToDoListViewModel.swift +++ b/ExampleApp/ExampleApp/ToDoListViewModel.swift @@ -13,7 +13,7 @@ struct ToDoListViewModel { private let model: LoadableModel<[ToDo]> private let interactor: ToDoListInteractor private let coordinator: ToDoListCoordinator - let cellViewModels: [String]? + let cellViewModels: [String] var title: String { return "ToDo" @@ -31,7 +31,7 @@ struct ToDoListViewModel { cellViewModels = model.value?.flatMap { return [$0.name] + ($0.subTasks?.map { " - \($0.name)" } ?? []) - } + } ?? [] } func load() { diff --git "a/MVVM\303\207.swift" "b/MVVM\303\207.swift" old mode 100644 new mode 100755 index 51906d7..3e96c9c --- "a/MVVM\303\207.swift" +++ "b/MVVM\303\207.swift" @@ -1,31 +1,44 @@ import Foundation import UIKit +import Combine protocol Interactor: class { associatedtype ModelType - var model: ModelType { get } - var modelDidUpdate: (() -> Void)? { get set } + var modelPublisher: AnyPublisher { get } } -protocol UpdateableView: class { +protocol UpdateableView { associatedtype ViewModelType - init(viewModel: ViewModelType) + var viewModelPublisher: ViewModelPublisher { get } +} - func configure(with viewModel: ViewModelType) +extension UpdateableView { + var viewModel: ViewModelType { + return viewModelPublisher.viewModel + } } // MARK: Binding extension Interactor { - func bind(with viewController: T, factory: @escaping (_ interactor: Self, _ vc: T) -> Void) { - modelDidUpdate = { [weak self, weak viewController] in - guard let interactor = self, let viewController = viewController else { - return - } - - factory(interactor, viewController) - } + func viewModelPublisher(_ factory: @escaping (_ interactor: Self, ModelType) -> T) -> ViewModelPublisher { + return ViewModelPublisher( + initialValue: factory(self, model), + publisher: AnyPublisher(modelPublisher.map { factory(self, $0) }) + ) + } +} + +class ViewModelPublisher: ObservableObject { + @Published var viewModel: T + private var cancellable: Any? + + init(initialValue: T, publisher: AnyPublisher) { + self.viewModel = initialValue + self.cancellable = publisher.sink(receiveValue: { [unowned self] viewModel in + self.viewModel = viewModel + }) } }