Skip to content

Commit

Permalink
💲[Native Checkout] Refactor cells to be backed by view model (#693)
Browse files Browse the repository at this point in the history
* Reformat UITableViewDelegate function

* Expose label and textfield publicly

* Refactor cells to be backed by view models

* Format code

* Extract date format to a convenience getter

* Fix tests
  • Loading branch information
dusi authored and justinswart committed Aug 5, 2019
1 parent 21adf22 commit 19c49ae
Show file tree
Hide file tree
Showing 35 changed files with 533 additions and 670 deletions.
105 changes: 8 additions & 97 deletions Kickstarter-iOS/DataSources/PledgeDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,114 +10,29 @@ final class PledgeDataSource: ValueCellDataSource {
case summary
}

enum PledgeInputRow: Equatable {
case pledgeAmount(amount: Double, currencySymbol: String)
case shippingLocation(location: String, shippingCost: Double, project: Project)

var isShipping: Bool {
switch self {
case .shippingLocation: return true
default: return false
}
}

var isPledgeAmount: Bool {
switch self {
case .pledgeAmount: return true
default: return false
}
}
}

// MARK: - Load

func load(data: PledgeTableViewData) {
self.loadProjectSection(delivery: data.estimatedDelivery)

self.loadInputsSection(
amount: data.amount,
currencySymbol: data.currencySymbol,
shippingLocation: data.shippingLocation,
shippingCost: data.shippingCost,
project: data.project,
requiresShippingRules: data.requiresShippingRules
)

self.loadSummarySection(isLoggedIn: data.isLoggedIn)
}

// MARK: - Update Shipping Location Cell

func loadSelectedShippingRule(data: SelectedShippingRuleData) {
guard let shippingCellIndex = shippingCellIndexPath(),
self.numberOfItems(in: PledgeDataSource.Section.inputs.rawValue) > shippingCellIndex.row else {
return
}

self.set(
value: PledgeInputRow.shippingLocation(
location: data.location,
shippingCost: data.shippingCost,
project: data.project
),
cellClass: PledgeShippingLocationCell.self,
inSection: Section.inputs.rawValue,
row: shippingCellIndex.row
)
}

// MARK: - Index

func shippingCellIndexPath() -> IndexPath? {
let inputsRowCount = self.numberOfItems(in: PledgeDataSource.Section.inputs.rawValue)
let shippingIndexPath = IndexPath(
item: inputsRowCount - 1,
section: PledgeDataSource.Section.inputs.rawValue
)

guard self.indexPathIsShippingLocationCell(shippingIndexPath) else { return nil }

return shippingIndexPath
}

// MARK: - Private

private func loadProjectSection(delivery: String) {
func load(project: Project, reward: Reward, isLoggedIn: Bool) {
self.appendRow(
value: delivery,
value: reward,
cellClass: PledgeDescriptionCell.self,
toSection: Section.project.rawValue
)
}

private func loadInputsSection(
amount: Double,
currencySymbol: String,
shippingLocation: String,
shippingCost: Double,
project: Project,
requiresShippingRules: Bool
) {
self.appendRow(
value: PledgeInputRow.pledgeAmount(amount: amount, currencySymbol: currencySymbol),
value: (project, reward),
cellClass: PledgeAmountCell.self,
toSection: Section.inputs.rawValue
)

if requiresShippingRules {
if reward.shipping.enabled {
self.appendRow(
value: PledgeInputRow.shippingLocation(
location: shippingLocation,
shippingCost: shippingCost,
project: project
),
value: (project, reward),
cellClass: PledgeShippingLocationCell.self,
toSection: Section.inputs.rawValue
)
}
}

private func loadSummarySection(isLoggedIn: Bool) {
self.appendRow(
value: Strings.Total(),
cellClass: PledgeRowCell.self,
Expand All @@ -133,21 +48,17 @@ final class PledgeDataSource: ValueCellDataSource {
}
}

private func indexPathIsShippingLocationCell(_ indexPath: IndexPath) -> Bool {
return (self[indexPath] as? PledgeInputRow)?.isShipping == true
}

override func configureCell(tableCell cell: UITableViewCell, withValue value: Any) {
switch (cell, value) {
case let (cell as PledgeAmountCell, value as PledgeInputRow):
case let (cell as PledgeAmountCell, value as (Project, Reward)):
cell.configureWith(value: value)
case let (cell as PledgeContinueCell, value as ()):
cell.configureWith(value: value)
case let (cell as PledgeDescriptionCell, value as String):
case let (cell as PledgeDescriptionCell, value as Reward):
cell.configureWith(value: value)
case let (cell as PledgeRowCell, value as String):
cell.configureWith(value: value)
case let (cell as PledgeShippingLocationCell, value as PledgeInputRow):
case let (cell as PledgeShippingLocationCell, value as (Project, Reward)):
cell.configureWith(value: value)
case let (cell as PledgeContinueCell, value as ()):
cell.configureWith(value: value)
Expand Down
125 changes: 23 additions & 102 deletions Kickstarter-iOS/DataSources/PledgeDataSourceTests.swift
Original file line number Diff line number Diff line change
@@ -1,57 +1,42 @@
@testable import Kickstarter_Framework
@testable import KsApi
@testable import Library
import Prelude
import XCTest

// swiftlint:disable line_length

final class PledgeDataSourceTests: XCTestCase {
let dataSource = PledgeDataSource()
let tableView = UITableView(frame: .zero, style: .plain)

// swiftlint:disable line_length
func testLoad_loggedIn() {
let data = PledgeTableViewData(
amount: 100, currencySymbol: "$", estimatedDelivery: "May 2020",
shippingLocation: "", shippingCost: 0.0, project: Project.template,
isLoggedIn: true, requiresShippingRules: true
)
self.dataSource.load(data: data)
func testLoad_LoggedIn() {
self.dataSource.load(project: .template, reward: .template, isLoggedIn: true)

XCTAssertEqual(3, self.dataSource.numberOfSections(in: self.tableView))
XCTAssertEqual(1, self.dataSource.tableView(self.tableView, numberOfRowsInSection: 0))
XCTAssertEqual(2, self.dataSource.tableView(self.tableView, numberOfRowsInSection: 1))
XCTAssertEqual(1, self.dataSource.tableView(self.tableView, numberOfRowsInSection: 1))
XCTAssertEqual(1, self.dataSource.tableView(self.tableView, numberOfRowsInSection: 2))
XCTAssertEqual(PledgeDescriptionCell.defaultReusableId, self.dataSource.reusableId(item: 0, section: 0))
XCTAssertEqual(PledgeAmountCell.defaultReusableId, self.dataSource.reusableId(item: 0, section: 1))
XCTAssertEqual(PledgeShippingLocationCell.defaultReusableId, self.dataSource.reusableId(item: 1, section: 1))
XCTAssertEqual(PledgeRowCell.defaultReusableId, self.dataSource.reusableId(item: 0, section: 2))
}

func testLoad_loggedOut() {
let data = PledgeTableViewData(
amount: 100, currencySymbol: "$", estimatedDelivery: "May 2020",
shippingLocation: "", shippingCost: 0.0, project: Project.template,
isLoggedIn: false, requiresShippingRules: true
)
self.dataSource.load(data: data)
func testLoad_LoggedOut() {
self.dataSource.load(project: .template, reward: .template, isLoggedIn: false)

XCTAssertEqual(3, self.dataSource.numberOfSections(in: self.tableView))
XCTAssertEqual(1, self.dataSource.tableView(self.tableView, numberOfRowsInSection: 0))
XCTAssertEqual(2, self.dataSource.tableView(self.tableView, numberOfRowsInSection: 1))
XCTAssertEqual(1, self.dataSource.tableView(self.tableView, numberOfRowsInSection: 1))
XCTAssertEqual(2, self.dataSource.tableView(self.tableView, numberOfRowsInSection: 2))
XCTAssertEqual(PledgeDescriptionCell.defaultReusableId, self.dataSource.reusableId(item: 0, section: 0))
XCTAssertEqual(PledgeAmountCell.defaultReusableId, self.dataSource.reusableId(item: 0, section: 1))
XCTAssertEqual(PledgeShippingLocationCell.defaultReusableId, self.dataSource.reusableId(item: 1, section: 1))
XCTAssertEqual(PledgeRowCell.defaultReusableId, self.dataSource.reusableId(item: 0, section: 2))
XCTAssertEqual(PledgeContinueCell.defaultReusableId, self.dataSource.reusableId(item: 1, section: 2))
}

func testLoad_requiresShippingRules_isFalse() {
let data = PledgeTableViewData(
amount: 100, currencySymbol: "$", estimatedDelivery: "May 2020",
shippingLocation: "", shippingCost: 0.0, project: Project.template,
isLoggedIn: false, requiresShippingRules: false
)
self.dataSource.load(data: data)
func testLoad_Shipping_Disabled() {
self.dataSource.load(project: .template, reward: .template, isLoggedIn: false)

XCTAssertEqual(3, self.dataSource.numberOfSections(in: self.tableView))
XCTAssertEqual(1, self.dataSource.tableView(self.tableView, numberOfRowsInSection: PledgeDataSource.Section.project.rawValue))
Expand All @@ -63,84 +48,20 @@ final class PledgeDataSourceTests: XCTestCase {
XCTAssertEqual(PledgeContinueCell.defaultReusableId, self.dataSource.reusableId(item: 1, section: PledgeDataSource.Section.summary.rawValue))
}

func testLoadSelectedShippingRule_requiresShipping_isTrue() {
let data = PledgeTableViewData(
amount: 100, currencySymbol: "$", estimatedDelivery: "May 2020",
shippingLocation: "", shippingCost: 0.0, project: Project.template,
isLoggedIn: false, requiresShippingRules: true
)
let selectedShippingData = SelectedShippingRuleData(
location: "Brooklyn", shippingCost: 1.0,
project: Project.template
)

let indexPath = IndexPath(item: 1, section: PledgeDataSource.Section.inputs.rawValue)

let initialShippingCellData = PledgeDataSource.PledgeInputRow.shippingLocation(
location: "",
shippingCost: 0.0,
project: .template
)

self.dataSource.load(data: data)
func testLoad_Shipping_Enabled() {
let shipping = Reward.Shipping.template |> Reward.Shipping.lens.enabled .~ true
let reward = Reward.template |> Reward.lens.shipping .~ shipping

XCTAssertEqual(initialShippingCellData, self.dataSource[indexPath] as? PledgeDataSource.PledgeInputRow)
self.dataSource.load(project: .template, reward: reward, isLoggedIn: false)

self.dataSource.loadSelectedShippingRule(data: selectedShippingData)

let shippingCellData = PledgeDataSource.PledgeInputRow.shippingLocation(
location: "Brooklyn",
shippingCost: 1.0,
project: .template
)

XCTAssertEqual(shippingCellData, self.dataSource[indexPath] as? PledgeDataSource.PledgeInputRow)
XCTAssertEqual(3, self.dataSource.numberOfSections(in: self.tableView))
XCTAssertEqual(1, self.dataSource.tableView(self.tableView, numberOfRowsInSection: PledgeDataSource.Section.project.rawValue))
XCTAssertEqual(2, self.dataSource.tableView(self.tableView, numberOfRowsInSection: PledgeDataSource.Section.inputs.rawValue))
XCTAssertEqual(2, self.dataSource.tableView(self.tableView, numberOfRowsInSection: PledgeDataSource.Section.summary.rawValue))
XCTAssertEqual(PledgeDescriptionCell.defaultReusableId, self.dataSource.reusableId(item: 0, section: PledgeDataSource.Section.project.rawValue))
XCTAssertEqual(PledgeAmountCell.defaultReusableId, self.dataSource.reusableId(item: 0, section: PledgeDataSource.Section.inputs.rawValue))
XCTAssertEqual(PledgeShippingLocationCell.defaultReusableId, self.dataSource.reusableId(item: 1, section: PledgeDataSource.Section.inputs.rawValue))
XCTAssertEqual(PledgeRowCell.defaultReusableId, self.dataSource.reusableId(item: 0, section: PledgeDataSource.Section.summary.rawValue))
XCTAssertEqual(PledgeContinueCell.defaultReusableId, self.dataSource.reusableId(item: 1, section: PledgeDataSource.Section.summary.rawValue))
}

func testLoadSelectedShippingRule_requiresShipping_isFalse() {
let data = PledgeTableViewData(
amount: 100, currencySymbol: "$", estimatedDelivery: "May 2020",
shippingLocation: "", shippingCost: 0.0, project: Project.template,
isLoggedIn: false, requiresShippingRules: false
)
let selectedShippingData = SelectedShippingRuleData(
location: "Brooklyn", shippingCost: 1.0,
project: Project.template
)

self.dataSource.load(data: data)
self.dataSource.loadSelectedShippingRule(data: selectedShippingData)

XCTAssertEqual(1, self.dataSource.tableView(self.tableView, numberOfRowsInSection: PledgeDataSource.Section.inputs.rawValue))
}

func testShippingCellIndexPath_isNil() {
let data = PledgeTableViewData(
amount: 100, currencySymbol: "$", estimatedDelivery: "May 2020",
shippingLocation: "", shippingCost: 0.0, project: Project.template,
isLoggedIn: false, requiresShippingRules: false
)

self.dataSource.load(data: data)

XCTAssertNil(self.dataSource.shippingCellIndexPath())
}

func testShippingCellIndexPath_isNotNil() {
let data = PledgeTableViewData(
amount: 100, currencySymbol: "$", estimatedDelivery: "May 2020",
shippingLocation: "", shippingCost: 0.0, project: Project.template,
isLoggedIn: false, requiresShippingRules: true
)

self.dataSource.load(data: data)

let indexPath = self.dataSource.shippingCellIndexPath()

XCTAssertNotNil(indexPath)
XCTAssertEqual(indexPath, IndexPath(item: 1, section: PledgeDataSource.Section.inputs.rawValue))
}

// swiftlint:enable line_length
}
4 changes: 2 additions & 2 deletions Kickstarter-iOS/Views/AmountInputView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import UIKit
class AmountInputView: UIView {
// MARK: - Properties

private lazy var label: UILabel = { UILabel(frame: .zero) }()
private lazy var textField: UITextField = { UITextField(frame: .zero) }()
private(set) lazy var label: UILabel = { UILabel(frame: .zero) }()
private(set) lazy var textField: UITextField = { UITextField(frame: .zero) }()
private lazy var stackView: UIStackView = { UIStackView(frame: .zero) }()
private var labelCenterYAnchor: NSLayoutConstraint?

Expand Down
26 changes: 16 additions & 10 deletions Kickstarter-iOS/Views/Cells/PledgeAmountCell.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import KsApi
import Library
import Prelude
import Prelude_UIKit
Expand All @@ -6,6 +7,8 @@ import UIKit
final class PledgeAmountCell: UITableViewCell, ValueCell {
// MARK: - Properties

private let viewModel = PledgeAmountCellViewModel()

private lazy var adaptableStackView: UIStackView = { UIStackView(frame: .zero) }()
private lazy var amountInputView: AmountInputView = { AmountInputView(frame: .zero) }()
private lazy var titleLabel: UILabel = { UILabel(frame: .zero) }()
Expand Down Expand Up @@ -36,6 +39,8 @@ final class PledgeAmountCell: UITableViewCell, ValueCell {
|> ksr_addArrangedSubviewsToStackView()

self.spacer.widthAnchor.constraint(greaterThanOrEqualToConstant: Styles.grid(3)).isActive = true

self.bindViewModel()
}

required init?(coder _: NSCoder) {
Expand Down Expand Up @@ -68,18 +73,19 @@ final class PledgeAmountCell: UITableViewCell, ValueCell {
|> stepperStyle
}

// MARK: - Configuration
// MARK: - Binding

override func bindViewModel() {
super.bindViewModel()

func configureWith(value: PledgeDataSource.PledgeInputRow) {
guard case let .pledgeAmount(amount, currencySymbol) = value else {
return
}
self.amountInputView.label.rac.text = self.viewModel.outputs.currency
self.amountInputView.textField.rac.text = self.viewModel.outputs.amount
}

// MARK: - Configuration

self.amountInputView.configureWith(
amount: String(format: "%i", amount),
placeholder: "\(0)",
currency: currencySymbol
)
func configureWith(value: (project: Project, reward: Reward)) {
self.viewModel.inputs.configureWith(project: value.project, reward: value.reward)
}
}

Expand Down
6 changes: 4 additions & 2 deletions Kickstarter-iOS/Views/Cells/PledgeDescriptionCell.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import KsApi
import Library
import Prelude
import UIKit
Expand Down Expand Up @@ -44,6 +45,7 @@ final class PledgeDescriptionCell: UITableViewCell, ValueCell {
super.init(style: style, reuseIdentifier: reuseIdentifier)

self.configureSubviews()

self.bindViewModel()
}

Expand Down Expand Up @@ -144,8 +146,8 @@ final class PledgeDescriptionCell: UITableViewCell, ValueCell {

// MARK: - Configuration

internal func configureWith(value: String) {
self.viewModel.inputs.configureWith(estimatedDeliveryDate: value)
internal func configureWith(value: Reward) {
self.viewModel.inputs.configureWith(reward: value)
}
}

Expand Down
Loading

0 comments on commit 19c49ae

Please sign in to comment.