Skip to content

Commit

Permalink
Food info / contents feature (#68)
Browse files Browse the repository at this point in the history
* 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

* 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

* 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
  • Loading branch information
lokae0 authored Jun 13, 2019
1 parent b0a474e commit 365cdea
Show file tree
Hide file tree
Showing 44 changed files with 561 additions and 85 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 { }
9 changes: 5 additions & 4 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 All @@ -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
Expand All @@ -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
}
51 changes: 30 additions & 21 deletions Application/Source/Detail/View/DetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +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 foodListTitle = UILabel()
let foodList = UILabel()
let foodInfoButton = UIButton()

let button: UIButton = {
let button = UIButton()
button.setTitleColor(.blue, for: .normal)
return button
private(set) lazy var selectionStackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [
title,
button,
selectionResult
])
stackView.axis = .vertical
stackView.alignment = .center
return stackView
}()

let selectionResult: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.numberOfLines = 0
return label
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,
selectionStackView,
foodStackView
])
stackView.axis = .vertical
stackView.alignment = .center
Expand All @@ -39,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)
}
}
Expand Down
13 changes: 12 additions & 1 deletion Application/Source/Detail/View/DetailViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,18 @@ class DetailViewController: UIViewController, ViewController {
.bind(to: detailView.selectionResult.rx.text)
.disposed(by: disposeBag)

viewModel.foodListTitle
.bind(to: detailView.foodListTitle.rx.text)
.disposed(by: disposeBag)
viewModel.foodListText
.bind(to: detailView.foodList.rx.text)
.disposed(by: disposeBag)
viewModel.foodInfoButtonTitle
.bind(to: detailView.foodInfoButton.rx.title())
.disposed(by: disposeBag)

detailView.button.rx.bind(to: viewModel.presentSelection, input: true)
detailView.foodInfoButton.rx.bind(to: viewModel.presentFoodInfo, input: true)

rx.isAppeared
.bind(to: viewModel.isActive)
Expand All @@ -58,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
25 changes: 17 additions & 8 deletions Application/Source/Detail/View/DetailViewControllerStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +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)
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.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
}

}
37 changes: 30 additions & 7 deletions Application/Source/Detail/View/DetailViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import RxSwift
import RxCocoa
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 @@ -16,8 +18,24 @@ class DetailViewModel: ViewModel, SelectionPresentingViewModel {

let presentSelectionTitle = Property(L10n.Detail.Select.title)

let foodListTitle = Property(L10n.Detail.FoodList.title)
let foodInfoButtonTitle = Property(L10n.Detail.FoodButton.title)

private(set) lazy var presentFoodInfo = makePresentFoodInfo(withFactory: factory)

private let foods: Property<[Food]>

private(set) lazy var foodListText: Property<String> = {
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,
withFactory: factory,
defaultValue: { [weak self] in
return self?.selectionResult.value
},
Expand All @@ -30,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)
}

}
Loading

0 comments on commit 365cdea

Please sign in to comment.