Skip to content

Commit

Permalink
Add Basic Food Table View (#66)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
lokae0 authored Feb 25, 2019
1 parent d423271 commit a0e3b73
Show file tree
Hide file tree
Showing 25 changed files with 352 additions and 30 deletions.
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 { }
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,3 @@ extension DetailNavigationModelFactoryProtocol {
}

}

class DetailNavigationModelFactory: DetailNavigationModelFactoryProtocol { }
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
}
4 changes: 2 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 Down
27 changes: 15 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,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<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 {

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: 0 additions & 2 deletions Application/Source/Home/Navigation/HomeNavigationModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,3 @@ extension HomeNavigationModelFactoryProtocol {
}

}

class HomeNavigationModelFactory: HomeNavigationModelFactoryProtocol { }
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: 4 additions & 0 deletions Application/Source/Root/RootTabBarViewModel.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import RxSwift
import RxCocoa
import RxExtensions
import Presentations
import Core

Expand Down Expand Up @@ -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
}
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
}
Loading

0 comments on commit a0e3b73

Please sign in to comment.