parent
8ae63b12f8
commit
9df873320c
@ -0,0 +1,21 @@ |
||||
{ |
||||
"images" : [ |
||||
{ |
||||
"idiom" : "universal", |
||||
"scale" : "1x" |
||||
}, |
||||
{ |
||||
"idiom" : "universal", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"idiom" : "universal", |
||||
"filename" : "time@3x.png", |
||||
"scale" : "3x" |
||||
} |
||||
], |
||||
"info" : { |
||||
"version" : 1, |
||||
"author" : "xcode" |
||||
} |
||||
} |
After Width: | Height: | Size: 2.2 KiB |
@ -0,0 +1,55 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import QRCodeReaderViewController |
||||
|
||||
protocol ScanQRCodeForWalletAddressToSellTicketCoordinatorDelegate: class { |
||||
func scanned(walletAddress: String, in coordinator: ScanQRCodeForWalletAddressToSellTicketCoordinator) |
||||
func cancelled(in coordinator: ScanQRCodeForWalletAddressToSellTicketCoordinator) |
||||
} |
||||
|
||||
class ScanQRCodeForWalletAddressToSellTicketCoordinator: NSObject, Coordinator { |
||||
var coordinators: [Coordinator] = [] |
||||
var ticketHolder: TicketHolder |
||||
var viewController: UIViewController |
||||
var linkExpiryDate: Date |
||||
var ethCost: String |
||||
var dollarCost: String |
||||
var paymentFlow: PaymentFlow |
||||
weak var delegate: ScanQRCodeForWalletAddressToSellTicketCoordinatorDelegate? |
||||
|
||||
init(ticketHolder: TicketHolder, linkExpiryDate: Date, ethCost: String, dollarCost: String, paymentFlow: PaymentFlow, in viewController: UIViewController) { |
||||
self.ticketHolder = ticketHolder |
||||
self.linkExpiryDate = linkExpiryDate |
||||
self.ethCost = ethCost |
||||
self.dollarCost = dollarCost |
||||
self.paymentFlow = paymentFlow |
||||
self.viewController = viewController |
||||
} |
||||
|
||||
func start() { |
||||
let controller = QRCodeReaderViewController() |
||||
controller.delegate = self |
||||
viewController.present(controller, animated: true, completion: nil) |
||||
} |
||||
} |
||||
|
||||
extension ScanQRCodeForWalletAddressToSellTicketCoordinator: QRCodeReaderDelegate { |
||||
func readerDidCancel(_ reader: QRCodeReaderViewController!) { |
||||
reader.stopScanning() |
||||
reader.dismiss(animated: true) { [weak self] in |
||||
if let celf = self { |
||||
celf.delegate?.cancelled(in: celf) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func reader(_ reader: QRCodeReaderViewController!, didScanResult result: String!) { |
||||
reader.stopScanning() |
||||
reader.dismiss(animated: true) { [weak self] in |
||||
if let celf = self { |
||||
celf.delegate?.scanned(walletAddress: result, in: celf) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,151 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import TrustKeystore |
||||
import BigInt |
||||
|
||||
protocol SellTicketsCoordinatorDelegate: class { |
||||
func didClose(in coordinator: SellTicketsCoordinator) |
||||
func didFinishSuccessfully(in coordinator: SellTicketsCoordinator) |
||||
func didFail(in coordinator: SellTicketsCoordinator) |
||||
} |
||||
|
||||
class SellTicketsCoordinator: Coordinator { |
||||
var coordinators: [Coordinator] = [] |
||||
var ticketHolder: TicketHolder |
||||
var linkExpiryDate: Date |
||||
var ethCost: String |
||||
var dollarCost: String |
||||
var walletAddress: String |
||||
var paymentFlow: PaymentFlow |
||||
var keystore: Keystore |
||||
var session: WalletSession |
||||
var account: Account |
||||
var viewController: UIViewController |
||||
var statusViewController: StatusViewController? |
||||
weak var delegate: SellTicketsCoordinatorDelegate? |
||||
var status = StatusViewControllerViewModel.State.processing { |
||||
didSet { |
||||
statusViewController?.configure(viewModel: .init( |
||||
state: status, |
||||
inProgressText: R.string.localizable.aClaimTicketInProgressTitle(), |
||||
succeededTextText: R.string.localizable.aClaimTicketSuccessTitle(), |
||||
failedText: R.string.localizable.aClaimTicketFailedTitle() |
||||
)) |
||||
} |
||||
} |
||||
|
||||
init(ticketHolder: TicketHolder, linkExpiryDate: Date, ethCost: String, dollarCost: String, walletAddress: String, paymentFlow: PaymentFlow, keystore: Keystore, session: WalletSession, account: Account, on viewController: UIViewController) { |
||||
self.ticketHolder = ticketHolder |
||||
self.linkExpiryDate = linkExpiryDate |
||||
self.ethCost = ethCost |
||||
self.dollarCost = dollarCost |
||||
self.walletAddress = walletAddress |
||||
self.paymentFlow = paymentFlow |
||||
self.keystore = keystore |
||||
self.session = session |
||||
self.account = account |
||||
self.viewController = viewController |
||||
} |
||||
|
||||
func start() { |
||||
guard let address = validateAddress() else { return } |
||||
showProgressViewController() |
||||
sell(address: address) |
||||
} |
||||
|
||||
private func showProgressViewController() { |
||||
statusViewController = StatusViewController() |
||||
if let vc = statusViewController { |
||||
vc.delegate = self |
||||
vc.configure(viewModel: .init( |
||||
state: .processing, |
||||
inProgressText: R.string.localizable.aWalletTicketTokenSellInProgressTitle(), |
||||
succeededTextText: R.string.localizable.aWalletTicketTokenSellSuccessTitle(), |
||||
failedText: R.string.localizable.aWalletTicketTokenSellFailedTitle() |
||||
)) |
||||
vc.modalPresentationStyle = .overCurrentContext |
||||
viewController.present(vc, animated: true) |
||||
} |
||||
} |
||||
|
||||
//TODO code is for transfers. Convert to sell |
||||
private func sell(address: Address) { |
||||
if case .send(let transferType) = paymentFlow { |
||||
let transaction = UnconfirmedTransaction( |
||||
transferType: transferType, |
||||
value: BigInt(0), |
||||
to: address, |
||||
data: Data(), |
||||
gasLimit: .none, |
||||
gasPrice: nil, |
||||
nonce: .none, |
||||
v: .none, |
||||
r: .none, |
||||
s: .none, |
||||
expiry: .none, |
||||
indices: ticketHolder.ticketIndices |
||||
) |
||||
|
||||
let configurator = TransactionConfigurator( |
||||
session: session, |
||||
account: account, |
||||
transaction: transaction |
||||
) |
||||
configurator.load { [weak self] result in |
||||
guard let `self` = self else { return } |
||||
switch result { |
||||
case .success: |
||||
self.sendTransaction(with: configurator) |
||||
case .failure(let error): |
||||
self.processFailed() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
//TODO code is for transfers. Convert to sell (if necessary) |
||||
private func sendTransaction(with configurator: TransactionConfigurator) { |
||||
let unsignedTransaction = configurator.formUnsignedTransaction() |
||||
let sendTransactionCoordinator = SendTransactionCoordinator( |
||||
session: session, |
||||
keystore: keystore, |
||||
confirmType: .signThenSend) |
||||
sendTransactionCoordinator.send(transaction: unsignedTransaction) { [weak self] result in |
||||
if let celf = self { |
||||
switch result { |
||||
case .success(let type): |
||||
celf.processSuccessful() |
||||
case .failure(let error): |
||||
celf.processFailed() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private func processSuccessful() { |
||||
status = .succeeded |
||||
} |
||||
|
||||
private func processFailed() { |
||||
status = .failed |
||||
} |
||||
|
||||
private func validateAddress() -> Address? { |
||||
return Address(string: walletAddress) |
||||
} |
||||
} |
||||
|
||||
extension SellTicketsCoordinator: StatusViewControllerDelegate { |
||||
func didPressDone(in viewController: StatusViewController) { |
||||
viewController.dismiss(animated: false) |
||||
switch status { |
||||
case .processing: |
||||
delegate?.didClose(in: self) |
||||
case .succeeded: |
||||
delegate?.didFinishSuccessfully(in: self) |
||||
case .failed: |
||||
delegate?.didFail(in: self) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,149 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import UIKit |
||||
import MessageUI |
||||
|
||||
enum TicketSellMode { |
||||
case walletAddressTextEntry |
||||
case walletAddressFromQRCode |
||||
case other |
||||
} |
||||
|
||||
protocol ChooseTicketSellModeViewControllerDelegate: class { |
||||
func didChoose(sellMode: TicketSellMode, in viewController: ChooseTicketSellModeViewController) |
||||
} |
||||
|
||||
class ChooseTicketSellModeViewController: UIViewController { |
||||
//roundedBackground is used to achieve the top 2 rounded corners-only effect since maskedCorners to not round bottom corners is not available in iOS 10 |
||||
let roundedBackground = UIView() |
||||
let titleLabel = UILabel() |
||||
let inputWalletAddressButton = ShareModeButton() |
||||
let qrCodeScannerButton = ShareModeButton() |
||||
let otherButton = ShareModeButton() |
||||
let ticketHolder: TicketHolder |
||||
var ethCost: String |
||||
var dollarCost: String |
||||
var linkExpiryDate: Date |
||||
var paymentFlow: PaymentFlow |
||||
weak var delegate: ChooseTicketSellModeViewControllerDelegate? |
||||
|
||||
init(ticketHolder: TicketHolder, linkExpiryDate: Date, ethCost: String, dollarCost: String, paymentFlow: PaymentFlow) { |
||||
self.ticketHolder = ticketHolder |
||||
self.linkExpiryDate = linkExpiryDate |
||||
self.ethCost = ethCost |
||||
//TODO if we only need ethCost, remove dollarCost? |
||||
self.dollarCost = dollarCost |
||||
self.paymentFlow = paymentFlow |
||||
|
||||
super.init(nibName: nil, bundle: nil) |
||||
view.backgroundColor = Colors.appBackground |
||||
|
||||
roundedBackground.translatesAutoresizingMaskIntoConstraints = false |
||||
roundedBackground.cornerRadius = 20 |
||||
view.addSubview(roundedBackground) |
||||
|
||||
inputWalletAddressButton.callback = { |
||||
self.delegate?.didChoose(sellMode: .walletAddressTextEntry, in: self) |
||||
} |
||||
inputWalletAddressButton.translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
qrCodeScannerButton.callback = { |
||||
self.delegate?.didChoose(sellMode: .walletAddressFromQRCode, in: self) |
||||
} |
||||
qrCodeScannerButton.translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
otherButton.callback = { |
||||
self.delegate?.didChoose(sellMode: .other, in: self) |
||||
} |
||||
otherButton.translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
let buttonRow1 = UIStackView(arrangedSubviews: [ |
||||
inputWalletAddressButton, |
||||
qrCodeScannerButton, |
||||
]) |
||||
buttonRow1.translatesAutoresizingMaskIntoConstraints = false |
||||
buttonRow1.axis = .horizontal |
||||
buttonRow1.spacing = 12 |
||||
buttonRow1.distribution = .fill |
||||
|
||||
let buttonPlaceholder = UIView() |
||||
let buttonRow2 = UIStackView(arrangedSubviews: [ |
||||
otherButton, |
||||
buttonPlaceholder, |
||||
]) |
||||
buttonRow2.translatesAutoresizingMaskIntoConstraints = false |
||||
buttonRow2.axis = .horizontal |
||||
buttonRow2.spacing = 12 |
||||
buttonRow2.distribution = .fill |
||||
|
||||
let stackView = UIStackView(arrangedSubviews: [ |
||||
.spacer(height: 7), |
||||
titleLabel, |
||||
.spacer(height: 20), |
||||
buttonRow1, |
||||
.spacer(height: 12), |
||||
buttonRow2, |
||||
]) |
||||
stackView.translatesAutoresizingMaskIntoConstraints = false |
||||
stackView.axis = .vertical |
||||
stackView.spacing = 0 |
||||
stackView.distribution = .fill |
||||
roundedBackground.addSubview(stackView) |
||||
|
||||
let marginToHideBottomRoundedCorners = CGFloat(30) |
||||
NSLayoutConstraint.activate([ |
||||
otherButton.widthAnchor.constraint(equalTo: inputWalletAddressButton.widthAnchor), |
||||
otherButton.heightAnchor.constraint(equalTo: inputWalletAddressButton.heightAnchor), |
||||
otherButton.widthAnchor.constraint(equalTo: buttonPlaceholder.widthAnchor), |
||||
otherButton.heightAnchor.constraint(equalTo: buttonPlaceholder.heightAnchor), |
||||
|
||||
stackView.leadingAnchor.constraint(equalTo: roundedBackground.leadingAnchor, constant: 30), |
||||
stackView.trailingAnchor.constraint(equalTo: roundedBackground.trailingAnchor, constant: -30), |
||||
stackView.topAnchor.constraint(equalTo: roundedBackground.topAnchor, constant: 16), |
||||
|
||||
roundedBackground.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
roundedBackground.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
roundedBackground.topAnchor.constraint(equalTo: view.topAnchor), |
||||
roundedBackground.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: marginToHideBottomRoundedCorners), |
||||
]) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
func configure(viewModel: ChooseTicketSellModeViewControllerViewModel) { |
||||
roundedBackground.backgroundColor = viewModel.contentsBackgroundColor |
||||
roundedBackground.layer.cornerRadius = 20 |
||||
|
||||
titleLabel.numberOfLines = 0 |
||||
titleLabel.textColor = viewModel.titleColor |
||||
titleLabel.font = viewModel.titleFont |
||||
titleLabel.textAlignment = .center |
||||
titleLabel.text = viewModel.titleLabelText |
||||
|
||||
inputWalletAddressButton.title = viewModel.inputWalletAddressButtonTitle |
||||
inputWalletAddressButton.image = viewModel.inputWalletAddressButtonImage |
||||
|
||||
qrCodeScannerButton.title = viewModel.qrCodeScannerButtonTitle |
||||
qrCodeScannerButton.image = viewModel.qrCodeScannerButtonImage |
||||
|
||||
otherButton.title = viewModel.otherButtonTitle |
||||
otherButton.image = viewModel.otherButtonImage |
||||
|
||||
inputWalletAddressButton.label.font = viewModel.buttonTitleFont |
||||
qrCodeScannerButton.label.font = viewModel.buttonTitleFont |
||||
otherButton.label.font = viewModel.buttonTitleFont |
||||
|
||||
inputWalletAddressButton.label.textColor = viewModel.buttonTitleColor |
||||
qrCodeScannerButton.label.textColor = viewModel.buttonTitleColor |
||||
otherButton.label.textColor = viewModel.buttonTitleColor |
||||
} |
||||
|
||||
override func viewDidLayoutSubviews() { |
||||
super.viewDidLayoutSubviews() |
||||
inputWalletAddressButton.layer.cornerRadius = inputWalletAddressButton.frame.size.height / 2 |
||||
qrCodeScannerButton.layer.cornerRadius = qrCodeScannerButton.frame.size.height / 2 |
||||
} |
||||
} |
||||
|
@ -0,0 +1,437 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import UIKit |
||||
|
||||
protocol EnterSellTicketsDetailsViewControllerDelegate: class { |
||||
func didEnterSellTicketDetails(ticketHolder: TicketHolder, linkExpiryDate: Date, ethCost: String, dollarCost: String, in viewController: EnterSellTicketsDetailsViewController) |
||||
func didPressViewInfo(in viewController: EnterSellTicketsDetailsViewController) |
||||
} |
||||
|
||||
class EnterSellTicketsDetailsViewController: UIViewController { |
||||
|
||||
let storage: TokensDataStore |
||||
//roundedBackground is used to achieve the top 2 rounded corners-only effect since maskedCorners to not round bottom corners is not available in iOS 10 |
||||
let roundedBackground = UIView() |
||||
let scrollView = UIScrollView() |
||||
let header = TicketsViewControllerTitleHeader() |
||||
let subtitleLabel = UILabel() |
||||
let ethHelpButton = UIButton(type: .system) |
||||
let pricePerTicketLabel = UILabel() |
||||
let pricePerTicketField = AmountTextField() |
||||
let quantityLabel = UILabel() |
||||
let quantityStepper = NumberStepper() |
||||
let linkExpiryDateLabel = UILabel() |
||||
let linkExpiryDateField = DateEntryField() |
||||
let linkExpiryTimeLabel = UILabel() |
||||
let linkExpiryTimeField = TimeEntryField() |
||||
let totalCostLabel = UILabel() |
||||
let costLabel = UILabel() |
||||
let ticketView = TicketRowView() |
||||
let nextButton = UIButton(type: .system) |
||||
var datePicker = UIDatePicker() |
||||
var timePicker = UIDatePicker() |
||||
var viewModel: SellTicketsQuantitySelectionViewModel! |
||||
var paymentFlow: PaymentFlow |
||||
weak var delegate: EnterSellTicketsDetailsViewControllerDelegate? |
||||
|
||||
init(storage: TokensDataStore, paymentFlow: PaymentFlow) { |
||||
self.storage = storage |
||||
self.paymentFlow = paymentFlow |
||||
super.init(nibName: nil, bundle: nil) |
||||
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(image: R.image.location(), style: .plain, target: self, action: #selector(showInfo)) |
||||
|
||||
roundedBackground.translatesAutoresizingMaskIntoConstraints = false |
||||
roundedBackground.backgroundColor = Colors.appWhite |
||||
roundedBackground.cornerRadius = 20 |
||||
view.addSubview(roundedBackground) |
||||
|
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false |
||||
roundedBackground.addSubview(scrollView) |
||||
|
||||
subtitleLabel.translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
ethHelpButton.setTitle(R.string.localizable.aWalletTicketTokenSellLearnAboutEthButtonTitle(), for: .normal) |
||||
ethHelpButton.addTarget(self, action: #selector(learnMoreAboutEthereumTapped), for: .touchUpInside) |
||||
|
||||
nextButton.setTitle(R.string.localizable.aWalletTicketTokenSellGenerateLinkButtonTitle(), for: .normal) |
||||
nextButton.addTarget(self, action: #selector(nextButtonTapped), for: .touchUpInside) |
||||
|
||||
ticketView.translatesAutoresizingMaskIntoConstraints = false |
||||
scrollView.addSubview(ticketView) |
||||
|
||||
pricePerTicketLabel.translatesAutoresizingMaskIntoConstraints = false |
||||
quantityLabel.translatesAutoresizingMaskIntoConstraints = false |
||||
linkExpiryDateLabel.translatesAutoresizingMaskIntoConstraints = false |
||||
linkExpiryTimeLabel.translatesAutoresizingMaskIntoConstraints = false |
||||
totalCostLabel.translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
pricePerTicketField.translatesAutoresizingMaskIntoConstraints = false |
||||
//TODO is there a better way to get the price? |
||||
if let rates = storage.tickers, let ticker = rates.values.first(where: { $0.symbol == "ETH" }), let price = Double(ticker.price) { |
||||
pricePerTicketField.ethToDollarRate = price |
||||
} |
||||
pricePerTicketField.delegate = self |
||||
|
||||
costLabel.translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
linkExpiryDateField.translatesAutoresizingMaskIntoConstraints = false |
||||
linkExpiryDateField.value = Date.yesterday |
||||
linkExpiryDateField.delegate = self |
||||
|
||||
linkExpiryTimeField.translatesAutoresizingMaskIntoConstraints = false |
||||
linkExpiryTimeField.delegate = self |
||||
|
||||
quantityStepper.translatesAutoresizingMaskIntoConstraints = false |
||||
quantityStepper.minimumValue = 1 |
||||
quantityStepper.value = 1 |
||||
|
||||
let col0 = UIStackView(arrangedSubviews: [ |
||||
pricePerTicketLabel, |
||||
.spacer(height: 4), |
||||
pricePerTicketField, |
||||
pricePerTicketField.alternativeAmountLabel, |
||||
.spacer(height: 16), |
||||
linkExpiryDateLabel, |
||||
.spacer(height: 4), |
||||
linkExpiryDateField, |
||||
]) |
||||
col0.translatesAutoresizingMaskIntoConstraints = false |
||||
col0.axis = .vertical |
||||
col0.spacing = 0 |
||||
col0.distribution = .fill |
||||
|
||||
let sameHeightAsPricePerTicketAlternativeAmountLabelPlaceholder = UIView() |
||||
sameHeightAsPricePerTicketAlternativeAmountLabelPlaceholder.translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
let col1 = UIStackView(arrangedSubviews: [ |
||||
quantityLabel, |
||||
.spacer(height: 4), |
||||
quantityStepper, |
||||
sameHeightAsPricePerTicketAlternativeAmountLabelPlaceholder, |
||||
.spacer(height: 16), |
||||
linkExpiryTimeLabel, |
||||
.spacer(height: 4), |
||||
linkExpiryTimeField, |
||||
]) |
||||
col1.translatesAutoresizingMaskIntoConstraints = false |
||||
col1.axis = .vertical |
||||
col1.spacing = 0 |
||||
col1.distribution = .fill |
||||
|
||||
let choicesStackView = UIStackView(arrangedSubviews: [ |
||||
col0, |
||||
.spacerWidth(10), |
||||
col1, |
||||
]) |
||||
choicesStackView.translatesAutoresizingMaskIntoConstraints = false |
||||
choicesStackView.axis = .horizontal |
||||
choicesStackView.spacing = 0 |
||||
choicesStackView.distribution = .fill |
||||
|
||||
datePicker.datePickerMode = .date |
||||
datePicker.minimumDate = Date() |
||||
datePicker.addTarget(self, action: #selector(datePickerValueChanged), for: .valueChanged) |
||||
datePicker.isHidden = true |
||||
|
||||
timePicker.datePickerMode = .time |
||||
timePicker.minimumDate = Date() |
||||
timePicker.addTarget(self, action: #selector(timePickerValueChanged), for: .valueChanged) |
||||
timePicker.isHidden = true |
||||
|
||||
let separator = UIView() |
||||
separator.backgroundColor = UIColor(red: 230, green: 230, blue: 230) |
||||
|
||||
let stackView = UIStackView(arrangedSubviews: [ |
||||
header, |
||||
subtitleLabel, |
||||
.spacer(height: 16), |
||||
ethHelpButton, |
||||
.spacer(height: 30), |
||||
ticketView, |
||||
.spacer(height: 20), |
||||
choicesStackView, |
||||
datePicker, |
||||
timePicker, |
||||
.spacer(height: 18), |
||||
totalCostLabel, |
||||
.spacer(height: 10), |
||||
separator, |
||||
.spacer(height: 10), |
||||
costLabel, |
||||
]) |
||||
stackView.translatesAutoresizingMaskIntoConstraints = false |
||||
stackView.axis = .vertical |
||||
stackView.spacing = 0 |
||||
stackView.distribution = .fill |
||||
stackView.alignment = .center |
||||
scrollView.addSubview(stackView) |
||||
|
||||
let buttonsStackView = UIStackView(arrangedSubviews: [nextButton]) |
||||
buttonsStackView.translatesAutoresizingMaskIntoConstraints = false |
||||
buttonsStackView.axis = .horizontal |
||||
buttonsStackView.spacing = 0 |
||||
buttonsStackView.distribution = .fillEqually |
||||
buttonsStackView.setContentHuggingPriority(.required, for: .horizontal) |
||||
|
||||
let marginToHideBottomRoundedCorners = CGFloat(30) |
||||
let footerBar = UIView() |
||||
footerBar.translatesAutoresizingMaskIntoConstraints = false |
||||
footerBar.backgroundColor = Colors.appHighlightGreen |
||||
roundedBackground.addSubview(footerBar) |
||||
|
||||
let buttonsHeight = CGFloat(60) |
||||
footerBar.addSubview(buttonsStackView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
header.heightAnchor.constraint(equalToConstant: 90), |
||||
//Strange repositioning of header horizontally while typing without this |
||||
header.leadingAnchor.constraint(equalTo: stackView.leadingAnchor), |
||||
header.trailingAnchor.constraint(equalTo: stackView.trailingAnchor), |
||||
|
||||
quantityStepper.heightAnchor.constraint(equalToConstant: 50), |
||||
|
||||
sameHeightAsPricePerTicketAlternativeAmountLabelPlaceholder.heightAnchor.constraint(equalTo: pricePerTicketField.alternativeAmountLabel.heightAnchor), |
||||
|
||||
ticketView.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
ticketView.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
|
||||
roundedBackground.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
roundedBackground.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
roundedBackground.topAnchor.constraint(equalTo: view.topAnchor), |
||||
roundedBackground.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: marginToHideBottomRoundedCorners), |
||||
|
||||
separator.heightAnchor.constraint(equalToConstant: 1), |
||||
separator.leadingAnchor.constraint(equalTo: ticketView.background.leadingAnchor), |
||||
separator.trailingAnchor.constraint(equalTo: ticketView.background.trailingAnchor), |
||||
|
||||
pricePerTicketField.leadingAnchor.constraint(equalTo: ticketView.background.leadingAnchor), |
||||
quantityStepper.rightAnchor.constraint(equalTo: ticketView.background.rightAnchor), |
||||
linkExpiryDateField.leadingAnchor.constraint(equalTo: ticketView.background.leadingAnchor), |
||||
linkExpiryTimeField.rightAnchor.constraint(equalTo: ticketView.background.rightAnchor), |
||||
|
||||
datePicker.leadingAnchor.constraint(equalTo: ticketView.background.leadingAnchor), |
||||
datePicker.trailingAnchor.constraint(equalTo: ticketView.background.trailingAnchor), |
||||
|
||||
timePicker.leadingAnchor.constraint(equalTo: ticketView.background.leadingAnchor), |
||||
timePicker.trailingAnchor.constraint(equalTo: ticketView.background.trailingAnchor), |
||||
|
||||
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), |
||||
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), |
||||
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor), |
||||
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), |
||||
|
||||
buttonsStackView.leadingAnchor.constraint(equalTo: footerBar.leadingAnchor), |
||||
buttonsStackView.trailingAnchor.constraint(equalTo: footerBar.trailingAnchor), |
||||
buttonsStackView.topAnchor.constraint(equalTo: footerBar.topAnchor), |
||||
buttonsStackView.heightAnchor.constraint(equalToConstant: buttonsHeight), |
||||
|
||||
footerBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
footerBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
footerBar.heightAnchor.constraint(equalToConstant: buttonsHeight), |
||||
footerBar.bottomAnchor.constraint(equalTo: view.bottomAnchor), |
||||
|
||||
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
scrollView.topAnchor.constraint(equalTo: view.topAnchor), |
||||
scrollView.bottomAnchor.constraint(equalTo: footerBar.topAnchor), |
||||
|
||||
pricePerTicketField.widthAnchor.constraint(equalTo: quantityStepper.widthAnchor), |
||||
pricePerTicketField.heightAnchor.constraint(equalTo: quantityStepper.heightAnchor), |
||||
linkExpiryDateField.widthAnchor.constraint(equalTo: quantityStepper.widthAnchor), |
||||
linkExpiryDateField.heightAnchor.constraint(equalTo: quantityStepper.heightAnchor), |
||||
linkExpiryTimeField.widthAnchor.constraint(equalTo: quantityStepper.widthAnchor), |
||||
linkExpiryTimeField.heightAnchor.constraint(equalTo: quantityStepper.heightAnchor), |
||||
]) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
@objc |
||||
func nextButtonTapped() { |
||||
guard quantityStepper.value > 0 else { |
||||
UIAlertController.alert(title: "", |
||||
message: R.string.localizable.aWalletTicketTokenSellSelectTicketQuantityAtLeastOneTitle(), |
||||
alertButtonTitles: [R.string.localizable.oK()], |
||||
alertButtonStyles: [.cancel], |
||||
viewController: self, |
||||
completion: nil) |
||||
return |
||||
} |
||||
|
||||
let noPrice: Bool |
||||
if let price = Double(pricePerTicketField.ethCost) { |
||||
noPrice = price.isZero |
||||
} else { |
||||
noPrice = true |
||||
} |
||||
|
||||
guard !noPrice else { |
||||
UIAlertController.alert(title: "", |
||||
message: R.string.localizable.aWalletTicketTokenSellPriceProvideTitle(), |
||||
alertButtonTitles: [R.string.localizable.oK()], |
||||
alertButtonStyles: [.cancel], |
||||
viewController: self, |
||||
completion: nil) |
||||
return |
||||
} |
||||
|
||||
//TODO be good if we check if date chosen is not too far into the future. Example 1 year ahead. Common error? |
||||
|
||||
delegate?.didEnterSellTicketDetails(ticketHolder: getTicketHolderFromQuantity(), linkExpiryDate: linkExpiryDate(), ethCost: pricePerTicketField.ethCost, dollarCost: pricePerTicketField.dollarCost, in: self) |
||||
} |
||||
|
||||
private func linkExpiryDate() -> Date { |
||||
let hour = NSCalendar.current.component(.hour, from: linkExpiryTimeField.value) |
||||
let minutes = NSCalendar.current.component(.minute, from: linkExpiryTimeField.value) |
||||
let seconds = NSCalendar.current.component(.second, from: linkExpiryTimeField.value) |
||||
if let date = NSCalendar.current.date(bySettingHour: hour, minute: minutes, second: seconds, of: linkExpiryDateField.value) { |
||||
return date |
||||
} else { |
||||
return Date() |
||||
} |
||||
} |
||||
|
||||
|
||||
@objc func learnMoreAboutEthereumTapped() { |
||||
showInfo() |
||||
} |
||||
|
||||
@objc func showInfo() { |
||||
delegate?.didPressViewInfo(in: self) |
||||
} |
||||
|
||||
func configure(viewModel: SellTicketsQuantitySelectionViewModel) { |
||||
self.viewModel = viewModel |
||||
|
||||
view.backgroundColor = viewModel.backgroundColor |
||||
|
||||
header.configure(title: viewModel.headerTitle) |
||||
|
||||
subtitleLabel.textAlignment = .center |
||||
subtitleLabel.textColor = viewModel.subtitleLabelColor |
||||
subtitleLabel.font = viewModel.subtitleLabelFont |
||||
subtitleLabel.text = viewModel.subtitleLabelText |
||||
|
||||
ethHelpButton.titleLabel?.font = viewModel.ethHelpButtonFont |
||||
|
||||
ticketView.configure(viewModel: .init()) |
||||
|
||||
pricePerTicketLabel.textAlignment = .center |
||||
pricePerTicketLabel.textColor = viewModel.subtitleLabelColor |
||||
pricePerTicketLabel.font = viewModel.choiceLabelFont |
||||
pricePerTicketLabel.text = viewModel.pricePerTicketLabelText |
||||
|
||||
linkExpiryDateLabel.textAlignment = .center |
||||
linkExpiryDateLabel.textColor = viewModel.subtitleLabelColor |
||||
linkExpiryDateLabel.font = viewModel.choiceLabelFont |
||||
linkExpiryDateLabel.text = viewModel.linkExpiryDateLabelText |
||||
|
||||
linkExpiryTimeLabel.textAlignment = .center |
||||
linkExpiryTimeLabel.textColor = viewModel.subtitleLabelColor |
||||
linkExpiryTimeLabel.font = viewModel.choiceLabelFont |
||||
linkExpiryTimeLabel.text = viewModel.linkExpiryTimeLabelText |
||||
|
||||
totalCostLabel.textAlignment = .center |
||||
totalCostLabel.textColor = viewModel.totalCostLabelColor |
||||
totalCostLabel.font = viewModel.totalCostLabelFont |
||||
totalCostLabel.text = viewModel.totalCostLabelText |
||||
|
||||
costLabel.textAlignment = .center |
||||
costLabel.textColor = viewModel.costLabelColor |
||||
costLabel.font = viewModel.costLabelFont |
||||
costLabel.text = viewModel.costLabelText |
||||
|
||||
quantityLabel.textAlignment = .center |
||||
quantityLabel.textColor = viewModel.choiceLabelColor |
||||
quantityLabel.font = viewModel.choiceLabelFont |
||||
quantityLabel.text = viewModel.quantityLabelText |
||||
|
||||
quantityStepper.borderWidth = 1 |
||||
quantityStepper.clipsToBounds = true |
||||
quantityStepper.borderColor = viewModel.stepperBorderColor |
||||
quantityStepper.maximumValue = viewModel.maxValue |
||||
|
||||
ticketView.stateLabel.isHidden = true |
||||
|
||||
ticketView.ticketCountLabel.text = viewModel.ticketCount |
||||
|
||||
ticketView.titleLabel.text = viewModel.title |
||||
|
||||
ticketView.venueLabel.text = viewModel.venue |
||||
|
||||
ticketView.dateLabel.text = viewModel.date |
||||
|
||||
ticketView.seatRangeLabel.text = viewModel.seatRange |
||||
|
||||
ticketView.zoneNameLabel.text = viewModel.zoneName |
||||
|
||||
nextButton.setTitleColor(viewModel.buttonTitleColor, for: .normal) |
||||
nextButton.backgroundColor = viewModel.buttonBackgroundColor |
||||
nextButton.titleLabel?.font = viewModel.buttonFont |
||||
} |
||||
|
||||
private func getTicketHolderFromQuantity() -> TicketHolder { |
||||
let quantity = quantityStepper.value |
||||
let ticketHolder = viewModel.ticketHolder |
||||
let tickets = Array(ticketHolder.tickets[..<quantity]) |
||||
return TicketHolder( |
||||
tickets: tickets, |
||||
zone: ticketHolder.zone, |
||||
name: ticketHolder.name, |
||||
venue: ticketHolder.venue, |
||||
date: ticketHolder.date, |
||||
status: ticketHolder.status |
||||
) |
||||
} |
||||
|
||||
override func viewDidLayoutSubviews() { |
||||
super.viewDidLayoutSubviews() |
||||
quantityStepper.layer.cornerRadius = quantityStepper.frame.size.height / 2 |
||||
pricePerTicketField.layer.cornerRadius = quantityStepper.frame.size.height / 2 |
||||
linkExpiryDateField.layer.cornerRadius = linkExpiryDateField.frame.size.height / 2 |
||||
linkExpiryTimeField.layer.cornerRadius = linkExpiryTimeField.frame.size.height / 2 |
||||
} |
||||
|
||||
@objc func datePickerValueChanged() { |
||||
linkExpiryDateField.value = datePicker.date |
||||
} |
||||
|
||||
@objc func timePickerValueChanged() { |
||||
linkExpiryTimeField.value = timePicker.date |
||||
} |
||||
} |
||||
|
||||
extension EnterSellTicketsDetailsViewController: DateEntryFieldDelegate { |
||||
func didTap(in dateEntryField: DateEntryField) { |
||||
datePicker.isHidden = !datePicker.isHidden |
||||
if !datePicker.isHidden { |
||||
datePicker.date = linkExpiryDateField.value |
||||
timePicker.isHidden = true |
||||
} |
||||
} |
||||
} |
||||
|
||||
extension EnterSellTicketsDetailsViewController: TimeEntryFieldDelegate { |
||||
func didTap(in timeEntryField: TimeEntryField) { |
||||
timePicker.isHidden = !timePicker.isHidden |
||||
if !timePicker.isHidden { |
||||
timePicker.date = linkExpiryTimeField.value |
||||
datePicker.isHidden = true |
||||
} |
||||
} |
||||
} |
||||
|
||||
extension EnterSellTicketsDetailsViewController: AmountTextFieldDelegate { |
||||
func changeAmount(in textField: AmountTextField) { |
||||
viewModel.ethCost = textField.ethCost |
||||
configure(viewModel: viewModel) |
||||
} |
||||
|
||||
func changeType(in textField: AmountTextField) { |
||||
viewModel.ethCost = textField.ethCost |
||||
configure(viewModel: viewModel) |
||||
} |
||||
} |
@ -0,0 +1,165 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import UIKit |
||||
import TrustKeystore |
||||
|
||||
protocol SellTicketViaWalletAddressViewControllerDelegate: class { |
||||
func didChooseSell(to walletAddress: String, viewController: SellTicketViaWalletAddressViewController) |
||||
} |
||||
|
||||
class SellTicketViaWalletAddressViewController: UIViewController { |
||||
//roundedBackground is used to achieve the top 2 rounded corners-only effect since maskedCorners to not round bottom corners is not available in iOS 10 |
||||
let roundedBackground = UIView() |
||||
let titleLabel = UILabel() |
||||
let subtitleLabel = UILabel() |
||||
let textField = UITextField() |
||||
let ticketView = TicketRowView() |
||||
let actionButton = UIButton(type: .system) |
||||
var paymentFlow: PaymentFlow |
||||
weak var delegate: SellTicketViaWalletAddressViewControllerDelegate? |
||||
let ticketHolder: TicketHolder |
||||
var linkExpiryDate: Date |
||||
var ethCost: String |
||||
var dollarCost: String |
||||
|
||||
init(ticketHolder: TicketHolder, linkExpiryDate: Date, ethCost: String, dollarCost: String, paymentFlow: PaymentFlow) { |
||||
self.ticketHolder = ticketHolder |
||||
self.linkExpiryDate = linkExpiryDate |
||||
self.ethCost = ethCost |
||||
self.dollarCost = dollarCost |
||||
self.paymentFlow = paymentFlow |
||||
super.init(nibName: nil, bundle: nil) |
||||
|
||||
view.backgroundColor = Colors.appBackground |
||||
|
||||
roundedBackground.translatesAutoresizingMaskIntoConstraints = false |
||||
roundedBackground.cornerRadius = 20 |
||||
view.addSubview(roundedBackground) |
||||
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false |
||||
textField.delegate = self |
||||
textField.returnKeyType = .done |
||||
|
||||
actionButton.translatesAutoresizingMaskIntoConstraints = false |
||||
actionButton.addTarget(self, action: #selector(sell), for: .touchUpInside) |
||||
roundedBackground.addSubview(actionButton) |
||||
|
||||
ticketView.translatesAutoresizingMaskIntoConstraints = false |
||||
view.addSubview(ticketView) |
||||
|
||||
let stackView = UIStackView(arrangedSubviews: [ |
||||
.spacer(height: 7), |
||||
titleLabel, |
||||
.spacer(height: 20), |
||||
subtitleLabel, |
||||
.spacer(height: 10), |
||||
textField, |
||||
.spacer(height: 40), |
||||
ticketView, |
||||
]) |
||||
stackView.translatesAutoresizingMaskIntoConstraints = false |
||||
stackView.axis = .vertical |
||||
stackView.spacing = 0 |
||||
stackView.distribution = .fill |
||||
stackView.alignment = .center |
||||
roundedBackground.addSubview(stackView) |
||||
|
||||
let marginToHideBottomRoundedCorners = CGFloat(30) |
||||
NSLayoutConstraint.activate([ |
||||
textField.leadingAnchor.constraint(equalTo: roundedBackground.leadingAnchor, constant: 30), |
||||
textField.trailingAnchor.constraint(equalTo: roundedBackground.trailingAnchor, constant: -30), |
||||
textField.heightAnchor.constraint(equalToConstant: 50), |
||||
|
||||
ticketView.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
ticketView.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
|
||||
stackView.leadingAnchor.constraint(equalTo: roundedBackground.leadingAnchor), |
||||
stackView.trailingAnchor.constraint(equalTo: roundedBackground.trailingAnchor), |
||||
stackView.topAnchor.constraint(equalTo: roundedBackground.topAnchor, constant: 16), |
||||
|
||||
actionButton.leadingAnchor.constraint(equalTo: roundedBackground.leadingAnchor), |
||||
actionButton.trailingAnchor.constraint(equalTo: roundedBackground.trailingAnchor), |
||||
actionButton.heightAnchor.constraint(equalToConstant: 60), |
||||
actionButton.bottomAnchor.constraint(equalTo: roundedBackground.bottomAnchor, constant: -marginToHideBottomRoundedCorners), |
||||
|
||||
roundedBackground.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
roundedBackground.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
roundedBackground.topAnchor.constraint(equalTo: view.topAnchor), |
||||
roundedBackground.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: marginToHideBottomRoundedCorners), |
||||
]) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
func configure(viewModel: SellTicketViaWalletAddressViewControllerViewModel) { |
||||
roundedBackground.backgroundColor = viewModel.contentsBackgroundColor |
||||
roundedBackground.layer.cornerRadius = 20 |
||||
|
||||
titleLabel.numberOfLines = 0 |
||||
titleLabel.textColor = viewModel.titleColor |
||||
titleLabel.font = viewModel.titleFont |
||||
titleLabel.textAlignment = .center |
||||
titleLabel.text = viewModel.titleLabelText |
||||
|
||||
subtitleLabel.textColor = viewModel.subtitleColor |
||||
subtitleLabel.font = viewModel.subtitleFont |
||||
subtitleLabel.textAlignment = .center |
||||
subtitleLabel.text = viewModel.subtitleLabelText |
||||
|
||||
textField.textColor = viewModel.textFieldTextColor |
||||
textField.font = viewModel.textFieldFont |
||||
textField.layer.borderColor = viewModel.textFieldBorderColor.cgColor |
||||
textField.layer.borderWidth = viewModel.textFieldBorderWidth |
||||
textField.leftView = .spacerWidth(viewModel.textFieldHorizontalPadding) |
||||
textField.leftViewMode = .always |
||||
textField.rightView = .spacerWidth(viewModel.textFieldHorizontalPadding) |
||||
textField.rightViewMode = .always |
||||
|
||||
ticketView.configure(viewModel: .init()) |
||||
|
||||
ticketView.stateLabel.isHidden = true |
||||
|
||||
ticketView.ticketCountLabel.text = viewModel.ticketCount |
||||
|
||||
ticketView.titleLabel.text = viewModel.title |
||||
|
||||
ticketView.venueLabel.text = viewModel.venue |
||||
|
||||
ticketView.dateLabel.text = viewModel.date |
||||
|
||||
ticketView.seatRangeLabel.text = viewModel.seatRange |
||||
|
||||
ticketView.zoneNameLabel.text = viewModel.zoneName |
||||
|
||||
actionButton.setTitle(viewModel.actionButtonTitle, for: .normal) |
||||
actionButton.setTitleColor(viewModel.actionButtonTitleColor, for: .normal) |
||||
actionButton.setBackgroundColor(viewModel.actionButtonBackgroundColor, forState: .normal) |
||||
actionButton.titleLabel?.font = viewModel.actionButtonTitleFont |
||||
} |
||||
|
||||
override func viewDidLayoutSubviews() { |
||||
super.viewDidLayoutSubviews() |
||||
actionButton.layer.cornerRadius = actionButton.frame.size.height / 2 |
||||
textField.layer.cornerRadius = textField.frame.size.height / 2 |
||||
} |
||||
|
||||
@objc func sell() { |
||||
if let address = textField.text, !address.isEmpty { |
||||
guard let _ = Address(string: address) else { |
||||
displayError(error: Errors.invalidAddress) |
||||
return |
||||
} |
||||
|
||||
delegate?.didChooseSell(to: address, viewController: self) |
||||
} |
||||
} |
||||
} |
||||
|
||||
extension SellTicketViaWalletAddressViewController: UITextFieldDelegate { |
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool { |
||||
textField.resignFirstResponder() |
||||
return true |
||||
} |
||||
} |
@ -0,0 +1,154 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import UIKit |
||||
|
||||
protocol SellTicketsViewControllerDelegate: class { |
||||
func didSelectTicketHolder(ticketHolder: TicketHolder, in viewController: SellTicketsViewController) |
||||
func didPressViewInfo(in viewController: SellTicketsViewController) |
||||
} |
||||
|
||||
class SellTicketsViewController: UIViewController { |
||||
|
||||
//roundedBackground is used to achieve the top 2 rounded corners-only effect since maskedCorners to not round bottom corners is not available in iOS 10 |
||||
let roundedBackground = UIView() |
||||
let header = TicketsViewControllerTitleHeader() |
||||
let tableView = UITableView(frame: .zero, style: .plain) |
||||
let nextButton = UIButton(type: .system) |
||||
var viewModel: SellTicketsViewModel! |
||||
var paymentFlow: PaymentFlow |
||||
weak var delegate: SellTicketsViewControllerDelegate? |
||||
|
||||
init(paymentFlow: PaymentFlow) { |
||||
self.paymentFlow = paymentFlow |
||||
super.init(nibName: nil, bundle: nil) |
||||
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(image: R.image.location(), style: .plain, target: self, action: #selector(showInfo)) |
||||
|
||||
view.backgroundColor = Colors.appBackground |
||||
|
||||
roundedBackground.translatesAutoresizingMaskIntoConstraints = false |
||||
roundedBackground.backgroundColor = Colors.appWhite |
||||
roundedBackground.cornerRadius = 20 |
||||
view.addSubview(roundedBackground) |
||||
|
||||
tableView.register(TicketTableViewCellWithCheckbox.self, forCellReuseIdentifier: TicketTableViewCellWithCheckbox.identifier) |
||||
tableView.translatesAutoresizingMaskIntoConstraints = false |
||||
tableView.delegate = self |
||||
tableView.separatorStyle = .none |
||||
tableView.backgroundColor = Colors.appWhite |
||||
tableView.tableHeaderView = header |
||||
roundedBackground.addSubview(tableView) |
||||
|
||||
nextButton.setTitle(R.string.localizable.aWalletNextButtonTitle(), for: .normal) |
||||
nextButton.addTarget(self, action: #selector(nextButtonTapped), for: .touchUpInside) |
||||
|
||||
let buttonsStackView = UIStackView(arrangedSubviews: [nextButton]) |
||||
buttonsStackView.translatesAutoresizingMaskIntoConstraints = false |
||||
buttonsStackView.axis = .horizontal |
||||
buttonsStackView.spacing = 0 |
||||
buttonsStackView.distribution = .fillEqually |
||||
buttonsStackView.setContentHuggingPriority(UILayoutPriority.required, for: .horizontal) |
||||
|
||||
let marginToHideBottomRoundedCorners = CGFloat(30) |
||||
let footerBar = UIView() |
||||
footerBar.translatesAutoresizingMaskIntoConstraints = false |
||||
footerBar.backgroundColor = Colors.appHighlightGreen |
||||
roundedBackground.addSubview(footerBar) |
||||
|
||||
let buttonsHeight = CGFloat(60) |
||||
footerBar.addSubview(buttonsStackView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
roundedBackground.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
roundedBackground.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
roundedBackground.topAnchor.constraint(equalTo: view.topAnchor), |
||||
roundedBackground.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: marginToHideBottomRoundedCorners), |
||||
|
||||
tableView.leadingAnchor.constraint(equalTo: roundedBackground.leadingAnchor), |
||||
tableView.trailingAnchor.constraint(equalTo: roundedBackground.trailingAnchor), |
||||
tableView.topAnchor.constraint(equalTo: roundedBackground.topAnchor), |
||||
tableView.bottomAnchor.constraint(equalTo: footerBar.topAnchor), |
||||
|
||||
buttonsStackView.leadingAnchor.constraint(equalTo: footerBar.leadingAnchor), |
||||
buttonsStackView.trailingAnchor.constraint(equalTo: footerBar.trailingAnchor), |
||||
buttonsStackView.topAnchor.constraint(equalTo: footerBar.topAnchor), |
||||
buttonsStackView.heightAnchor.constraint(equalToConstant: buttonsHeight), |
||||
|
||||
footerBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
footerBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
footerBar.heightAnchor.constraint(equalToConstant: buttonsHeight), |
||||
footerBar.bottomAnchor.constraint(equalTo: view.bottomAnchor), |
||||
]) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
func configure(viewModel: SellTicketsViewModel) { |
||||
self.viewModel = viewModel |
||||
tableView.dataSource = self |
||||
|
||||
header.configure(title: viewModel.title) |
||||
tableView.tableHeaderView = header |
||||
|
||||
nextButton.setTitleColor(viewModel.buttonTitleColor, for: .normal) |
||||
nextButton.backgroundColor = viewModel.buttonBackgroundColor |
||||
nextButton.titleLabel?.font = viewModel.buttonFont |
||||
} |
||||
|
||||
private func resetSelection(for ticketHolder: TicketHolder) { |
||||
let selected = ticketHolder.isSelected |
||||
viewModel.ticketHolders?.forEach { $0.isSelected = false } |
||||
ticketHolder.isSelected = !selected |
||||
tableView.reloadData() |
||||
} |
||||
|
||||
@objc |
||||
func nextButtonTapped() { |
||||
let selectedTicketHolders = viewModel.ticketHolders?.filter { $0.isSelected } |
||||
if selectedTicketHolders!.isEmpty { |
||||
UIAlertController.alert(title: "", |
||||
message: R.string.localizable.aWalletTicketTokenSellSelectTicketsAtLeastOneTitle(), |
||||
alertButtonTitles: [R.string.localizable.oK()], |
||||
alertButtonStyles: [.cancel], |
||||
viewController: self, |
||||
completion: nil) |
||||
} else { |
||||
self.delegate?.didSelectTicketHolder(ticketHolder: selectedTicketHolders!.first!, in: self) |
||||
} |
||||
} |
||||
|
||||
@objc func showInfo() { |
||||
delegate?.didPressViewInfo(in: self) |
||||
} |
||||
} |
||||
|
||||
extension SellTicketsViewController: UITableViewDelegate, UITableViewDataSource { |
||||
func numberOfSections(in tableView: UITableView) -> Int { |
||||
return 1 |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { |
||||
return viewModel.numberOfItems(for: section) |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { |
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TicketTableViewCellWithCheckbox.identifier, for: indexPath) as! TicketTableViewCellWithCheckbox |
||||
let ticketHolder = viewModel.item(for: indexPath) |
||||
cell.configure(viewModel: .init(ticketHolder: ticketHolder)) |
||||
return cell |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { |
||||
let ticketHolder = viewModel.item(for: indexPath) |
||||
let cellViewModel = BaseTicketTableViewCellViewModel(ticketHolder: ticketHolder) |
||||
return cellViewModel.cellHeight |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { |
||||
let ticketHolder = viewModel.item(for: indexPath) |
||||
resetSelection(for: ticketHolder) |
||||
tableView.deselectRow(at: indexPath, animated: true) |
||||
} |
||||
} |
@ -0,0 +1,49 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import UIKit |
||||
|
||||
class ChooseTicketSellModeViewControllerViewModel { |
||||
var contentsBackgroundColor: UIColor { |
||||
return Colors.appWhite |
||||
} |
||||
var titleColor: UIColor { |
||||
return Colors.appText |
||||
} |
||||
var titleFont: UIFont { |
||||
return Fonts.light(size: 25)! |
||||
} |
||||
var titleLabelText: String { |
||||
return R.string.localizable.aWalletTicketTokenSellModeChooseTitle() |
||||
} |
||||
var textButtonTitle: String { |
||||
return R.string.localizable.aWalletTicketTokenSellModeChooseTextTitle() |
||||
} |
||||
var inputWalletAddressButtonTitle: String { |
||||
return R.string.localizable.aWalletTicketTokenSellModeChooseInputWalletAddressTitle() |
||||
} |
||||
var inputWalletAddressButtonImage: UIImage? { |
||||
return R.image.transfer_wallet_address() |
||||
} |
||||
var qrCodeScannerButtonTitle: String { |
||||
return R.string.localizable.aWalletTicketTokenSellModeChooseWalletAddressViaQRCodeScannerTitle() |
||||
} |
||||
var qrCodeScannerButtonImage: UIImage? { |
||||
return R.image.transfer_qr_code() |
||||
} |
||||
var otherButtonTitle: String { |
||||
return R.string.localizable.aWalletTicketTokenSellModeChooseOtherTitle() |
||||
} |
||||
var otherButtonImage: UIImage? { |
||||
return R.image.transfer_others() |
||||
} |
||||
var buttonTitleFont: UIFont { |
||||
if ScreenChecker().isNarrowScreen() { |
||||
return Fonts.light(size: 18)! |
||||
} else { |
||||
return Fonts.light(size: 21)! |
||||
} |
||||
} |
||||
var buttonTitleColor: UIColor { |
||||
return Colors.appText |
||||
} |
||||
} |
@ -0,0 +1,81 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import UIKit |
||||
|
||||
struct SellTicketViaWalletAddressViewControllerViewModel { |
||||
var ticketHolder: TicketHolder |
||||
|
||||
var contentsBackgroundColor: UIColor { |
||||
return Colors.appWhite |
||||
} |
||||
var titleColor: UIColor { |
||||
return Colors.appText |
||||
} |
||||
var titleFont: UIFont { |
||||
return Fonts.light(size: 25)! |
||||
} |
||||
var subtitleColor: UIColor { |
||||
return UIColor(red: 155, green: 155, blue: 155) |
||||
} |
||||
var subtitleFont: UIFont { |
||||
return Fonts.regular(size: 10)! |
||||
} |
||||
var actionButtonTitleColor: UIColor { |
||||
return Colors.appWhite |
||||
} |
||||
var actionButtonBackgroundColor: UIColor { |
||||
return Colors.appHighlightGreen |
||||
} |
||||
var actionButtonTitleFont: UIFont { |
||||
return Fonts.regular(size: 20)! |
||||
} |
||||
var titleLabelText: String { |
||||
return R.string.localizable.aWalletTicketTokenSellModeWalletAddressTitle() |
||||
} |
||||
var subtitleLabelText: String { |
||||
return R.string.localizable.aWalletTicketTokenSellModeWalletAddressTargetTitle() |
||||
} |
||||
var actionButtonTitle: String { |
||||
return R.string.localizable.aWalletTicketTokenSellButtonTitle() |
||||
} |
||||
var textFieldTextColor: UIColor { |
||||
return Colors.appText |
||||
} |
||||
var textFieldFont: UIFont { |
||||
return Fonts.light(size: 15)! |
||||
} |
||||
var textFieldBorderColor: UIColor { |
||||
return Colors.appBackground |
||||
} |
||||
var textFieldBorderWidth: CGFloat { |
||||
return 1 |
||||
} |
||||
var textFieldHorizontalPadding: CGFloat { |
||||
return 22 |
||||
} |
||||
|
||||
var ticketCount: String { |
||||
return "x\(ticketHolder.tickets.count)" |
||||
} |
||||
|
||||
var title: String { |
||||
return ticketHolder.name |
||||
} |
||||
|
||||
var seatRange: String { |
||||
return ticketHolder.seatRange |
||||
} |
||||
|
||||
var zoneName: String { |
||||
return ticketHolder.zone |
||||
} |
||||
|
||||
var venue: String { |
||||
return ticketHolder.venue |
||||
} |
||||
|
||||
var date: String { |
||||
//TODO Should format be localized? |
||||
return ticketHolder.date.format("dd MMM yyyy") |
||||
} |
||||
} |
@ -0,0 +1,131 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
struct SellTicketsQuantitySelectionViewModel { |
||||
|
||||
var ticketHolder: TicketHolder |
||||
var ethCost: String = "0" |
||||
|
||||
var headerTitle: String { |
||||
return R.string.localizable.aWalletTicketTokenSellSelectQuantityTitle() |
||||
} |
||||
|
||||
var maxValue: Int { |
||||
return ticketHolder.tickets.count |
||||
} |
||||
|
||||
var backgroundColor: UIColor { |
||||
return Colors.appBackground |
||||
} |
||||
|
||||
var buttonTitleColor: UIColor { |
||||
return Colors.appWhite |
||||
} |
||||
|
||||
var buttonBackgroundColor: UIColor { |
||||
return Colors.appHighlightGreen |
||||
} |
||||
|
||||
var buttonFont: UIFont { |
||||
return Fonts.regular(size: 20)! |
||||
} |
||||
|
||||
var subtitleLabelColor: UIColor { |
||||
return UIColor(red: 155, green: 155, blue: 155) |
||||
} |
||||
|
||||
var subtitleLabelFont: UIFont { |
||||
return Fonts.light(size: 18)! |
||||
} |
||||
|
||||
var subtitleLabelText: String { |
||||
return R.string.localizable.aWalletTicketTokenSellEthHelpTitle() |
||||
} |
||||
|
||||
var ethHelpButtonFont: UIFont { |
||||
return Fonts.semibold(size: 18)! |
||||
} |
||||
|
||||
var choiceLabelColor: UIColor { |
||||
return UIColor(red: 155, green: 155, blue: 155) |
||||
} |
||||
|
||||
var choiceLabelFont: UIFont { |
||||
return Fonts.regular(size: 10)! |
||||
} |
||||
|
||||
var stepperBorderColor: UIColor { |
||||
return Colors.appBackground |
||||
} |
||||
|
||||
var ticketCount: String { |
||||
return "x\(ticketHolder.tickets.count)" |
||||
} |
||||
|
||||
var title: String { |
||||
return ticketHolder.name |
||||
} |
||||
|
||||
var seatRange: String { |
||||
return ticketHolder.seatRange |
||||
} |
||||
|
||||
var zoneName: String { |
||||
return ticketHolder.zone |
||||
} |
||||
|
||||
var venue: String { |
||||
return ticketHolder.venue |
||||
} |
||||
|
||||
var quantityLabelText: String { |
||||
return R.string.localizable.aWalletTicketTokenSellQuantityTitle() |
||||
} |
||||
|
||||
var date: String { |
||||
//TODO Should format be localized? |
||||
return ticketHolder.date.format("dd MMM yyyy") |
||||
} |
||||
|
||||
var pricePerTicketLabelText: String { |
||||
return R.string.localizable.aWalletTicketTokenSellPricePerTicketTitle() |
||||
} |
||||
|
||||
var linkExpiryDateLabelText: String { |
||||
return R.string.localizable.aWalletTicketTokenSellLinkExpiryDateTitle() |
||||
} |
||||
|
||||
var linkExpiryTimeLabelText: String { |
||||
return R.string.localizable.aWalletTicketTokenSellLinkExpiryTimeTitle() |
||||
} |
||||
|
||||
var totalCostLabelText: String { |
||||
return R.string.localizable.aWalletTicketTokenSellTotalCostTitle() |
||||
} |
||||
|
||||
var totalCostLabelFont: UIFont { |
||||
return Fonts.light(size: 21)! |
||||
} |
||||
|
||||
var totalCostLabelColor: UIColor { |
||||
return Colors.appText |
||||
} |
||||
|
||||
var costLabelText: String { |
||||
return "\(ethCost) ETH" |
||||
} |
||||
|
||||
var costLabelColor: UIColor { |
||||
return Colors.appBackground |
||||
} |
||||
|
||||
var costLabelFont: UIFont { |
||||
return Fonts.semibold(size: 21)! |
||||
} |
||||
|
||||
init(ticketHolder: TicketHolder) { |
||||
self.ticketHolder = ticketHolder |
||||
} |
||||
} |
@ -0,0 +1,43 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
struct SellTicketsViewModel { |
||||
|
||||
var token: TokenObject |
||||
var ticketHolders: [TicketHolder]? |
||||
|
||||
init(token: TokenObject) { |
||||
self.token = token |
||||
self.ticketHolders = TicketAdaptor.getTicketHolders(for: token) |
||||
} |
||||
|
||||
func item(for indexPath: IndexPath) -> TicketHolder { |
||||
return ticketHolders![indexPath.row] |
||||
} |
||||
|
||||
func numberOfItems(for section: Int) -> Int { |
||||
return ticketHolders!.count |
||||
} |
||||
|
||||
func height(for section: Int) -> CGFloat { |
||||
return 90 |
||||
} |
||||
|
||||
var title: String { |
||||
return R.string.localizable.aWalletTicketTokenSellSelectTicketsTitle () |
||||
} |
||||
|
||||
var buttonTitleColor: UIColor { |
||||
return Colors.appWhite |
||||
} |
||||
|
||||
var buttonBackgroundColor: UIColor { |
||||
return Colors.appHighlightGreen |
||||
} |
||||
|
||||
var buttonFont: UIFont { |
||||
return Fonts.regular(size: 20)! |
||||
} |
||||
} |
@ -0,0 +1,197 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import UIKit |
||||
|
||||
protocol AmountTextFieldDelegate: class { |
||||
func changeAmount(in textField: AmountTextField) |
||||
func changeType(in textField: AmountTextField) |
||||
} |
||||
|
||||
class AmountTextField: UIControl { |
||||
struct Pair { |
||||
let left: String |
||||
let right: String |
||||
|
||||
func swapPair() -> Pair { |
||||
return Pair(left: right, right: left) |
||||
} |
||||
} |
||||
var ethToDollarRate: Double? = nil |
||||
var ethCost: String { |
||||
if currentPair.left == "ETH" { |
||||
return textField.text ?? "0" |
||||
} else { |
||||
return String(convertToAlternateAmount()) |
||||
} |
||||
} |
||||
var dollarCost: String { |
||||
if currentPair.left == "ETH" { |
||||
return String(convertToAlternateAmount()) |
||||
} else { |
||||
return textField.text ?? "0" |
||||
} |
||||
} |
||||
var currentPair: Pair |
||||
let textField = UITextField() |
||||
let alternativeAmountLabel = UILabel() |
||||
let fiatButton = Button(size: .normal, style: .borderless) |
||||
weak var delegate: AmountTextFieldDelegate? |
||||
|
||||
private var allowedCharacters: String = { |
||||
let decimalSeparator = Locale.current.decimalSeparator ?? "." |
||||
return "0123456789" + decimalSeparator |
||||
}() |
||||
lazy var decimalFormatter: DecimalFormatter = { |
||||
return DecimalFormatter() |
||||
}() |
||||
|
||||
init() { |
||||
currentPair = Pair(left: "ETH", right: "USD") |
||||
|
||||
super.init(frame: .zero) |
||||
|
||||
translatesAutoresizingMaskIntoConstraints = false |
||||
layer.borderColor = Colors.appBackground.cgColor |
||||
layer.borderWidth = 1 |
||||
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false |
||||
textField.delegate = self |
||||
textField.keyboardType = .decimalPad |
||||
textField.leftViewMode = .always |
||||
textField.rightViewMode = .always |
||||
textField.inputAccessoryView = makeToolbarWithDoneButton() |
||||
textField.leftView = .spacerWidth(22) |
||||
textField.rightView = makeAmountRightView() |
||||
textField.textColor = Colors.appBackground |
||||
textField.font = Fonts.bold(size: 21) |
||||
addSubview(textField) |
||||
|
||||
alternativeAmountLabel.translatesAutoresizingMaskIntoConstraints = false |
||||
alternativeAmountLabel.numberOfLines = 0 |
||||
alternativeAmountLabel.textColor = UIColor(red: 155, green: 155, blue: 155) |
||||
alternativeAmountLabel.font = Fonts.regular(size: 10)! |
||||
alternativeAmountLabel.textAlignment = .center |
||||
|
||||
computeAlternateAmount() |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
textField.leadingAnchor.constraint(equalTo: leadingAnchor), |
||||
textField.trailingAnchor.constraint(equalTo: trailingAnchor), |
||||
textField.topAnchor.constraint(equalTo: topAnchor), |
||||
textField.bottomAnchor.constraint(equalTo: bottomAnchor), |
||||
]) |
||||
} |
||||
|
||||
private func makeAmountRightView() -> UIView { |
||||
fiatButton.translatesAutoresizingMaskIntoConstraints = false |
||||
fiatButton.setTitle(currentPair.left, for: .normal) |
||||
fiatButton.setTitleColor(UIColor(red: 155, green: 155, blue: 155), for: .normal) |
||||
fiatButton.addTarget(self, action: #selector(fiatAction), for: .touchUpInside) |
||||
|
||||
let amountRightView = UIStackView(arrangedSubviews: [ |
||||
fiatButton, |
||||
]) |
||||
|
||||
amountRightView.translatesAutoresizingMaskIntoConstraints = false |
||||
amountRightView.distribution = .equalSpacing |
||||
amountRightView.spacing = 1 |
||||
amountRightView.axis = .horizontal |
||||
|
||||
return amountRightView |
||||
} |
||||
|
||||
@objc func fiatAction(button: UIButton) { |
||||
let swappedPair = currentPair.swapPair() |
||||
//New pair for future calculation we should swap pair each time we press fiat button. |
||||
self.currentPair = swappedPair |
||||
fiatButton.setTitle(currentPair.left, for: .normal) |
||||
button.setTitle(currentPair.left, for: .normal) |
||||
textField.text = nil |
||||
computeAlternateAmount() |
||||
activateAmountView() |
||||
delegate?.changeType(in: self) |
||||
} |
||||
|
||||
private func activateAmountView() { |
||||
becomeFirstResponder() |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
private func makeToolbarWithDoneButton() -> UIToolbar { |
||||
//Frame needed, but actual values aren't that important |
||||
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 100, height: 40)) |
||||
toolbar.barStyle = .default |
||||
|
||||
let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) |
||||
let done = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(closeKeyboard)) |
||||
|
||||
toolbar.items = [flexSpace, done] |
||||
toolbar.sizeToFit() |
||||
|
||||
return toolbar |
||||
} |
||||
|
||||
@objc func closeKeyboard() { |
||||
endEditing(true) |
||||
} |
||||
|
||||
private func amountChanged(in range: NSRange, to string: String) -> Bool { |
||||
guard let input = textField.text else { |
||||
return true |
||||
} |
||||
//In this step we validate only allowed characters it is because of the iPad keyboard. |
||||
let characterSet = NSCharacterSet(charactersIn: allowedCharacters).inverted |
||||
let separatedChars = string.components(separatedBy: characterSet) |
||||
let filteredNumbersAndSeparator = separatedChars.joined(separator: "") |
||||
if string != filteredNumbersAndSeparator { |
||||
return false |
||||
} |
||||
//This is required to prevent user from input of numbers like 1.000.25 or 1,000,25. |
||||
if string == "," || string == "." || string == "'" { |
||||
return !input.contains(string) |
||||
} |
||||
return true |
||||
} |
||||
|
||||
private func computeAlternateAmount() { |
||||
if currentPair.left == "ETH" { |
||||
alternativeAmountLabel.text = "~ \(convertToAlternateAmount()) USD" |
||||
} else { |
||||
alternativeAmountLabel.text = "~ \(convertToAlternateAmount()) ETH" |
||||
} |
||||
} |
||||
|
||||
private func convertToAlternateAmount() -> String { |
||||
if let ethToDollarRate = ethToDollarRate, let string = textField.text, let amount = Double(string) { |
||||
if currentPair.left == "ETH" { |
||||
return String(amount * ethToDollarRate) |
||||
} else { |
||||
return String(amount / ethToDollarRate) |
||||
} |
||||
} else { |
||||
if currentPair.left == "ETH" { |
||||
return "0.000000" |
||||
} else { |
||||
return "0.00" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
extension AmountTextField: UITextFieldDelegate { |
||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { |
||||
let allowChange = amountChanged(in: range, to: string) |
||||
if allowChange { |
||||
//We have to allow the text field the chance to update, so we have to use asyncAfter.. |
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { |
||||
self.computeAlternateAmount() |
||||
self.delegate?.changeAmount(in: self) |
||||
} |
||||
|
||||
} |
||||
return allowChange |
||||
} |
||||
} |
@ -0,0 +1,108 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import UIKit |
||||
|
||||
protocol DateEntryFieldDelegate: class { |
||||
func didTap(in dateEntryField: DateEntryField) |
||||
} |
||||
|
||||
class DateEntryField: UIControl { |
||||
var leftButton = UIButton(type: .custom) |
||||
var value = Date() { |
||||
didSet { |
||||
displayDateString() |
||||
} |
||||
} |
||||
weak var delegate: DateEntryFieldDelegate? |
||||
|
||||
init() { |
||||
super.init(frame: .zero) |
||||
|
||||
translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
leftButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) |
||||
displayDateString() |
||||
|
||||
let rightView = makeRightView() |
||||
let stackView = UIStackView(arrangedSubviews: [ |
||||
.spacerWidth(22), |
||||
leftButton, |
||||
rightView, |
||||
]) |
||||
stackView.translatesAutoresizingMaskIntoConstraints = false |
||||
stackView.axis = .horizontal |
||||
stackView.spacing = 0 |
||||
stackView.distribution = .fill |
||||
stackView.alignment = .center |
||||
addSubview(stackView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
stackView.leadingAnchor.constraint(equalTo: leadingAnchor), |
||||
stackView.trailingAnchor.constraint(equalTo: trailingAnchor), |
||||
stackView.topAnchor.constraint(equalTo: topAnchor), |
||||
stackView.bottomAnchor.constraint(equalTo: bottomAnchor), |
||||
]) |
||||
|
||||
configure() |
||||
} |
||||
|
||||
private func configure() { |
||||
layer.borderColor = Colors.appBackground.cgColor |
||||
layer.borderWidth = 1 |
||||
|
||||
leftButton.setTitleColor(Colors.appBackground, for: .normal) |
||||
leftButton.titleLabel?.font = Fonts.regular(size: 18) |
||||
} |
||||
|
||||
private func makeRightView() -> UIView { |
||||
let rightButton = UIButton(type: .system) |
||||
rightButton.translatesAutoresizingMaskIntoConstraints = false |
||||
rightButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 10) |
||||
rightButton.imageView?.contentMode = .scaleAspectFit |
||||
rightButton.setImage(R.image.calendar(), for: .normal) |
||||
rightButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) |
||||
|
||||
let rightView = UIStackView(arrangedSubviews: [ |
||||
rightButton, |
||||
]) |
||||
|
||||
rightView.translatesAutoresizingMaskIntoConstraints = false |
||||
rightView.distribution = .equalSpacing |
||||
rightView.spacing = 1 |
||||
rightView.axis = .horizontal |
||||
|
||||
return rightView |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
private func makeToolbarWithDoneButton() -> UIToolbar { |
||||
//Frame needed, but actual values aren't that important |
||||
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 100, height: 40)) |
||||
toolbar.barStyle = .default |
||||
|
||||
let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) |
||||
let done = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(closeKeyboard)) |
||||
|
||||
toolbar.items = [flexSpace, done] |
||||
toolbar.sizeToFit() |
||||
|
||||
return toolbar |
||||
} |
||||
|
||||
@objc func closeKeyboard() { |
||||
delegate?.didTap(in: self) |
||||
} |
||||
|
||||
@objc func buttonTapped() { |
||||
delegate?.didTap(in: self) |
||||
} |
||||
|
||||
private func displayDateString() { |
||||
//TODO Should format be localized? |
||||
let dateString = value.format("dd MMM yyyy") |
||||
leftButton.setTitle(dateString, for: .normal) |
||||
} |
||||
} |
@ -0,0 +1,110 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import UIKit |
||||
|
||||
protocol TimeEntryFieldDelegate: class { |
||||
func didTap(in timeEntryField: TimeEntryField) |
||||
} |
||||
|
||||
class TimeEntryField: UIControl { |
||||
var leftButton = UIButton(type: .custom) |
||||
var value = Date() { |
||||
didSet { |
||||
displayTimeString() |
||||
} |
||||
} |
||||
weak var delegate: TimeEntryFieldDelegate? |
||||
|
||||
init() { |
||||
super.init(frame: .zero) |
||||
|
||||
translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
leftButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) |
||||
displayTimeString() |
||||
|
||||
let rightView = makeRightView() |
||||
let stackView = UIStackView(arrangedSubviews: [ |
||||
.spacerWidth(22), |
||||
leftButton, |
||||
rightView, |
||||
]) |
||||
stackView.translatesAutoresizingMaskIntoConstraints = false |
||||
stackView.axis = .horizontal |
||||
stackView.spacing = 0 |
||||
stackView.distribution = .fill |
||||
stackView.alignment = .center |
||||
addSubview(stackView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
stackView.leadingAnchor.constraint(equalTo: leadingAnchor), |
||||
stackView.trailingAnchor.constraint(equalTo: trailingAnchor), |
||||
stackView.topAnchor.constraint(equalTo: topAnchor), |
||||
stackView.bottomAnchor.constraint(equalTo: bottomAnchor), |
||||
]) |
||||
|
||||
configure() |
||||
} |
||||
|
||||
private func configure() { |
||||
layer.borderColor = Colors.appBackground.cgColor |
||||
layer.borderWidth = 1 |
||||
|
||||
leftButton.setTitleColor(Colors.appBackground, for: .normal) |
||||
leftButton.titleLabel?.font = Fonts.regular(size: 18) |
||||
} |
||||
|
||||
private func makeRightView() -> UIView { |
||||
let rightButton = UIButton(type: .system) |
||||
rightButton.translatesAutoresizingMaskIntoConstraints = false |
||||
rightButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 10) |
||||
rightButton.imageView?.contentMode = .scaleAspectFit |
||||
rightButton.setImage(R.image.time(), for: .normal) |
||||
rightButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) |
||||
|
||||
let rightView = UIStackView(arrangedSubviews: [ |
||||
rightButton, |
||||
]) |
||||
|
||||
rightView.translatesAutoresizingMaskIntoConstraints = false |
||||
rightView.distribution = .equalSpacing |
||||
rightView.spacing = 1 |
||||
rightView.axis = .horizontal |
||||
|
||||
return rightView |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
private func makeToolbarWithDoneButton() -> UIToolbar { |
||||
//Frame needed, but actual values aren't that important |
||||
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 100, height: 40)) |
||||
toolbar.barStyle = .default |
||||
|
||||
let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) |
||||
let done = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(closeKeyboard)) |
||||
|
||||
toolbar.items = [flexSpace, done] |
||||
toolbar.sizeToFit() |
||||
|
||||
return toolbar |
||||
} |
||||
|
||||
@objc func closeKeyboard() { |
||||
delegate?.didTap(in: self) |
||||
} |
||||
|
||||
@objc func buttonTapped() { |
||||
delegate?.didTap(in: self) |
||||
} |
||||
|
||||
private func displayTimeString() { |
||||
//TODO Should format be localized? |
||||
let formatter = DateFormatter() |
||||
formatter.timeStyle = .short |
||||
let timeString = formatter.string(from: value) |
||||
leftButton.setTitle(timeString, for: .normal) |
||||
} |
||||
} |
Loading…
Reference in new issue