Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Basic Food Table View #66

Merged
merged 11 commits into from
Feb 25, 2019
Original file line number Diff line number Diff line change
Expand Up @@ -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 { }
5 changes: 3 additions & 2 deletions Application/Source/Detail/View/DetailPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bool, DetailViewModel> { get }
}
Expand Down Expand Up @@ -30,6 +30,7 @@ extension DetailPresentingViewModel {
let viewModel = factory.makeDetailViewModel()

viewModel.selectionPresenter = presenter
viewModel.foodInfoPresenter = presenter

setupViewModel?(viewModel)

Expand All @@ -41,6 +42,6 @@ extension DetailPresentingViewModel {

}

protocol DetailPresenter: SelectionPresenter {
protocol DetailPresenter: SelectionPresenter, FoodInfoPresenter {
func detailPresentation(of viewModel: DetailViewModel) -> DismissablePresentation
}
8 changes: 6 additions & 2 deletions Application/Source/Detail/View/DetailViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -69,7 +69,7 @@ class DetailViewController: UIViewController, ViewController {

}

protocol DetailViewControllerFactoryProtocol: SelectionViewControllerFactoryProtocol {
protocol DetailViewControllerFactoryProtocol: SelectionViewControllerFactoryProtocol, FoodInfoViewControllerFactoryProtocol {
var themeProvider: ThemeProvider { get }

func makeDetailViewController(viewModel: DetailViewModel) -> DetailViewController
Expand All @@ -81,4 +81,8 @@ extension DetailViewControllerFactoryProtocol {
return DetailViewController(viewModel: viewModel, themeProvider: themeProvider)
}

func makeFoodInfoViewController(viewModel: FoodInfoViewModel) -> FoodInfoViewController {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe this is needed. This function should be implemented by the protocol extension.

return FoodInfoViewController(viewModel: viewModel, themeProvider: themeProvider)
}

}
32 changes: 20 additions & 12 deletions Application/Source/Detail/View/DetailViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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<String> = {
let observable = foods.map { foods -> String in
Expand All @@ -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
},
Expand All @@ -50,24 +48,34 @@ 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<String?>(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 {

// This can be backed by a datastore. For now, use a computed property to represent persistent data
var foods: Property<[Food]> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this should just be defined on the class that implements DetailViewModelFactoryProtocol (DetailNavigationModelFactory). Even though this is temporary, it's odd that it would instantiate the data model in a protocol extension. It could look like this (in DetailNavigationModel.swift):

class DetailNavigationModelFactory: DetailNavigationModelFactoryProtocol {
    let foods = Property([.beans, .greens, .potatoes, .tomatoes])
}

I believe one of the Stubs would also need to be updated.

return Property([.beans, .greens, .potatoes, .tomatoes])
}

func makeDetailViewModel() -> DetailViewModel {
return DetailViewModel(selectionFactory: self)
return DetailViewModel(foods: foods, factory: self)
}

}
61 changes: 61 additions & 0 deletions Application/Source/Food Info/FoodInfoViewController.swift
Original file line number Diff line number Diff line change
@@ -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)
}

}
16 changes: 16 additions & 0 deletions Application/Source/Food Info/FoodInfoViewControllerStyle.swift
Original file line number Diff line number Diff line change
@@ -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) {
}

}
37 changes: 37 additions & 0 deletions Application/Source/Food Info/FoodInfoViewModel.swift
Original file line number Diff line number Diff line change
@@ -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)
}

}
45 changes: 45 additions & 0 deletions Application/Source/Food Info/FoodInfoViewPresenter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import RxSwift
import RxCocoa
import Action
import Presentations

protocol FoodInfoPresentingViewModel: AnyObject, PresentingViewModel {
var foodInfoPresenter: FoodInfoPresenter? { get set }
var presentFoodInfo: Action<Bool, FoodInfoViewModel> { 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<Bool, FoodInfoViewModel> {
return makePresentAction { [weak self] animated -> DismissablePresentationContext<FoodInfoViewModel>? 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion Application/Source/Home/View/HomeViewPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bool, HomeViewModel> { get }
}
Expand Down
4 changes: 2 additions & 2 deletions Application/Source/Selection/SelectionPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bool, SelectionViewModel> { get }
}
Expand Down Expand Up @@ -40,6 +40,6 @@ extension SelectionPresentingViewModel {

}

protocol SelectionPresenter: class {
protocol SelectionPresenter: AnyObject {
func selectionPresentation(of viewModel: SelectionViewModel) -> DismissablePresentation
}
4 changes: 2 additions & 2 deletions Application/Source/Settings/View/SettingsPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bool, SettingsViewModel> { get }
}
Expand Down Expand Up @@ -37,6 +37,6 @@ extension SettingsPresentingViewModel {

}

protocol SettingsPresenter: class {
protocol SettingsPresenter: AnyObject {
func settingsPresentation(of viewModel: SettingsViewModel) -> DismissablePresentation
}
5 changes: 3 additions & 2 deletions Application/Tests/DetailViewModelSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@
import Quick
import Nimble
import RxSwift
import RxExtensions

@testable import Application

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") {
Expand Down Expand Up @@ -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))
Expand Down
Loading