From 2c4fc3963f0e75ef01545f431ff2d2a66dd09251 Mon Sep 17 00:00:00 2001 From: Ian Luo Date: Wed, 16 Jan 2019 15:33:51 -0800 Subject: [PATCH 1/3] Add content title and button to Details view (#60) * Added strings for 'Contents' on details view * Change simulated delay to 1.0 to be more noticable * Clarify presenting VM vs presented VM in comments * Added content list title and button to detail view * Add some breathing room to stackViews * Fix unit test timeout --- .../Source/Detail/View/DetailPresenter.swift | 4 ++-- Application/Source/Detail/View/DetailView.swift | 15 +++++++++++++++ .../Source/Detail/View/DetailViewController.swift | 7 +++++++ .../Detail/View/DetailViewControllerStyle.swift | 2 ++ .../Source/Detail/View/DetailViewModel.swift | 9 +++++++++ Application/Source/Home/View/HomeViewModel.swift | 2 +- .../Source/Home/View/HomeViewPresenter.swift | 4 ++-- .../Source/Selection/SelectionPresenter.swift | 4 ++-- .../Source/Settings/View/SettingsPresenter.swift | 4 ++-- Application/Source/SwiftGen/Strings.swift | 8 ++++++++ .../Base.lproj/Localizable.strings | 6 ++++++ Application/Tests/HomeViewModelSpec.swift | 2 +- Core/Source/Themes/Themes.swift | 2 +- 13 files changed, 58 insertions(+), 11 deletions(-) diff --git a/Application/Source/Detail/View/DetailPresenter.swift b/Application/Source/Detail/View/DetailPresenter.swift index 77c0ce7..38b2c26 100644 --- a/Application/Source/Detail/View/DetailPresenter.swift +++ b/Application/Source/Detail/View/DetailPresenter.swift @@ -14,8 +14,8 @@ extension DetailPresentingViewModel { /// This action should be executed with a Bool indicating whether the presentation should be animated. /// /// - Parameter factory: A factory to be used to generate the presented view model. - /// - Parameter setupViewModel: This closure will be called with the presenting view model when a present action - /// is executed. Consumers can use this to observe changes to the presenting view model if necessary. + /// - Parameter setupViewModel: This closure will be called with the presented view model when a present action + /// is executed. Consumers can use this to observe changes to the presented view model if necessary. func makePresentDetail( withFactory factory: DetailViewModelFactoryProtocol, setupViewModel: ((DetailViewModel) -> Void)? = nil diff --git a/Application/Source/Detail/View/DetailView.swift b/Application/Source/Detail/View/DetailView.swift index 445290c..10ca09a 100644 --- a/Application/Source/Detail/View/DetailView.swift +++ b/Application/Source/Detail/View/DetailView.swift @@ -23,11 +23,26 @@ class DetailView: UIView { return label }() + let contentsListTitle: UILabel = { + let label = UILabel() + label.textAlignment = .center + label.numberOfLines = 0 + return label + }() + + let contentsButton: UIButton = { + let button = UIButton() + button.setTitleColor(.blue, for: .normal) + return button + }() + private(set) lazy var stackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [ title, button, selectionResult, + contentsListTitle, + contentsButton ]) stackView.axis = .vertical stackView.alignment = .center diff --git a/Application/Source/Detail/View/DetailViewController.swift b/Application/Source/Detail/View/DetailViewController.swift index c638db9..adaacdf 100644 --- a/Application/Source/Detail/View/DetailViewController.swift +++ b/Application/Source/Detail/View/DetailViewController.swift @@ -38,8 +38,15 @@ class DetailViewController: UIViewController, ViewController { viewModel.selectionResult .bind(to: detailView.selectionResult.rx.text) .disposed(by: disposeBag) + viewModel.contentsListTitle + .bind(to: detailView.contentsListTitle.rx.text) + .disposed(by: disposeBag) + viewModel.contentsButtonTitle + .bind(to: detailView.contentsButton.rx.title()) + .disposed(by: disposeBag) detailView.button.rx.bind(to: viewModel.presentSelection, input: true) + detailView.contentsButton.rx.bind(to: viewModel.presentContents, input: ()) rx.isAppeared .bind(to: viewModel.isActive) diff --git a/Application/Source/Detail/View/DetailViewControllerStyle.swift b/Application/Source/Detail/View/DetailViewControllerStyle.swift index c41884b..7904783 100644 --- a/Application/Source/Detail/View/DetailViewControllerStyle.swift +++ b/Application/Source/Detail/View/DetailViewControllerStyle.swift @@ -22,8 +22,10 @@ struct DetailViewControllerStyle: Style { background.apply(to: view) title.apply(to: view.title) + title.apply(to: view.contentsListTitle) selectionResult.apply(to: view.selectionResult) button.apply(to: view.button) + button.apply(to: view.contentsButton) view.stackView.spacing = theme.layout.interitemSpacing } diff --git a/Application/Source/Detail/View/DetailViewModel.swift b/Application/Source/Detail/View/DetailViewModel.swift index 8961566..2bcdc49 100644 --- a/Application/Source/Detail/View/DetailViewModel.swift +++ b/Application/Source/Detail/View/DetailViewModel.swift @@ -2,6 +2,7 @@ import RxSwift import RxCocoa import RxExtensions import Presentations +import Action class DetailViewModel: ViewModel, SelectionPresentingViewModel { @@ -16,6 +17,14 @@ class DetailViewModel: ViewModel, SelectionPresentingViewModel { let presentSelectionTitle = Property(L10n.Detail.Select.title) + let contentsListTitle = Property(L10n.Detail.ContentsList.title) + let contentsButtonTitle = Property(L10n.Detail.ContentsButton.title) + + let presentContents = CocoaAction { _ in + print("Content button pressed") + return .empty() + } + private(set) lazy var presentSelection = makePresentSelection( withFactory: selectionFactory, defaultValue: { [weak self] in diff --git a/Application/Source/Home/View/HomeViewModel.swift b/Application/Source/Home/View/HomeViewModel.swift index 90b75e4..562d176 100644 --- a/Application/Source/Home/View/HomeViewModel.swift +++ b/Application/Source/Home/View/HomeViewModel.swift @@ -24,7 +24,7 @@ class HomeViewModel: ViewModel, DetailPresentingViewModel { let testTextInternalObserver = Observable // Add a delay to simulate a network operation. - .timer(RxTimeInterval(0.5), scheduler: backgroundScheduler) + .timer(RxTimeInterval(1.0), scheduler: backgroundScheduler) .take(1) .map { _ -> String? in return L10n.Home.testText diff --git a/Application/Source/Home/View/HomeViewPresenter.swift b/Application/Source/Home/View/HomeViewPresenter.swift index 4cbc307..9d655c5 100644 --- a/Application/Source/Home/View/HomeViewPresenter.swift +++ b/Application/Source/Home/View/HomeViewPresenter.swift @@ -14,8 +14,8 @@ extension HomePresentingViewModel { /// This action should be executed with a Bool indicating whether the presentation should be animated. /// /// - Parameter factory: A factory to be used to generate the presented view model. - /// - Parameter setupViewModel: This closure will be called with the presenting view model when a present action - /// is executed. Consumers can use this to observe changes to the presenting view model if necessary. + /// - Parameter setupViewModel: This closure will be called with the presented view model when a present action + /// is executed. Consumers can use this to observe changes to the presented view model if necessary. func makePresentHome( withFactory factory: HomeViewModelFactoryProtocol, setupViewModel: ((HomeViewModel) -> Void)? = nil diff --git a/Application/Source/Selection/SelectionPresenter.swift b/Application/Source/Selection/SelectionPresenter.swift index 3b0ae3d..ca14de1 100644 --- a/Application/Source/Selection/SelectionPresenter.swift +++ b/Application/Source/Selection/SelectionPresenter.swift @@ -13,8 +13,8 @@ extension SelectionPresentingViewModel { /// /// - Parameter factory: A factory to be used to generate the presented view model. /// - Parameter defaultValue: A closure that provides the default value of the selection input. - /// - Parameter setupViewModel: This closure will be called with the presenting view model when a present action - /// is executed. Consumers can use this to observe changes to the presenting view model if necessary. + /// - Parameter setupViewModel: This closure will be called with the presented view model when a present action + /// is executed. Consumers can use this to observe changes to the presented view model if necessary. func makePresentSelection( withFactory factory: SelectionViewModelFactoryProtocol, defaultValue: (() -> String?)? = nil, diff --git a/Application/Source/Settings/View/SettingsPresenter.swift b/Application/Source/Settings/View/SettingsPresenter.swift index 61afcf5..a29eb21 100644 --- a/Application/Source/Settings/View/SettingsPresenter.swift +++ b/Application/Source/Settings/View/SettingsPresenter.swift @@ -12,8 +12,8 @@ extension SettingsPresentingViewModel { /// Makes an action that is suitable to be set as the presentSettings action. /// /// - Parameter factory: A factory to be used to generate the presented view model. - /// - Parameter setupViewModel: This closure will be called with the presenting view model when a present action - /// is executed. Consumers can use this to observe changes to the presenting view model if necessary. + /// - Parameter setupViewModel: This closure will be called with the presented view model when a present action + /// is executed. Consumers can use this to observe changes to the presented view model if necessary. func makePresentSettings( withFactory factory: SettingsViewModelFactoryProtocol, setupViewModel: ((SettingsViewModel) -> Void)? = nil diff --git a/Application/Source/SwiftGen/Strings.swift b/Application/Source/SwiftGen/Strings.swift index 0c3f93d..c14a833 100644 --- a/Application/Source/SwiftGen/Strings.swift +++ b/Application/Source/SwiftGen/Strings.swift @@ -15,6 +15,14 @@ internal enum L10n { internal enum Detail { /// Details internal static let title = L10n.tr("Localizable", "detail.title") + internal enum ContentsButton { + /// Contents Info + internal static let title = L10n.tr("Localizable", "detail.contents_button.title") + } + internal enum ContentsList { + /// Contents + internal static let title = L10n.tr("Localizable", "detail.contents_list.title") + } internal enum Select { /// Select Text internal static let title = L10n.tr("Localizable", "detail.select.title") diff --git a/Application/Supporting Files/Base.lproj/Localizable.strings b/Application/Supporting Files/Base.lproj/Localizable.strings index 06f604d..df431bd 100644 --- a/Application/Supporting Files/Base.lproj/Localizable.strings +++ b/Application/Supporting Files/Base.lproj/Localizable.strings @@ -7,6 +7,12 @@ /* The title of the tab bar item for the detail navigation flow. */ "detail_navigation.tab_bar_item.title" = "Detail"; +/* Title for the contents list on the details view. */ +"detail.contents_list.title" = "Contents"; + +/* Title for the contents button that presents the contents view. */ +"detail.contents_button.title" = "Contents Info"; + /* The title of the button that presents the detail view. */ "home.present_detail.title" = "Details"; diff --git a/Application/Tests/HomeViewModelSpec.swift b/Application/Tests/HomeViewModelSpec.swift index 3c53242..fbeaf86 100644 --- a/Application/Tests/HomeViewModelSpec.swift +++ b/Application/Tests/HomeViewModelSpec.swift @@ -26,7 +26,7 @@ class HomeViewModelSpec: QuickSpec { viewModel.isActive.accept(true) - expect(viewModel.testText.value).toEventually(equal(L10n.Home.testText)) + expect(viewModel.testText.value).toEventually(equal(L10n.Home.testText), timeout: 2.0) } } } diff --git a/Core/Source/Themes/Themes.swift b/Core/Source/Themes/Themes.swift index b24b867..fc3709b 100644 --- a/Core/Source/Themes/Themes.swift +++ b/Core/Source/Themes/Themes.swift @@ -34,7 +34,7 @@ public extension Theme { public var layout: Layout { return Layout( - interitemSpacing: 10, + interitemSpacing: 20, buttonContentEdgeInsets: UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 20), buttonBorderWidth: 1, buttonCornerRadius: 12, From d423271a11d8590ac158c8083a7aa7fc6e6507fe Mon Sep 17 00:00:00 2001 From: Ian Luo Date: Sun, 27 Jan 2019 16:51:29 -0800 Subject: [PATCH 2/3] Add Content model and list display (#63) * Abstract centered label, remove unnecessary button boilerplate, and add spacing view * No longer excluding colors.txt from project in Xcode * Add alternate body text color * Adding content / food enum for testing * Display contents in detail view * Further abstract contents list creation and add unit test * Refactor contents list to be more reactive * Changes per PR review * Remove centeredLabel extension in favor of UILabelStyle additions * Use container stackView instead of spacingView --- .../Source/Detail/View/DetailView.swift | 64 +++++++++---------- .../Detail/View/DetailViewController.swift | 14 ++-- .../View/DetailViewControllerStyle.swift | 27 +++++--- .../Source/Detail/View/DetailViewModel.swift | 15 ++++- Application/Source/Home/View/HomeView.swift | 13 +--- Application/Source/Models/Food.swift | 22 +++++++ .../Source/Selection/SelectionView.swift | 7 +- .../SelectionViewControllerStyle.swift | 1 + Application/Source/SwiftGen/Strings.swift | 23 +++++-- .../Base.lproj/Localizable.strings | 20 ++++-- Application/Tests/DetailViewModelSpec.swift | 10 ++- Core/Resources/Colors.txt | 2 + Core/Source/SwiftGen/Colors.swift | 6 ++ Core/Source/Themes/Styles/LabelStyle.swift | 14 ++++ .../Source/Themes/Styles/TextFieldStyle.swift | 2 +- Core/Source/Themes/Themes.swift | 15 +++++ Project_Core.yml | 2 - Themer/Source/UIKit/UILabelStyle.swift | 6 ++ Themer/Tests/Stubs/StubUILabelStyle.swift | 2 + Themer/Tests/UILabelStyleSpec.swift | 10 ++- 20 files changed, 191 insertions(+), 84 deletions(-) create mode 100644 Application/Source/Models/Food.swift diff --git a/Application/Source/Detail/View/DetailView.swift b/Application/Source/Detail/View/DetailView.swift index 10ca09a..6491de1 100644 --- a/Application/Source/Detail/View/DetailView.swift +++ b/Application/Source/Detail/View/DetailView.swift @@ -3,46 +3,40 @@ import SnapKit class DetailView: UIView { - let title: UILabel = { - let label = UILabel() - label.textAlignment = .center - label.numberOfLines = 0 - return label - }() + let title = UILabel() + let button = UIButton() + let selectionResult = UILabel() - let button: UIButton = { - let button = UIButton() - button.setTitleColor(.blue, for: .normal) - return button - }() + let foodListTitle = UILabel() + let foodList = UILabel() + let foodInfoButton = UIButton() - let selectionResult: UILabel = { - let label = UILabel() - label.textAlignment = .center - label.numberOfLines = 0 - return label - }() - - let contentsListTitle: UILabel = { - let label = UILabel() - label.textAlignment = .center - label.numberOfLines = 0 - return label + private(set) lazy var selectionStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [ + title, + button, + selectionResult + ]) + stackView.axis = .vertical + stackView.alignment = .center + return stackView }() - let contentsButton: UIButton = { - let button = UIButton() - button.setTitleColor(.blue, for: .normal) - return button + private(set) lazy var foodStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [ + foodListTitle, + foodList, + foodInfoButton + ]) + stackView.axis = .vertical + stackView.alignment = .center + return stackView }() - private(set) lazy var stackView: UIStackView = { + private(set) lazy var containerStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [ - title, - button, - selectionResult, - contentsListTitle, - contentsButton + selectionStackView, + foodStackView ]) stackView.axis = .vertical stackView.alignment = .center @@ -54,8 +48,8 @@ class DetailView: UIView { override init(frame: CGRect) { super.init(frame: frame) - addSubview(stackView) - stackView.snp.makeConstraints { make in + addSubview(containerStackView) + containerStackView.snp.makeConstraints { make in make.center.equalTo(self) } } diff --git a/Application/Source/Detail/View/DetailViewController.swift b/Application/Source/Detail/View/DetailViewController.swift index adaacdf..cb114a5 100644 --- a/Application/Source/Detail/View/DetailViewController.swift +++ b/Application/Source/Detail/View/DetailViewController.swift @@ -38,15 +38,19 @@ class DetailViewController: UIViewController, ViewController { viewModel.selectionResult .bind(to: detailView.selectionResult.rx.text) .disposed(by: disposeBag) - viewModel.contentsListTitle - .bind(to: detailView.contentsListTitle.rx.text) + + viewModel.foodListTitle + .bind(to: detailView.foodListTitle.rx.text) + .disposed(by: disposeBag) + viewModel.foodListText + .bind(to: detailView.foodList.rx.text) .disposed(by: disposeBag) - viewModel.contentsButtonTitle - .bind(to: detailView.contentsButton.rx.title()) + viewModel.foodInfoButtonTitle + .bind(to: detailView.foodInfoButton.rx.title()) .disposed(by: disposeBag) detailView.button.rx.bind(to: viewModel.presentSelection, input: true) - detailView.contentsButton.rx.bind(to: viewModel.presentContents, input: ()) + detailView.foodInfoButton.rx.bind(to: viewModel.presentContents, input: ()) rx.isAppeared .bind(to: viewModel.isActive) diff --git a/Application/Source/Detail/View/DetailViewControllerStyle.swift b/Application/Source/Detail/View/DetailViewControllerStyle.swift index 7904783..40ee318 100644 --- a/Application/Source/Detail/View/DetailViewControllerStyle.swift +++ b/Application/Source/Detail/View/DetailViewControllerStyle.swift @@ -3,31 +3,38 @@ import UIKit import Core struct DetailViewControllerStyle: Style { + let theme: Theme let background: BackgroundViewStyle - let title: LabelStyle - let selectionResult: LabelStyle + let label: LabelStyle + let alternateLabel: AlternateLabelStyle let button: ButtonStyle init(theme: Theme) { self.theme = theme background = BackgroundViewStyle(theme: theme) - title = LabelStyle(theme: theme) - selectionResult = LabelStyle(theme: theme) + label = LabelStyle(theme: theme) + alternateLabel = AlternateLabelStyle(theme: theme) button = ButtonStyle(theme: theme) } func apply(to styleable: DetailViewController) { let view = styleable.detailView - background.apply(to: view) - title.apply(to: view.title) - title.apply(to: view.contentsListTitle) - selectionResult.apply(to: view.selectionResult) + + label.apply(to: view.title) + label.apply(to: view.foodListTitle) + + alternateLabel.apply(to: view.selectionResult) + alternateLabel.apply(to: view.foodList) + button.apply(to: view.button) - button.apply(to: view.contentsButton) + button.apply(to: view.foodInfoButton) + + view.selectionStackView.spacing = theme.layout.interitemSpacing + view.foodStackView.spacing = theme.layout.interitemSpacing - view.stackView.spacing = theme.layout.interitemSpacing + view.containerStackView.spacing = theme.layout.containerSpacing } } diff --git a/Application/Source/Detail/View/DetailViewModel.swift b/Application/Source/Detail/View/DetailViewModel.swift index 2bcdc49..a70332a 100644 --- a/Application/Source/Detail/View/DetailViewModel.swift +++ b/Application/Source/Detail/View/DetailViewModel.swift @@ -17,14 +17,25 @@ class DetailViewModel: ViewModel, SelectionPresentingViewModel { let presentSelectionTitle = Property(L10n.Detail.Select.title) - let contentsListTitle = Property(L10n.Detail.ContentsList.title) - let contentsButtonTitle = Property(L10n.Detail.ContentsButton.title) + let foodListTitle = Property(L10n.Detail.FoodList.title) + let foodInfoButtonTitle = Property(L10n.Detail.FoodButton.title) let presentContents = CocoaAction { _ in print("Content button pressed") return .empty() } + let foods: BehaviorRelay<[Food]> = BehaviorRelay(value: [.beans, .greens, .potatoes, .tomatoes]) + + private(set) lazy var foodListText: Property = { + let observable = foods.map { foods -> String in + return foods + .map { $0.name } + .joined(separator: ", ") + } + return Property(observable, initial: "") + }() + private(set) lazy var presentSelection = makePresentSelection( withFactory: selectionFactory, defaultValue: { [weak self] in diff --git a/Application/Source/Home/View/HomeView.swift b/Application/Source/Home/View/HomeView.swift index 6cc8e70..7167175 100644 --- a/Application/Source/Home/View/HomeView.swift +++ b/Application/Source/Home/View/HomeView.swift @@ -3,12 +3,7 @@ import SnapKit class HomeView: UIView { - let label: UILabel = { - let label = UILabel() - label.textAlignment = .center - label.numberOfLines = 0 - return label - }() + let label = UILabel() let imageView: UIImageView = { let imageView = UIImageView() @@ -17,11 +12,7 @@ class HomeView: UIView { return imageView }() - let detailButton: UIButton = { - let button = UIButton() - button.setTitleColor(.blue, for: .normal) - return button - }() + let detailButton = UIButton() private(set) lazy var stackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [ diff --git a/Application/Source/Models/Food.swift b/Application/Source/Models/Food.swift new file mode 100644 index 0000000..546cf6b --- /dev/null +++ b/Application/Source/Models/Food.swift @@ -0,0 +1,22 @@ +import Foundation +import RxCocoa + +/** + It's not a side project if you can't have some fun, right? + - [Original.](https://www.youtube.com/watch?v=amONEHAhLHY) + - [Crispy.](https://www.youtube.com/watch?v=1BC1G33-fNY) + */ +enum Food { + + case beans, greens, potatoes, tomatoes + + var name: String { + switch self { + case .beans: return L10n.Food.beans + case .greens: return L10n.Food.greens + case .potatoes: return L10n.Food.potatoes + case .tomatoes: return L10n.Food.tomatoes + } + } + +} diff --git a/Application/Source/Selection/SelectionView.swift b/Application/Source/Selection/SelectionView.swift index ff03719..cbaa67e 100644 --- a/Application/Source/Selection/SelectionView.swift +++ b/Application/Source/Selection/SelectionView.swift @@ -4,12 +4,7 @@ import SnapKit class SelectionView: UIView { let textField = UITextField() - - let submitButton: UIButton = { - let button = UIButton() - button.setTitleColor(.blue, for: .normal) - return button - }() + let submitButton = UIButton() private(set) lazy var stackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [ diff --git a/Application/Source/Selection/SelectionViewControllerStyle.swift b/Application/Source/Selection/SelectionViewControllerStyle.swift index 2b894e0..9956c4b 100644 --- a/Application/Source/Selection/SelectionViewControllerStyle.swift +++ b/Application/Source/Selection/SelectionViewControllerStyle.swift @@ -3,6 +3,7 @@ import UIKit import Core struct SelectionViewControllerStyle: Style { + let theme: Theme let background: BackgroundViewStyle let textField: TextFieldStyle diff --git a/Application/Source/SwiftGen/Strings.swift b/Application/Source/SwiftGen/Strings.swift index c14a833..b54610f 100644 --- a/Application/Source/SwiftGen/Strings.swift +++ b/Application/Source/SwiftGen/Strings.swift @@ -15,13 +15,13 @@ internal enum L10n { internal enum Detail { /// Details internal static let title = L10n.tr("Localizable", "detail.title") - internal enum ContentsButton { - /// Contents Info - internal static let title = L10n.tr("Localizable", "detail.contents_button.title") + internal enum FoodButton { + /// Food Info + internal static let title = L10n.tr("Localizable", "detail.food_button.title") } - internal enum ContentsList { - /// Contents - internal static let title = L10n.tr("Localizable", "detail.contents_list.title") + internal enum FoodList { + /// Ingredients + internal static let title = L10n.tr("Localizable", "detail.food_list.title") } internal enum Select { /// Select Text @@ -36,6 +36,17 @@ internal enum L10n { } } + internal enum Food { + /// Beans + internal static let beans = L10n.tr("Localizable", "food.beans") + /// Greens + internal static let greens = L10n.tr("Localizable", "food.greens") + /// Potatoes + internal static let potatoes = L10n.tr("Localizable", "food.potatoes") + /// Tomatoes + internal static let tomatoes = L10n.tr("Localizable", "food.tomatoes") + } + internal enum Home { /// It works! internal static let testText = L10n.tr("Localizable", "home.test_text") diff --git a/Application/Supporting Files/Base.lproj/Localizable.strings b/Application/Supporting Files/Base.lproj/Localizable.strings index df431bd..a6b8dbd 100644 --- a/Application/Supporting Files/Base.lproj/Localizable.strings +++ b/Application/Supporting Files/Base.lproj/Localizable.strings @@ -7,11 +7,11 @@ /* The title of the tab bar item for the detail navigation flow. */ "detail_navigation.tab_bar_item.title" = "Detail"; -/* Title for the contents list on the details view. */ -"detail.contents_list.title" = "Contents"; +/* Title for the food list on the details view. */ +"detail.food_list.title" = "Ingredients"; -/* Title for the contents button that presents the contents view. */ -"detail.contents_button.title" = "Contents Info"; +/* Title for the food button that presents the contents view. */ +"detail.food_button.title" = "Food Info"; /* The title of the button that presents the detail view. */ "home.present_detail.title" = "Details"; @@ -30,3 +30,15 @@ /* The title of the tab bar item for the settings navigation flow. */ "settings_navigation.tab_bar_item.title" = "Settings"; + +/* Name of Food: beans. */ +"food.beans" = "Beans"; + +/* Name of Food: greens. */ +"food.greens" = "Greens"; + +/* Name of Food: potatoes. */ +"food.potatoes" = "Potatoes"; + +/* Name of Food: tomatoes. */ +"food.tomatoes" = "Tomatoes"; diff --git a/Application/Tests/DetailViewModelSpec.swift b/Application/Tests/DetailViewModelSpec.swift index ec6fcae..3ea3edb 100644 --- a/Application/Tests/DetailViewModelSpec.swift +++ b/Application/Tests/DetailViewModelSpec.swift @@ -78,8 +78,16 @@ class DetailViewModelSpec: QuickSpec { expect(viewModel.title.value).to(equal(L10n.Detail.title)) } } - } + describe("foodListText") { + it("should return a String list from the array of Contents") { + viewModel.foods.accept([.tomatoes, .potatoes]) + let expected = L10n.Food.tomatoes + ", " + L10n.Food.potatoes + + expect(viewModel.foodListText.value).to(equal(expected)) + } + } + } } } diff --git a/Core/Resources/Colors.txt b/Core/Resources/Colors.txt index a042b07..171b045 100644 --- a/Core/Resources/Colors.txt +++ b/Core/Resources/Colors.txt @@ -1,6 +1,8 @@ LightContent : #000000 +LightContentAlt : #7b7b7b LightBackground : #FFFFFF LightActionColor : #11A4D6 DarkContent : #D3D3D3 +DarkContentAlt : #7b7b7b DarkBackground : #000000 DarkActionColor : #15B7ED diff --git a/Core/Source/SwiftGen/Colors.swift b/Core/Source/SwiftGen/Colors.swift index 36a2b48..53deadd 100644 --- a/Core/Source/SwiftGen/Colors.swift +++ b/Core/Source/SwiftGen/Colors.swift @@ -28,6 +28,9 @@ public struct Color { /// /// Alpha: 100%
(0xd3d3d3ff) public static let darkContent = Color(rgbaValue: 0xd3d3d3ff) + /// + /// Alpha: 100%
(0x7b7b7bff) + public static let darkContentAlt = Color(rgbaValue: 0x7b7b7bff) /// /// Alpha: 100%
(0x11a4d6ff) public static let lightActionColor = Color(rgbaValue: 0x11a4d6ff) @@ -37,6 +40,9 @@ public struct Color { /// /// Alpha: 100%
(0x000000ff) public static let lightContent = Color(rgbaValue: 0x000000ff) + /// + /// Alpha: 100%
(0x7b7b7bff) + public static let lightContentAlt = Color(rgbaValue: 0x7b7b7bff) } // swiftlint:enable identifier_name line_length type_body_length diff --git a/Core/Source/Themes/Styles/LabelStyle.swift b/Core/Source/Themes/Styles/LabelStyle.swift index c38dd8a..5322be6 100644 --- a/Core/Source/Themes/Styles/LabelStyle.swift +++ b/Core/Source/Themes/Styles/LabelStyle.swift @@ -4,9 +4,23 @@ import UIKit public struct LabelStyle: UILabelStyle { public let textColor: UIColor + public let numberOfLines: Int = 0 + public let textAlignment: NSTextAlignment = .center public init(theme: Theme) { textColor = theme.color.bodyText } } + +public struct AlternateLabelStyle: UILabelStyle { + + public let textColor: UIColor + public let numberOfLines: Int = 0 + public let textAlignment: NSTextAlignment = .center + + public init(theme: Theme) { + textColor = theme.color.alternateBodyText + } + +} diff --git a/Core/Source/Themes/Styles/TextFieldStyle.swift b/Core/Source/Themes/Styles/TextFieldStyle.swift index 25b24e9..333a833 100644 --- a/Core/Source/Themes/Styles/TextFieldStyle.swift +++ b/Core/Source/Themes/Styles/TextFieldStyle.swift @@ -6,7 +6,7 @@ public struct TextFieldStyle: UITextFieldStyle { public let textColor: UIColor public init(theme: Theme) { - textColor = theme.color.bodyText + textColor = theme.color.alternateBodyText } } diff --git a/Core/Source/Themes/Themes.swift b/Core/Source/Themes/Themes.swift index fc3709b..ceb1f88 100644 --- a/Core/Source/Themes/Themes.swift +++ b/Core/Source/Themes/Themes.swift @@ -6,10 +6,12 @@ public enum Theme { } public extension Theme { + public var color: ColorSet { switch self { case .light: return ColorSet( bodyText: Color.lightContent.color, + alternateBodyText: Color.lightContentAlt.color, inputText: Color.lightContent.color, actionColor: Color.lightActionColor.color, viewBackground: Color.lightBackground.color, @@ -17,6 +19,7 @@ public extension Theme { tabBarTint: nil) case .dark: return ColorSet( bodyText: Color.darkContent.color, + alternateBodyText: Color.darkContentAlt.color, inputText: Color.darkContent.color, actionColor: Color.darkActionColor.color, viewBackground: Color.darkBackground.color, @@ -35,26 +38,38 @@ public extension Theme { public var layout: Layout { return Layout( interitemSpacing: 20, + containerSpacing: 35, buttonContentEdgeInsets: UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 20), buttonBorderWidth: 1, buttonCornerRadius: 12, contentLayoutMargins: UIEdgeInsets(top: 10, left: 15, bottom: 10, right: 15)) } + } public struct ColorSet { + public let bodyText: UIColor + public let alternateBodyText: UIColor public let inputText: UIColor public let actionColor: UIColor public let viewBackground: UIColor? public let navigationBarTint: UIColor? public let tabBarTint: UIColor? + } public struct Layout { + + /// Spacing between elements within an inner (or single) stackView. public let interitemSpacing: CGFloat + + /// Spacing between inner stackViews within an outer container stackView. + public let containerSpacing: CGFloat + public let buttonContentEdgeInsets: UIEdgeInsets public let buttonBorderWidth: CGFloat public let buttonCornerRadius: CGFloat public let contentLayoutMargins: UIEdgeInsets + } diff --git a/Project_Core.yml b/Project_Core.yml index d413004..90db074 100644 --- a/Project_Core.yml +++ b/Project_Core.yml @@ -7,8 +7,6 @@ targets: - Core/Source - Core/Supporting Files - path: Core/Resources - excludes: - - "*.txt" dependencies: - target: Themer - target: Logger diff --git a/Themer/Source/UIKit/UILabelStyle.swift b/Themer/Source/UIKit/UILabelStyle.swift index 49e17e1..218fa22 100644 --- a/Themer/Source/UIKit/UILabelStyle.swift +++ b/Themer/Source/UIKit/UILabelStyle.swift @@ -2,10 +2,16 @@ import UIKit public protocol UILabelStyle: Style where Styleable: UILabel { var textColor: UIColor { get } + var textAlignment: NSTextAlignment { get } + var numberOfLines: Int { get } } public extension UILabelStyle { + public func apply(to styleable: UILabel) { styleable.textColor = textColor + styleable.textAlignment = textAlignment + styleable.numberOfLines = numberOfLines } + } diff --git a/Themer/Tests/Stubs/StubUILabelStyle.swift b/Themer/Tests/Stubs/StubUILabelStyle.swift index 22b6dd5..d002a68 100644 --- a/Themer/Tests/Stubs/StubUILabelStyle.swift +++ b/Themer/Tests/Stubs/StubUILabelStyle.swift @@ -4,5 +4,7 @@ import UIKit struct StubUILabelStyle: UILabelStyle { let textColor: UIColor + let textAlignment: NSTextAlignment + let numberOfLines: Int } diff --git a/Themer/Tests/UILabelStyleSpec.swift b/Themer/Tests/UILabelStyleSpec.swift index 35b2f04..92caa0d 100644 --- a/Themer/Tests/UILabelStyleSpec.swift +++ b/Themer/Tests/UILabelStyleSpec.swift @@ -10,15 +10,23 @@ class UILabelStyleSpec: QuickSpec { describe("UILabelStyle") { it("should update appropriate values") { let textColor: UIColor = .red + let textAlignment: NSTextAlignment = .right + let numberOfLines = 99 let style = StubUILabelStyle( - textColor: textColor) + textColor: textColor, + textAlignment: textAlignment, + numberOfLines: numberOfLines) let label = UILabel() expect(label.textColor).notTo(equal(textColor)) + expect(label.textAlignment).notTo(equal(textAlignment)) + expect(label.numberOfLines).notTo(equal(numberOfLines)) style.apply(to: label) expect(label.textColor).to(equal(textColor)) + expect(label.textAlignment).to(equal(textAlignment)) + expect(label.numberOfLines).to(equal(numberOfLines)) } } From a0e3b7331f090f38b823e9d545c929483f873b8f Mon Sep 17 00:00:00 2001 From: Ian Luo Date: Sun, 24 Feb 2019 17:52:05 -0800 Subject: [PATCH 3/3] Add Basic Food Table View (#66) * Add FoodTable class cluster * Populate basic tableView * Add tests for FoodTable * Per Swift4 convention, use AnyObject instead of class for protocol constraints * Per PR review, fix FoodVCFactoryProtocol conformance * Per PR review, rename FoodTable to FoodInfo * Per PR review, streamline DetailVM factory protocol parameter * Per PR review, make cell identifier local var * Per PR review, update foods dependency structure * PR cleanup * Per PR review, cleanup where Food is located --- .../DetailNavigationController.swift | 9 +++ .../Navigation/DetailNavigationModel.swift | 2 - .../Source/Detail/View/DetailPresenter.swift | 5 +- .../Detail/View/DetailViewController.swift | 4 +- .../Source/Detail/View/DetailViewModel.swift | 27 ++++---- .../Food Info/FoodInfoViewController.swift | 61 +++++++++++++++++++ .../FoodInfoViewControllerStyle.swift | 16 +++++ .../Source/Food Info/FoodInfoViewModel.swift | 37 +++++++++++ .../Food Info/FoodInfoViewPresenter.swift | 45 ++++++++++++++ .../Navigation/HomeNavigationController.swift | 9 +++ .../Home/Navigation/HomeNavigationModel.swift | 2 - .../Source/Home/View/HomeViewPresenter.swift | 2 +- .../Source/Root/RootTabBarViewModel.swift | 4 ++ .../Source/Selection/SelectionPresenter.swift | 4 +- .../Settings/View/SettingsPresenter.swift | 4 +- Application/Tests/DetailViewModelSpec.swift | 5 +- Application/Tests/FoodInfoPresenterSpec.swift | 45 ++++++++++++++ Application/Tests/FoodInfoViewModelSpec.swift | 32 ++++++++++ .../Tests/Stubs/StubDetailPresenter.swift | 4 ++ .../Stubs/StubDetailPresentingViewModel.swift | 5 +- .../Tests/Stubs/StubFoodInfoPresenter.swift | 19 ++++++ .../StubFoodInfoPresentingViewModel.swift | 34 +++++++++++ .../Stubs/StubHomePresentingViewModel.swift | 3 + .../Source/PresentationContext.swift | 2 +- Presentations/Source/Presention.swift | 2 +- 25 files changed, 352 insertions(+), 30 deletions(-) create mode 100644 Application/Source/Food Info/FoodInfoViewController.swift create mode 100644 Application/Source/Food Info/FoodInfoViewControllerStyle.swift create mode 100644 Application/Source/Food Info/FoodInfoViewModel.swift create mode 100644 Application/Source/Food Info/FoodInfoViewPresenter.swift create mode 100644 Application/Tests/FoodInfoPresenterSpec.swift create mode 100644 Application/Tests/FoodInfoViewModelSpec.swift create mode 100644 Application/Tests/Stubs/StubFoodInfoPresenter.swift create mode 100644 Application/Tests/Stubs/StubFoodInfoPresentingViewModel.swift diff --git a/Application/Source/Detail/Navigation/DetailNavigationController.swift b/Application/Source/Detail/Navigation/DetailNavigationController.swift index 41d1b8b..a04e504 100644 --- a/Application/Source/Detail/Navigation/DetailNavigationController.swift +++ b/Application/Source/Detail/Navigation/DetailNavigationController.swift @@ -44,4 +44,13 @@ extension DetailNavigationController: SelectionPresenter { } +extension DetailNavigationController: FoodInfoPresenter { + + func foodInfoPresentation(of viewModel: FoodInfoViewModel) -> DismissablePresentation { + let viewController = factory.makeFoodInfoViewController(viewModel: viewModel) + return makePushPresentation(of: viewController) + } + +} + protocol DetailNavigationControllerFactoryProtocol: DetailViewControllerFactoryProtocol, SingleViewNavigationControllerFactoryProtocol { } diff --git a/Application/Source/Detail/Navigation/DetailNavigationModel.swift b/Application/Source/Detail/Navigation/DetailNavigationModel.swift index 24a60ce..c0e2372 100644 --- a/Application/Source/Detail/Navigation/DetailNavigationModel.swift +++ b/Application/Source/Detail/Navigation/DetailNavigationModel.swift @@ -31,5 +31,3 @@ extension DetailNavigationModelFactoryProtocol { } } - -class DetailNavigationModelFactory: DetailNavigationModelFactoryProtocol { } diff --git a/Application/Source/Detail/View/DetailPresenter.swift b/Application/Source/Detail/View/DetailPresenter.swift index 38b2c26..a3b247c 100644 --- a/Application/Source/Detail/View/DetailPresenter.swift +++ b/Application/Source/Detail/View/DetailPresenter.swift @@ -2,7 +2,7 @@ import RxSwift import Action import Presentations -protocol DetailPresentingViewModel: class, PresentingViewModel { +protocol DetailPresentingViewModel: AnyObject, PresentingViewModel { var detailPresenter: DetailPresenter? { get set } var presentDetail: Action { get } } @@ -30,6 +30,7 @@ extension DetailPresentingViewModel { let viewModel = factory.makeDetailViewModel() viewModel.selectionPresenter = presenter + viewModel.foodInfoPresenter = presenter setupViewModel?(viewModel) @@ -41,6 +42,6 @@ extension DetailPresentingViewModel { } -protocol DetailPresenter: SelectionPresenter { +protocol DetailPresenter: SelectionPresenter, FoodInfoPresenter { func detailPresentation(of viewModel: DetailViewModel) -> DismissablePresentation } diff --git a/Application/Source/Detail/View/DetailViewController.swift b/Application/Source/Detail/View/DetailViewController.swift index cb114a5..b2a08ce 100644 --- a/Application/Source/Detail/View/DetailViewController.swift +++ b/Application/Source/Detail/View/DetailViewController.swift @@ -50,7 +50,7 @@ class DetailViewController: UIViewController, ViewController { .disposed(by: disposeBag) detailView.button.rx.bind(to: viewModel.presentSelection, input: true) - detailView.foodInfoButton.rx.bind(to: viewModel.presentContents, input: ()) + detailView.foodInfoButton.rx.bind(to: viewModel.presentFoodInfo, input: true) rx.isAppeared .bind(to: viewModel.isActive) @@ -69,7 +69,7 @@ class DetailViewController: UIViewController, ViewController { } -protocol DetailViewControllerFactoryProtocol: SelectionViewControllerFactoryProtocol { +protocol DetailViewControllerFactoryProtocol: SelectionViewControllerFactoryProtocol, FoodInfoViewControllerFactoryProtocol { var themeProvider: ThemeProvider { get } func makeDetailViewController(viewModel: DetailViewModel) -> DetailViewController diff --git a/Application/Source/Detail/View/DetailViewModel.swift b/Application/Source/Detail/View/DetailViewModel.swift index a70332a..ff296b5 100644 --- a/Application/Source/Detail/View/DetailViewModel.swift +++ b/Application/Source/Detail/View/DetailViewModel.swift @@ -4,11 +4,12 @@ import RxExtensions import Presentations import Action -class DetailViewModel: ViewModel, SelectionPresentingViewModel { +class DetailViewModel: ViewModel, SelectionPresentingViewModel, FoodInfoPresentingViewModel { let isActive = BehaviorRelay(value: false) weak var selectionPresenter: SelectionPresenter? + weak var foodInfoPresenter: FoodInfoPresenter? let title = Property(L10n.Detail.title) @@ -20,12 +21,9 @@ class DetailViewModel: ViewModel, SelectionPresentingViewModel { let foodListTitle = Property(L10n.Detail.FoodList.title) let foodInfoButtonTitle = Property(L10n.Detail.FoodButton.title) - let presentContents = CocoaAction { _ in - print("Content button pressed") - return .empty() - } + private(set) lazy var presentFoodInfo = makePresentFoodInfo(withFactory: factory) - let foods: BehaviorRelay<[Food]> = BehaviorRelay(value: [.beans, .greens, .potatoes, .tomatoes]) + private let foods: Property<[Food]> private(set) lazy var foodListText: Property = { let observable = foods.map { foods -> String in @@ -37,7 +35,7 @@ class DetailViewModel: ViewModel, SelectionPresentingViewModel { }() private(set) lazy var presentSelection = makePresentSelection( - withFactory: selectionFactory, + withFactory: factory, defaultValue: { [weak self] in return self?.selectionResult.value }, @@ -50,24 +48,29 @@ class DetailViewModel: ViewModel, SelectionPresentingViewModel { .disposed(by: self.disposeBag) }) - init(selectionFactory: SelectionViewModelFactoryProtocol) { - self.selectionFactory = selectionFactory + typealias Factory = SelectionViewModelFactoryProtocol & FoodInfoViewModelFactoryProtocol + + init(foods: Property<[Food]>, factory: Factory) { + self.foods = foods + self.factory = factory } private let selectionResultRelay = BehaviorRelay(value: nil) - private let selectionFactory: SelectionViewModelFactoryProtocol + private let factory: Factory private let disposeBag = DisposeBag() } -protocol DetailViewModelFactoryProtocol: SelectionViewModelFactoryProtocol { +protocol DetailViewModelFactoryProtocol: SelectionViewModelFactoryProtocol, FoodInfoViewModelFactoryProtocol { + var foods: Property<[Food]> { get } + func makeDetailViewModel() -> DetailViewModel } extension DetailViewModelFactoryProtocol { func makeDetailViewModel() -> DetailViewModel { - return DetailViewModel(selectionFactory: self) + return DetailViewModel(foods: foods, factory: self) } } diff --git a/Application/Source/Food Info/FoodInfoViewController.swift b/Application/Source/Food Info/FoodInfoViewController.swift new file mode 100644 index 0000000..5840c1d --- /dev/null +++ b/Application/Source/Food Info/FoodInfoViewController.swift @@ -0,0 +1,61 @@ +import UIKit +import Presentations +import RxSwift +import Action +import Core + +class FoodInfoViewController: UITableViewController, ViewController { + + let viewModel: FoodInfoViewModel + + let themeProvider: ThemeProvider + + required init(viewModel: FoodInfoViewModel, themeProvider: ThemeProvider) { + self.viewModel = viewModel + self.themeProvider = themeProvider + + super.init(nibName: nil, bundle: nil) + } + + override func viewDidLoad() { + super.viewDidLoad() + + let cellIdentifier = "FoodCell" + tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier) + + viewModel.foods + .bind(to: tableView.rx.items(cellIdentifier: cellIdentifier)) { _, food, cell in + cell.textLabel?.text = food.name + } + .disposed(by: disposeBag) + + rx.isAppeared + .bind(to: viewModel.isActive) + .disposed(by: disposeBag) + + themeProvider.bindToStyleable(self) { FoodInfoViewControllerStyle(theme: $0) } + } + + @available(*, unavailable) + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { fatalError("\(#function) not implemented.") } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { fatalError("\(#function) not implemented.") } + + private let disposeBag = DisposeBag() + +} + +protocol FoodInfoViewControllerFactoryProtocol { + var themeProvider: ThemeProvider { get } + + func makeFoodInfoViewController(viewModel: FoodInfoViewModel) -> FoodInfoViewController +} + +extension FoodInfoViewControllerFactoryProtocol { + + func makeFoodInfoViewController(viewModel: FoodInfoViewModel) -> FoodInfoViewController { + return FoodInfoViewController(viewModel: viewModel, themeProvider: themeProvider) + } + +} diff --git a/Application/Source/Food Info/FoodInfoViewControllerStyle.swift b/Application/Source/Food Info/FoodInfoViewControllerStyle.swift new file mode 100644 index 0000000..55198d4 --- /dev/null +++ b/Application/Source/Food Info/FoodInfoViewControllerStyle.swift @@ -0,0 +1,16 @@ +import Themer +import UIKit +import Core + +struct FoodInfoViewControllerStyle: Style { + + let theme: Theme + + init(theme: Theme) { + self.theme = theme + } + + func apply(to styleable: FoodInfoViewController) { + } + +} diff --git a/Application/Source/Food Info/FoodInfoViewModel.swift b/Application/Source/Food Info/FoodInfoViewModel.swift new file mode 100644 index 0000000..01581e0 --- /dev/null +++ b/Application/Source/Food Info/FoodInfoViewModel.swift @@ -0,0 +1,37 @@ +import RxSwift +import RxCocoa +import RxExtensions +import Presentations +import Action + +class FoodInfoViewModel: ViewModel { + + let isActive = BehaviorRelay(value: false) + let foods: Property<[Food]> + + init(with foods: Property<[Food]>) { + self.foods = foods + foods + .asObservable() + .logValue(.info, .application) { "FOODS: \($0)" } + .subscribe() + .disposed(by: disposeBag) + } + + private let disposeBag = DisposeBag() + +} + +protocol FoodInfoViewModelFactoryProtocol { + var foods: Property<[Food]> { get } + + func makeFoodInfoViewModel() -> FoodInfoViewModel +} + +extension FoodInfoViewModelFactoryProtocol { + + func makeFoodInfoViewModel() -> FoodInfoViewModel { + return FoodInfoViewModel(with: foods) + } + +} diff --git a/Application/Source/Food Info/FoodInfoViewPresenter.swift b/Application/Source/Food Info/FoodInfoViewPresenter.swift new file mode 100644 index 0000000..4c114b4 --- /dev/null +++ b/Application/Source/Food Info/FoodInfoViewPresenter.swift @@ -0,0 +1,45 @@ +import RxSwift +import RxCocoa +import Action +import Presentations + +protocol FoodInfoPresentingViewModel: AnyObject, PresentingViewModel { + var foodInfoPresenter: FoodInfoPresenter? { get set } + var presentFoodInfo: Action { get } +} + +extension FoodInfoPresentingViewModel { + + /// Makes an action that is suitable to be set as the presentFoodInfo action. + /// + /// This action should be executed with a Bool indicating whether the presentation should be animated. + /// + /// - Parameter factory: A factory to be used to generate the presented view model. + /// - Parameter setupViewModel: This closure will be called with the presented view model when a present action + /// is executed. Consumers can use this to observe changes to the presented view model if necessary. + func makePresentFoodInfo( + withFactory factory: FoodInfoViewModelFactoryProtocol, + setupViewModel: ((FoodInfoViewModel) -> Void)? = nil + ) -> Action { + return makePresentAction { [weak self] animated -> DismissablePresentationContext? in + guard + let self = self, + let presenter = self.foodInfoPresenter else { + return nil + } + + let viewModel = factory.makeFoodInfoViewModel() + + setupViewModel?(viewModel) + + let presentation = presenter.foodInfoPresentation(of: viewModel) + + return DismissablePresentationContext(presentation: presentation, viewModel: viewModel, presentAnimated: animated) + } + } + +} + +protocol FoodInfoPresenter: AnyObject { + func foodInfoPresentation(of viewModel: FoodInfoViewModel) -> DismissablePresentation +} diff --git a/Application/Source/Home/Navigation/HomeNavigationController.swift b/Application/Source/Home/Navigation/HomeNavigationController.swift index 9439f6a..c2f96f8 100644 --- a/Application/Source/Home/Navigation/HomeNavigationController.swift +++ b/Application/Source/Home/Navigation/HomeNavigationController.swift @@ -39,6 +39,15 @@ extension HomeNavigationController: DetailPresenter { } +extension HomeNavigationController: FoodInfoPresenter { + + func foodInfoPresentation(of viewModel: FoodInfoViewModel) -> DismissablePresentation { + let viewController = factory.makeFoodInfoViewController(viewModel: viewModel) + return makePushPresentation(of: viewController) + } + +} + extension HomeNavigationController: SelectionPresenter { func selectionPresentation(of viewModel: SelectionViewModel) -> DismissablePresentation { diff --git a/Application/Source/Home/Navigation/HomeNavigationModel.swift b/Application/Source/Home/Navigation/HomeNavigationModel.swift index 00813ac..6ccae1b 100644 --- a/Application/Source/Home/Navigation/HomeNavigationModel.swift +++ b/Application/Source/Home/Navigation/HomeNavigationModel.swift @@ -31,5 +31,3 @@ extension HomeNavigationModelFactoryProtocol { } } - -class HomeNavigationModelFactory: HomeNavigationModelFactoryProtocol { } diff --git a/Application/Source/Home/View/HomeViewPresenter.swift b/Application/Source/Home/View/HomeViewPresenter.swift index 9d655c5..ddd2161 100644 --- a/Application/Source/Home/View/HomeViewPresenter.swift +++ b/Application/Source/Home/View/HomeViewPresenter.swift @@ -2,7 +2,7 @@ import RxSwift import Action import Presentations -protocol HomePresentingViewModel: class, PresentingViewModel { +protocol HomePresentingViewModel: AnyObject, PresentingViewModel { var homePresenter: HomePresenter? { get set } var presentHome: Action { get } } diff --git a/Application/Source/Root/RootTabBarViewModel.swift b/Application/Source/Root/RootTabBarViewModel.swift index b3f2d27..aaeab3d 100644 --- a/Application/Source/Root/RootTabBarViewModel.swift +++ b/Application/Source/Root/RootTabBarViewModel.swift @@ -1,5 +1,6 @@ import RxSwift import RxCocoa +import RxExtensions import Presentations import Core @@ -39,6 +40,9 @@ class RootTabBarModelFactory: RootTabBarModelFactoryProtocol { let themeProvider: ThemeProvider + // This can be backed by a model store. For now, use a property to represent persistent data + let foods = Property<[Food]>([.beans, .greens, .potatoes, .tomatoes]) + init(themeProvider: ThemeProvider) { self.themeProvider = themeProvider } diff --git a/Application/Source/Selection/SelectionPresenter.swift b/Application/Source/Selection/SelectionPresenter.swift index ca14de1..5eb2673 100644 --- a/Application/Source/Selection/SelectionPresenter.swift +++ b/Application/Source/Selection/SelectionPresenter.swift @@ -2,7 +2,7 @@ import RxSwift import Action import Presentations -protocol SelectionPresentingViewModel: class, PresentingViewModel { +protocol SelectionPresentingViewModel: AnyObject, PresentingViewModel { var selectionPresenter: SelectionPresenter? { get set } var presentSelection: Action { get } } @@ -40,6 +40,6 @@ extension SelectionPresentingViewModel { } -protocol SelectionPresenter: class { +protocol SelectionPresenter: AnyObject { func selectionPresentation(of viewModel: SelectionViewModel) -> DismissablePresentation } diff --git a/Application/Source/Settings/View/SettingsPresenter.swift b/Application/Source/Settings/View/SettingsPresenter.swift index a29eb21..354b65c 100644 --- a/Application/Source/Settings/View/SettingsPresenter.swift +++ b/Application/Source/Settings/View/SettingsPresenter.swift @@ -2,7 +2,7 @@ import RxSwift import Action import Presentations -protocol SettingsPresentingViewModel: class, PresentingViewModel { +protocol SettingsPresentingViewModel: AnyObject, PresentingViewModel { var settingsPresenter: SettingsPresenter? { get set } var presentSettings: Action { get } } @@ -37,6 +37,6 @@ extension SettingsPresentingViewModel { } -protocol SettingsPresenter: class { +protocol SettingsPresenter: AnyObject { func settingsPresentation(of viewModel: SettingsViewModel) -> DismissablePresentation } diff --git a/Application/Tests/DetailViewModelSpec.swift b/Application/Tests/DetailViewModelSpec.swift index 3ea3edb..4603161 100644 --- a/Application/Tests/DetailViewModelSpec.swift +++ b/Application/Tests/DetailViewModelSpec.swift @@ -3,6 +3,7 @@ import Quick import Nimble import RxSwift +import RxExtensions @testable import Application @@ -10,9 +11,10 @@ class DetailViewModelSpec: QuickSpec { override func spec() { var viewModel: DetailViewModel! + let foods = Property<[Food]>([.tomatoes, .potatoes]) beforeEach { - viewModel = DetailViewModel(selectionFactory: StubSelectionViewModelFactory()) + viewModel = DetailViewModel(foods: foods, factory: StubDetailViewModelFactory()) } describe("DetailViewModel") { @@ -81,7 +83,6 @@ class DetailViewModelSpec: QuickSpec { describe("foodListText") { it("should return a String list from the array of Contents") { - viewModel.foods.accept([.tomatoes, .potatoes]) let expected = L10n.Food.tomatoes + ", " + L10n.Food.potatoes expect(viewModel.foodListText.value).to(equal(expected)) diff --git a/Application/Tests/FoodInfoPresenterSpec.swift b/Application/Tests/FoodInfoPresenterSpec.swift new file mode 100644 index 0000000..d1ad0d3 --- /dev/null +++ b/Application/Tests/FoodInfoPresenterSpec.swift @@ -0,0 +1,45 @@ +import Quick +import Nimble +import RxSwift +import Core + +@testable import Application + +class FoodInfoPresenterSpec: QuickSpec { + override func spec() { + + describe("FoodInfoPresentingViewModel") { + describe("makePresentFoodInfo()") { + + var presentingViewModel: StubFoodInfoPresentingViewModel! + var presenter: StubFoodInfoPresenter! + + beforeEach { + presentingViewModel = StubFoodInfoPresentingViewModel() + presenter = StubFoodInfoPresenter() + + presentingViewModel.foodInfoPresenter = presenter + } + + it("should call the presenter to create a view model") { + presentingViewModel.presentFoodInfo.execute(false) + + expect(presentingViewModel.factory.makeViewModel.value).notTo(beNil()) + } + + it("should call the presenter to create a presentation context") { + presentingViewModel.presentFoodInfo.execute(false) + + expect(presenter.foodInfoPresentation.value).to(be(presentingViewModel.factory.makeViewModel.value)) + } + + it("should call the setup block") { + presentingViewModel.presentFoodInfo.execute(false) + + expect(presentingViewModel.setupViewModel.value).to(be(presentingViewModel.factory.makeViewModel.value)) + } + } + } + + } +} diff --git a/Application/Tests/FoodInfoViewModelSpec.swift b/Application/Tests/FoodInfoViewModelSpec.swift new file mode 100644 index 0000000..5a79b28 --- /dev/null +++ b/Application/Tests/FoodInfoViewModelSpec.swift @@ -0,0 +1,32 @@ +import Quick +import Nimble +import RxSwift +import RxCocoa +import RxExtensions + +@testable import Application + +class FoodInfoViewModelSpec: QuickSpec { + override func spec() { + + var viewModel: FoodInfoViewModel! + let foods = Property<[Food]>([.potatoes]) + + beforeEach { + viewModel = FoodInfoViewModel(with: foods) + } + + describe("FoodInfoViewModel") { + it("should initialize with a false isActive") { + expect(viewModel.isActive.value).to(beFalse()) + } + + describe("foods") { + it("should be set with the initial value") { + expect(viewModel.foods.value).to(equal(foods.value)) + } + } + } + + } +} diff --git a/Application/Tests/Stubs/StubDetailPresenter.swift b/Application/Tests/Stubs/StubDetailPresenter.swift index 5edd84d..ad3ed09 100644 --- a/Application/Tests/Stubs/StubDetailPresenter.swift +++ b/Application/Tests/Stubs/StubDetailPresenter.swift @@ -11,6 +11,10 @@ class StubDetailPresenter: StubSelectionPresenter { extension StubDetailPresenter: DetailPresenter { + func foodInfoPresentation(of viewModel: FoodInfoViewModel) -> DismissablePresentation { + return DismissablePresentation.stub() + } + func detailPresentation(of viewModel: DetailViewModel) -> DismissablePresentation { detailPresentation.accept(viewModel) return DismissablePresentation.stub() diff --git a/Application/Tests/Stubs/StubDetailPresentingViewModel.swift b/Application/Tests/Stubs/StubDetailPresentingViewModel.swift index 4a44ec6..239ed51 100644 --- a/Application/Tests/Stubs/StubDetailPresentingViewModel.swift +++ b/Application/Tests/Stubs/StubDetailPresentingViewModel.swift @@ -1,4 +1,5 @@ import RxCocoa +import RxExtensions @testable import Application @@ -20,10 +21,12 @@ class StubDetailPresentingViewModel: DetailPresentingViewModel { class StubDetailViewModelFactory: DetailViewModelFactoryProtocol { + let foods = Property<[Food]>([.tomatoes]) + let makeViewModel = BehaviorRelay(value: nil) func makeDetailViewModel() -> DetailViewModel { - let viewModel = DetailViewModel(selectionFactory: self) + let viewModel = DetailViewModel(foods: foods, factory: self) makeViewModel.accept(viewModel) return viewModel } diff --git a/Application/Tests/Stubs/StubFoodInfoPresenter.swift b/Application/Tests/Stubs/StubFoodInfoPresenter.swift new file mode 100644 index 0000000..ca7c372 --- /dev/null +++ b/Application/Tests/Stubs/StubFoodInfoPresenter.swift @@ -0,0 +1,19 @@ +import RxCocoa +import Presentations + +@testable import Application + +class StubFoodInfoPresenter { + + let foodInfoPresentation = BehaviorRelay(value: nil) + +} + +extension StubFoodInfoPresenter: FoodInfoPresenter { + + func foodInfoPresentation(of viewModel: FoodInfoViewModel) -> DismissablePresentation { + foodInfoPresentation.accept(viewModel) + return DismissablePresentation.stub() + } + +} diff --git a/Application/Tests/Stubs/StubFoodInfoPresentingViewModel.swift b/Application/Tests/Stubs/StubFoodInfoPresentingViewModel.swift new file mode 100644 index 0000000..ac707f2 --- /dev/null +++ b/Application/Tests/Stubs/StubFoodInfoPresentingViewModel.swift @@ -0,0 +1,34 @@ +import RxCocoa +import Presentations +import RxExtensions + +@testable import Application + +class StubFoodInfoPresentingViewModel: FoodInfoPresentingViewModel { + + let setupViewModel = BehaviorRelay(value: nil) + + weak var foodInfoPresenter: FoodInfoPresenter? + + private(set) lazy var presentFoodInfo = makePresentFoodInfo(withFactory: factory) { [unowned self] viewModel in + self.setupViewModel.accept(viewModel) + } + + let isActive = BehaviorRelay(value: false) + + let factory = StubFoodInfoViewModelFactory() + +} + +class StubFoodInfoViewModelFactory: FoodInfoViewModelFactoryProtocol { + + let foods = Property<[Food]>([.tomatoes]) + + let makeViewModel = BehaviorRelay(value: nil) + + func makeFoodInfoViewModel() -> FoodInfoViewModel { + let viewModel = FoodInfoViewModel(with: foods) + makeViewModel.accept(viewModel) + return viewModel + } +} diff --git a/Application/Tests/Stubs/StubHomePresentingViewModel.swift b/Application/Tests/Stubs/StubHomePresentingViewModel.swift index 36ba943..27f1492 100644 --- a/Application/Tests/Stubs/StubHomePresentingViewModel.swift +++ b/Application/Tests/Stubs/StubHomePresentingViewModel.swift @@ -1,4 +1,5 @@ import RxSwift +import RxExtensions import RxCocoa import Action @@ -22,6 +23,8 @@ class StubHomePresentingViewModel: HomePresentingViewModel { class StubHomeViewModelFactory: HomeViewModelFactoryProtocol { + let foods = Property<[Food]>([.tomatoes]) + let makeViewModel = BehaviorRelay(value: nil) func makeHomeViewModel() -> HomeViewModel { diff --git a/Presentations/Source/PresentationContext.swift b/Presentations/Source/PresentationContext.swift index f4237dd..4c47b44 100644 --- a/Presentations/Source/PresentationContext.swift +++ b/Presentations/Source/PresentationContext.swift @@ -2,7 +2,7 @@ import UIKit import RxSwift import RxExtensions -public protocol PresentationContext: class { +public protocol PresentationContext: AnyObject { associatedtype ViewModelType: ViewModel associatedtype PresentationType: Presentation var presentation: PresentationType { get } diff --git a/Presentations/Source/Presention.swift b/Presentations/Source/Presention.swift index c2cb13c..a52e5eb 100644 --- a/Presentations/Source/Presention.swift +++ b/Presentations/Source/Presention.swift @@ -4,7 +4,7 @@ import RxCocoa import Action import RxExtensions -public protocol Presentation: class { +public protocol Presentation: AnyObject { typealias MakePresent = (_ presentedViewController: UIViewController, _ animated: Bool) -> Completable var viewController: UIViewController { get } var present: CompletableAction { get }